diff --git a/Sources/ActivityPubKit/Entities/NodeInfoLinksDto.swift b/Sources/ActivityPubKit/Entities/NodeInfoLinksDto.swift new file mode 100644 index 00000000..c7078bb6 --- /dev/null +++ b/Sources/ActivityPubKit/Entities/NodeInfoLinksDto.swift @@ -0,0 +1,16 @@ +// +// https://mczachurski.dev +// Copyright © 2024 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +public struct NodeInfoLinksDto { + public let links: [NodeInfoLinkDto] + + public init(links: [NodeInfoLinkDto]) { + self.links = links + } +} + +extension NodeInfoLinksDto: Codable { } +extension NodeInfoLinksDto: Sendable { } diff --git a/Sources/ActivityPubKit/Entities/NodeInfoMetadataDto.swift b/Sources/ActivityPubKit/Entities/NodeInfoMetadataDto.swift index 83fd3015..065345e1 100644 --- a/Sources/ActivityPubKit/Entities/NodeInfoMetadataDto.swift +++ b/Sources/ActivityPubKit/Entities/NodeInfoMetadataDto.swift @@ -6,9 +6,11 @@ public struct NodeInfoMetadataDto { public let nodeName: String + public let nodeDescription: String - public init(nodeName: String) { + public init(nodeName: String, nodeDescription: String) { self.nodeName = nodeName + self.nodeDescription = nodeDescription } } diff --git a/Sources/VernissageServer/Application+Configure.swift b/Sources/VernissageServer/Application+Configure.swift index 94482182..daf6d95b 100644 --- a/Sources/VernissageServer/Application+Configure.swift +++ b/Sources/VernissageServer/Application+Configure.swift @@ -122,6 +122,9 @@ extension Application { try self.register(collection: UserAliasesController()) try self.register(collection: HealthController()) try self.register(collection: ErrorItemsController()) + + // Profile controller shuld be the last one (it registers: https://example.com/@johndoe). + try self.register(collection: ProfileController()) } private func registerMiddlewares() { @@ -447,12 +450,12 @@ extension Application { let password = try await self.services.settingsService.get(.emailPassword, on: self.db) let secureMethod = try await self.services.settingsService.get(.emailSecureMethod, on: self.db) - self.services.emailsService.setServerSettings(on: self, - hostName: hostName, + self.services.emailsService.setServerSettings(hostName: hostName, port: port, userName: userName, password: password, - secureMethod: secureMethod) + secureMethod: secureMethod, + on: self) } private func configureS3() { @@ -503,7 +506,7 @@ extension Application { private func configureJsonCoders() { // Create a new JSON encoder/decoder that uses unix-timestamp dates let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] encoder.dateEncodingStrategy = .customISO8601 let decoder = JSONDecoder() diff --git a/Sources/VernissageServer/Controllers/AccountController.swift b/Sources/VernissageServer/Controllers/AccountController.swift index 30a0bcc9..91ad8d43 100644 --- a/Sources/VernissageServer/Controllers/AccountController.swift +++ b/Sources/VernissageServer/Controllers/AccountController.swift @@ -154,13 +154,13 @@ struct AccountController { let usersService = request.application.services.usersService let isMachineTrusted = self.isMachineTrusted(on: request) - let user = try await usersService.login(on: request, - userNameOrEmail: loginRequestDto.userNameOrEmail, + let user = try await usersService.login(userNameOrEmail: loginRequestDto.userNameOrEmail, password: loginRequestDto.password, - isMachineTrusted: isMachineTrusted) + isMachineTrusted: isMachineTrusted, + on: request) let tokensService = request.application.services.tokensService - let accessToken = try await tokensService.createAccessTokens(on: request, forUser: user, useCookies: loginRequestDto.useCookies) + let accessToken = try await tokensService.createAccessTokens(forUser: user, useCookies: loginRequestDto.useCookies, on: request) return try await self.createAccessTokenResponse(on: request, accessToken: accessToken, @@ -235,13 +235,13 @@ struct AccountController { try ChangeEmailDto.validate(content: request) let usersService = request.application.services.usersService - try await usersService.validateEmail(on: request, email: changeEmailDto.email) + try await usersService.validateEmail(email: changeEmailDto.email, on: request) // Change email in database. try await usersService.changeEmail( - on: request, userId: authorizationPayloadId, - email: changeEmailDto.email + email: changeEmailDto.email, + on: request ) // Send email with email confirmation message. @@ -291,9 +291,9 @@ struct AccountController { throw ConfirmEmailError.invalidIdOrToken } - try await usersService.confirmEmail(on: request, - userId: userId, - confirmationGuid: confirmEmailRequestDto.confirmationGuid) + try await usersService.confirmEmail(userId: userId, + confirmationGuid: confirmEmailRequestDto.confirmationGuid, + on: request) return HTTPStatus.ok } @@ -346,9 +346,9 @@ struct AccountController { } let emailsService = request.application.services.emailsService - try await emailsService.dispatchConfirmAccountEmail(on: request, - user: user, - redirectBaseUrl: resendEmailConfirmationDto.redirectBaseUrl) + try await emailsService.dispatchConfirmAccountEmail(user: user, + redirectBaseUrl: resendEmailConfirmationDto.redirectBaseUrl, + on: request) return HTTPStatus.ok } @@ -401,10 +401,10 @@ struct AccountController { let usersService = request.application.services.usersService try await usersService.changePassword( - on: request, userId: authorizationPayloadId, currentPassword: changePasswordRequestDto.currentPassword, - newPassword: changePasswordRequestDto.newPassword + newPassword: changePasswordRequestDto.newPassword, + on: request ) return HTTPStatus.ok @@ -451,11 +451,11 @@ struct AccountController { let usersService = request.application.services.usersService let emailsService = request.application.services.emailsService - let user = try await usersService.forgotPassword(on: request, email: forgotPasswordRequestDto.email) + let user = try await usersService.forgotPassword(email: forgotPasswordRequestDto.email, on: request) - try await emailsService.dispatchForgotPasswordEmail(on: request, - user: user, - redirectBaseUrl: forgotPasswordRequestDto.redirectBaseUrl) + try await emailsService.dispatchForgotPasswordEmail(user: user, + redirectBaseUrl: forgotPasswordRequestDto.redirectBaseUrl, + on: request) return HTTPStatus.ok } @@ -505,9 +505,9 @@ struct AccountController { let usersService = request.application.services.usersService try await usersService.confirmForgotPassword( - on: request, forgotPasswordGuid: confirmationDto.forgotPasswordGuid, - password: confirmationDto.password + password: confirmationDto.password, + on: request ) return HTTPStatus.ok @@ -565,14 +565,14 @@ struct AccountController { let tokensService = request.application.services.tokensService - let refreshTokenFromDb = try await tokensService.validateRefreshToken(on: request, refreshToken: oldRefreshToken.refreshToken) - let user = try await tokensService.getUserByRefreshToken(on: request, refreshToken: refreshTokenFromDb.token) + let refreshTokenFromDb = try await tokensService.validateRefreshToken(refreshToken: oldRefreshToken.refreshToken, on: request) + let user = try await tokensService.getUserByRefreshToken(refreshToken: refreshTokenFromDb.token, on: request) - let accessToken = try await tokensService.updateAccessTokens(on: request, - forUser: user, + let accessToken = try await tokensService.updateAccessTokens(forUser: user, refreshToken: refreshTokenFromDb, regenerateRefreshToken: oldRefreshToken.regenerateRefreshToken, - useCookies: oldRefreshToken.useCookies) + useCookies: oldRefreshToken.useCookies, + on: request) return try await self.createAccessTokenResponse(on: request, accessToken: accessToken) } @@ -610,7 +610,7 @@ struct AccountController { let usersService = request.application.services.usersService let userNameNormalized = userName.deletingPrefix("@").uppercased() - let userFromDb = try await usersService.get(on: request.db, userName: userNameNormalized) + let userFromDb = try await usersService.get(userName: userNameNormalized, on: request.db) guard let user = userFromDb else { throw EntityNotFoundError.userNotFound @@ -622,7 +622,7 @@ struct AccountController { } let tokensService = request.application.services.tokensService - try await tokensService.revokeRefreshTokens(on: request, forUser: user) + try await tokensService.revokeRefreshTokens(forUser: user, on: request) return HTTPStatus.ok } @@ -806,7 +806,7 @@ struct AccountController { private func sendConfirmEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { let emailsService = request.application.services.emailsService - try await emailsService.dispatchConfirmAccountEmail(on: request, user: user, redirectBaseUrl: redirectBaseUrl) + try await emailsService.dispatchConfirmAccountEmail(user: user, redirectBaseUrl: redirectBaseUrl, on: request) } private func createAccessTokenResponse(on request: Request, accessToken: AccessTokens, trustMachine: Bool? = nil) async throws -> Response { diff --git a/Sources/VernissageServer/Controllers/ActivityPubActorController.swift b/Sources/VernissageServer/Controllers/ActivityPubActorController.swift index fe254883..369d581c 100644 --- a/Sources/VernissageServer/Controllers/ActivityPubActorController.swift +++ b/Sources/VernissageServer/Controllers/ActivityPubActorController.swift @@ -77,7 +77,7 @@ struct ActivityPubActorController { /// - Parameters: /// - request: The Vapor request to the endpoint. /// - /// - Returns: List of countries. + /// - Returns: Main instance actor data. @Sendable func read(request: Request) async throws -> Response { let usersService = request.application.services.usersService @@ -144,7 +144,7 @@ struct ActivityPubActorController { // Skip requests from domains blocked by the instance. let activityPubService = request.application.services.activityPubService - if try await activityPubService.isDomainBlockedByInstance(on: request.application, activity: activityDto) { + if try await activityPubService.isDomainBlockedByInstance(activity: activityDto, on: request.executionContext) { request.logger.info("Activity blocked by instance (type: \(activityDto.type), id: '\(activityDto.id)', activityPubProfile: \(activityDto.actor.actorIds().first ?? "")") return HTTPStatus.ok } @@ -204,7 +204,7 @@ struct ActivityPubActorController { // Skip requests from domains blocked by the instance. let activityPubService = request.application.services.activityPubService - if try await activityPubService.isDomainBlockedByInstance(on: request.application, activity: activityDto) { + if try await activityPubService.isDomainBlockedByInstance(activity: activityDto, on: request.executionContext) { request.logger.info("Activity blocked by instance (type: \(activityDto.type), id: '\(activityDto.id)', activityPubProfile: \(activityDto.actor.actorIds().first ?? "")") return HTTPStatus.ok } diff --git a/Sources/VernissageServer/Controllers/ActivityPubActorsController.swift b/Sources/VernissageServer/Controllers/ActivityPubActorsController.swift index 3e0c679b..6f22b035 100644 --- a/Sources/VernissageServer/Controllers/ActivityPubActorsController.swift +++ b/Sources/VernissageServer/Controllers/ActivityPubActorsController.swift @@ -14,6 +14,7 @@ extension ActivityPubActorsController: RouteCollection { func boot(routes: RoutesBuilder) throws { let activityPubGroup = routes.grouped(ActivityPubActorsController.uri) + let statusesGroup = routes.grouped(StatusesController.uri) activityPubGroup .grouped(EventHandlerMiddleware(.activityPubRead)) @@ -35,9 +36,15 @@ extension ActivityPubActorsController: RouteCollection { .grouped(EventHandlerMiddleware(.activityPubFollowers)) .get(":name", "followers", use: followers) + // Support for: https://example.com/@johndoe/statuses/:id activityPubGroup .grouped(EventHandlerMiddleware(.activityPubStatus)) .get(":name", "statuses", ":id", use: status) + + // Support for: https://example.com/statuses/:id + statusesGroup + .grouped(EventHandlerMiddleware(.activityPubStatus)) + .get(":id", use: status) } } @@ -147,7 +154,8 @@ struct ActivityPubActorsController { } let usersService = request.application.services.usersService - let userFromDb = try await usersService.get(on: request.db, userName: userName) + let clearedUserName = userName.deletingPrefix("@") + let userFromDb = try await usersService.get(userName: clearedUserName, on: request.db) guard let user = userFromDb else { throw EntityNotFoundError.userNotFound @@ -173,8 +181,8 @@ struct ActivityPubActorsController { publicKey: PersonPublicKeyDto(id: "\(user.activityPubProfile)#main-key", owner: user.activityPubProfile, publicKeyPem: user.publicKey ?? ""), - icon: self.getPersonImage(for: user.avatarFileName, on: request), - image: self.getPersonImage(for: user.headerFileName, on: request), + icon: self.getPersonImage(for: user.avatarFileName, on: request.executionContext), + image: self.getPersonImage(for: user.headerFileName, on: request.executionContext), endpoints: PersonEndpointsDto(sharedInbox: "\(baseAddress)/shared/inbox"), attachment: attachments.map({ PersonAttachmentDto(name: $0.key ?? "", value: $0.htmlValue(baseAddress: baseAddress)) }), @@ -225,7 +233,7 @@ struct ActivityPubActorsController { // Skip requests from domains blocked by the instance. let activityPubService = request.application.services.activityPubService - if try await activityPubService.isDomainBlockedByInstance(on: request.application, activity: activityDto) { + if try await activityPubService.isDomainBlockedByInstance(activity: activityDto, on: request.executionContext) { request.logger.info("Activity blocked by instance (type: \(activityDto.type), user: '\(userName)', id: '\(activityDto.id)', activityPubProfile: \(activityDto.actor.actorIds().first ?? "")") return HTTPStatus.ok } @@ -289,7 +297,7 @@ struct ActivityPubActorsController { // Skip requests from domains blocked by the instance. let activityPubService = request.application.services.activityPubService - if try await activityPubService.isDomainBlockedByInstance(on: request.application, activity: activityDto) { + if try await activityPubService.isDomainBlockedByInstance(activity: activityDto, on: request.executionContext) { request.logger.info("Activity blocked by instance (type: \(activityDto.type), user: '\(userName)', id: '\(activityDto.id)', activityPubProfile: \(activityDto.actor.actorIds().first ?? "")") return HTTPStatus.ok } @@ -382,21 +390,26 @@ struct ActivityPubActorsController { let usersService = request.application.services.usersService let followsService = request.application.services.followsService - guard let user = try await usersService.get(on: request.db, userName: userName) else { + guard let user = try await usersService.get(userName: userName, on: request.db) else { throw Abort(.notFound) } let page: String? = request.query["page"] let userId = try user.requireID() - let totalItems = try await followsService.count(on: request.db, sourceId: userId) + let totalItems = try await followsService.count(sourceId: userId, on: request.db) if let page { guard let pageInt = Int(page) else { throw Abort(.badRequest) } - let following = try await followsService.following(on: request.db, sourceId: userId, onlyApproved: true, page: pageInt, size: orderdCollectionSize) + let following = try await followsService.following(sourceId: userId, + onlyApproved: true, + page: pageInt, + size: orderdCollectionSize, + on: request.db) + let showPrev = pageInt > 1 let showNext = (pageInt * orderdCollectionSize) < totalItems @@ -487,21 +500,26 @@ struct ActivityPubActorsController { let usersService = request.application.services.usersService let followsService = request.application.services.followsService - guard let user = try await usersService.get(on: request.db, userName: userName) else { + guard let user = try await usersService.get(userName: userName, on: request.db) else { throw Abort(.notFound) } let page: String? = request.query["page"] let userId = try user.requireID() - let totalItems = try await followsService.count(on: request.db, targetId: userId) + let totalItems = try await followsService.count(targetId: userId, on: request.db) if let page { guard let pageInt = Int(page) else { throw Abort(.badRequest) } - let follows = try await followsService.follows(on: request.db, targetId: userId, onlyApproved: true, page: pageInt, size: orderdCollectionSize) + let follows = try await followsService.follows(targetId: userId, + onlyApproved: true, + page: pageInt, + size: orderdCollectionSize, + on: request.db) + let showPrev = pageInt > 1 let showNext = (pageInt * orderdCollectionSize) < totalItems @@ -613,7 +631,7 @@ struct ActivityPubActorsController { } let statusesService = request.application.services.statusesService - guard let status = try await statusesService.get(on: request.db, id: id) else { + guard let status = try await statusesService.get(id: id, on: request.db) else { throw Abort(.notFound) } @@ -625,16 +643,16 @@ struct ActivityPubActorsController { throw Abort(.forbidden) } - let noteDto = try statusesService.note(basedOn: status, replyToStatus: nil, on: request.application) + let noteDto = try statusesService.note(basedOn: status, replyToStatus: nil, on: request.executionContext) return try await noteDto.encodeActivityResponse(for: request) } - private func getPersonImage(for fileName: String?, on request: Request) -> PersonImageDto? { + private func getPersonImage(for fileName: String?, on context: ExecutionContext) -> PersonImageDto? { guard let fileName else { return nil } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = context.application.services.storageService.getBaseStoragePath(on: context) return PersonImageDto(mediaType: "image/jpeg", url: "\(baseStoragePath)/\(fileName)") } diff --git a/Sources/VernissageServer/Controllers/ActivityPubSharedController.swift b/Sources/VernissageServer/Controllers/ActivityPubSharedController.swift index e1b20adc..01018302 100644 --- a/Sources/VernissageServer/Controllers/ActivityPubSharedController.swift +++ b/Sources/VernissageServer/Controllers/ActivityPubSharedController.swift @@ -145,7 +145,7 @@ struct ActivityPubSharedController { // Skip requests from domains blocked by the instance. let activityPubService = request.application.services.activityPubService - if try await activityPubService.isDomainBlockedByInstance(on: request.application, activity: activityDto) { + if try await activityPubService.isDomainBlockedByInstance(activity: activityDto, on: request.executionContext) { request.logger.info("Activity blocked by instance (type: \(activityDto.type), id: '\(activityDto.id)', activityPubProfile: \(activityDto.actor.actorIds().first ?? "")") return HTTPStatus.ok } diff --git a/Sources/VernissageServer/Controllers/AttachmentsController.swift b/Sources/VernissageServer/Controllers/AttachmentsController.swift index 3268b5bf..5e21eadf 100644 --- a/Sources/VernissageServer/Controllers/AttachmentsController.swift +++ b/Sources/VernissageServer/Controllers/AttachmentsController.swift @@ -142,7 +142,7 @@ struct AttachmentsController { // Save image to temp folder. let tmpOriginalFileUrl = try await temporaryFileService.save(fileName: attachmentRequest.file.filename, byteBuffer: attachmentRequest.file.data, - on: request) + on: request.executionContext) // Create image in the memory. guard let image = Image.create(path: tmpOriginalFileUrl) else { @@ -164,7 +164,7 @@ struct AttachmentsController { } // Save exported image in temp folder. - let tmpExportedFileUrl = try temporaryFileService.temporaryPath(on: request.application, based: attachmentRequest.file.filename) + let tmpExportedFileUrl = try temporaryFileService.temporaryPath(based: attachmentRequest.file.filename, on: request.executionContext) exported.write(to: tmpExportedFileUrl, quality: Constants.imageQuality) // Resize image. @@ -173,20 +173,20 @@ struct AttachmentsController { } // Save resized image in temp folder. - let tmpSmallFileUrl = try temporaryFileService.temporaryPath(on: request.application, based: attachmentRequest.file.filename) + let tmpSmallFileUrl = try temporaryFileService.temporaryPath(based: attachmentRequest.file.filename, on: request.executionContext) resized.write(to: tmpSmallFileUrl, quality: Constants.imageQuality) // Save exported image. guard let savedExportedFileName = try await storageService.save(fileName: attachmentRequest.file.filename, url: tmpExportedFileUrl, - on: request) else { + on: request.executionContext) else { throw AttachmentError.savedFailed } // Save small image. guard let savedSmallFileName = try await storageService.save(fileName: attachmentRequest.file.filename, url: tmpSmallFileUrl, - on: request) else { + on: request.executionContext) else { throw AttachmentError.savedFailed } @@ -221,7 +221,7 @@ struct AttachmentsController { try await temporaryFileService.delete(url: tmpExportedFileUrl, on: request) try await temporaryFileService.delete(url: tmpSmallFileUrl, on: request) - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let temporaryAttachmentDto = TemporaryAttachmentDto(from: attachment, originalFileName: savedExportedFileName, smallFileName: savedSmallFileName, @@ -327,12 +327,12 @@ struct AttachmentsController { // Save image to temp folder. let tmpOriginalHdrFileUrl = try await temporaryFileService.save(fileName: attachmentRequest.file.filename, byteBuffer: attachmentRequest.file.data, - on: request) + on: request.executionContext) // Save orginal image. guard let savedHdrFileName = try await storageService.save(fileName: attachmentRequest.file.filename, url: tmpOriginalHdrFileUrl, - on: request) else { + on: request.executionContext) else { throw AttachmentError.savedFailed } @@ -355,7 +355,7 @@ struct AttachmentsController { // Remove temporary files. try await temporaryFileService.delete(url: tmpOriginalHdrFileUrl, on: request) - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let temporaryAttachmentDto = TemporaryAttachmentDto(from: attachment, originalFileName: attachment.originalFile.fileName, smallFileName: attachment.smallFile.fileName, @@ -427,10 +427,10 @@ struct AttachmentsController { if let orginalHdrFileName = attachment.originalHdrFile?.fileName { request.logger.info("Delete orginal HDR file from storage: \(orginalHdrFileName).") - try await storageService.delete(fileName: orginalHdrFileName, on: request) + try await storageService.delete(fileName: orginalHdrFileName, on: request.executionContext) } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let temporaryAttachmentDto = TemporaryAttachmentDto(from: attachment, originalFileName: attachment.originalFile.fileName, smallFileName: attachment.smallFile.fileName, @@ -646,14 +646,14 @@ struct AttachmentsController { // Remove files from external storage provider. request.logger.info("Delete orginal file from storage: \(attachment.originalFile.fileName).") - try await storageService.delete(fileName: attachment.originalFile.fileName, on: request) + try await storageService.delete(fileName: attachment.originalFile.fileName, on: request.executionContext) request.logger.info("Delete small file from storage: \(attachment.smallFile.fileName).") - try await storageService.delete(fileName: attachment.smallFile.fileName, on: request) + try await storageService.delete(fileName: attachment.smallFile.fileName, on: request.executionContext) if let orginalHdrFileName = attachment.originalHdrFile?.fileName { request.logger.info("Delete orginal HDR file from storage: \(orginalHdrFileName).") - try await storageService.delete(fileName: orginalHdrFileName, on: request) + try await storageService.delete(fileName: orginalHdrFileName, on: request.executionContext) } return HTTPStatus.ok @@ -736,7 +736,7 @@ struct AttachmentsController { let openAIService = request.application.services.openAIService let storageService = request.application.services.storageService - let baseStoragePath = storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = storageService.getBaseStoragePath(on: request.executionContext) let previewUrl = AttachmentDto.getPreviewUrl(attachment: attachment, baseStoragePath: baseStoragePath) let description = try await openAIService.generateImageDescription(imageUrl: previewUrl, model: openAIModel, apiKey: openAIKey) @@ -820,7 +820,7 @@ struct AttachmentsController { let openAIService = request.application.services.openAIService let storageService = request.application.services.storageService - let baseStoragePath = storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = storageService.getBaseStoragePath(on: request.executionContext) let previewUrl = AttachmentDto.getPreviewUrl(attachment: attachment, baseStoragePath: baseStoragePath) let hashtags = try await openAIService.generateHashtags(imageUrl: previewUrl, model: openAIModel, apiKey: openAIKey) diff --git a/Sources/VernissageServer/Controllers/AuthenticationClientsController.swift b/Sources/VernissageServer/Controllers/AuthenticationClientsController.swift index 9c4c394a..24fc9d47 100644 --- a/Sources/VernissageServer/Controllers/AuthenticationClientsController.swift +++ b/Sources/VernissageServer/Controllers/AuthenticationClientsController.swift @@ -61,7 +61,7 @@ struct AuthenticationClientsController { let authClientDto = try request.content.decode(AuthClientDto.self) try AuthClientDto.validate(content: request) - try await authClientsService.validateUri(on: request.db, uri: authClientDto.uri, authClientId: nil) + try await authClientsService.validate(uri: authClientDto.uri, authClientId: nil, on: request.db) let authClient = try await self.createAuthClient(on: request, authClientDto: authClientDto) let response = try await self.createNewAuthClientResponse(on: request, authClient: authClient) @@ -115,7 +115,7 @@ struct AuthenticationClientsController { throw EntityNotFoundError.authClientNotFound } - try await authClientsService.validateUri(on: request.db, uri: authClientDto.uri, authClientId: authClient.id) + try await authClientsService.validate(uri: authClientDto.uri, authClientId: authClient.id, on: request.db) try await self.updateAuthClient(on: request, from: authClientDto, to: authClient) return AuthClientDto(from: authClient) diff --git a/Sources/VernissageServer/Controllers/AvatarsController.swift b/Sources/VernissageServer/Controllers/AvatarsController.swift index c3bbb51a..d6fb334c 100644 --- a/Sources/VernissageServer/Controllers/AvatarsController.swift +++ b/Sources/VernissageServer/Controllers/AvatarsController.swift @@ -95,11 +95,11 @@ struct AvatarsController { } let usersService = request.application.services.usersService - guard usersService.isSignedInUser(on: request, userName: userName) else { + guard usersService.isSignedInUser(userName: userName, on: request) else { throw EntityForbiddenError.userForbidden } - guard let userFromDb = try await usersService.get(on: request.db, userName: request.userNameNormalized) else { + guard let userFromDb = try await usersService.get(userName: request.userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -112,7 +112,7 @@ struct AvatarsController { let temporaryFileService = request.application.services.temporaryFileService let tmpFileUrl = try await temporaryFileService.save(fileName: avatar.file.filename, byteBuffer: avatar.file.data, - on: request) + on: request.executionContext) // Create image in the memory. guard let image = Image.create(path: tmpFileUrl) else { @@ -133,12 +133,12 @@ struct AvatarsController { } // Save resized image. - let resizedTmpFileUrl = try temporaryFileService.temporaryPath(on: request.application, based: avatar.file.filename) + let resizedTmpFileUrl = try temporaryFileService.temporaryPath(based: avatar.file.filename, on: request.executionContext) resized.write(to: resizedTmpFileUrl, quality: Constants.imageQuality) // Update user's avatar. let storageService = request.application.services.storageService - guard let savedFileName = try await storageService.save(fileName: avatar.file.filename, url: resizedTmpFileUrl, on: request) else { + guard let savedFileName = try await storageService.save(fileName: avatar.file.filename, url: resizedTmpFileUrl, on: request.executionContext) else { throw AvatarError.savedFailed } @@ -183,11 +183,11 @@ struct AvatarsController { } let usersService = request.application.services.usersService - guard usersService.isSignedInUser(on: request, userName: userName) else { + guard usersService.isSignedInUser(userName: userName, on: request) else { throw EntityForbiddenError.userForbidden } - guard let userFromDb = try await usersService.get(on: request.db, userName: request.userNameNormalized) else { + guard let userFromDb = try await usersService.get(userName: request.userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -197,7 +197,7 @@ struct AvatarsController { // Update user's avatar. let storageService = request.application.services.storageService - try await storageService.delete(fileName: avatarFileName, on: request) + try await storageService.delete(fileName: avatarFileName, on: request.executionContext) // Delete user's avatar. userFromDb.avatarFileName = nil diff --git a/Sources/VernissageServer/Controllers/BookmarksController.swift b/Sources/VernissageServer/Controllers/BookmarksController.swift index 2caf2103..79204159 100644 --- a/Sources/VernissageServer/Controllers/BookmarksController.swift +++ b/Sources/VernissageServer/Controllers/BookmarksController.swift @@ -150,10 +150,10 @@ struct BookmarksController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let statuses = try await timelineService.bookmarks(on: request.db, for: authorizationPayloadId, linkableParams: linkableParams) + let statuses = try await timelineService.bookmarks(for: authorizationPayloadId, linkableParams: linkableParams, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: statuses.data, on: request.executionContext) return LinkableResultDto( maxId: statuses.maxId, diff --git a/Sources/VernissageServer/Controllers/FavouritesController.swift b/Sources/VernissageServer/Controllers/FavouritesController.swift index c2c02e52..fbf74789 100644 --- a/Sources/VernissageServer/Controllers/FavouritesController.swift +++ b/Sources/VernissageServer/Controllers/FavouritesController.swift @@ -150,10 +150,10 @@ struct FavouritesController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let statuses = try await timelineService.favourites(on: request.db, for: authorizationPayloadId, linkableParams: linkableParams) + let statuses = try await timelineService.favourites(for: authorizationPayloadId, linkableParams: linkableParams, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: statuses.data, on: request.executionContext) return LinkableResultDto( maxId: statuses.maxId, diff --git a/Sources/VernissageServer/Controllers/FollowRequestsController.swift b/Sources/VernissageServer/Controllers/FollowRequestsController.swift index 49056029..0a1f98de 100644 --- a/Sources/VernissageServer/Controllers/FollowRequestsController.swift +++ b/Sources/VernissageServer/Controllers/FollowRequestsController.swift @@ -100,7 +100,7 @@ struct FollowRequestsController { let linkableParams = request.linkableParams() let followsService = request.application.services.followsService - let linkableResult = try await followsService.toApprove(on: request, userId: authorizationPayloadId, linkableParams: linkableParams) + let linkableResult = try await followsService.toApprove(userId: authorizationPayloadId, linkableParams: linkableParams, on: request.executionContext) return LinkableResultDto(basedOn: linkableResult) } @@ -168,7 +168,7 @@ struct FollowRequestsController { } let followsService = request.application.services.followsService - guard let follow = try await followsService.get(on: request.db, sourceId: userId, targetId: authorizationPayloadId) else { + guard let follow = try await followsService.get(sourceId: userId, targetId: authorizationPayloadId, on: request.db) else { throw FollowRequestError.missingFollowEntity(userId, authorizationPayloadId) } @@ -181,7 +181,7 @@ struct FollowRequestsController { } // Approve in local database. - try await followsService.approve(on: request.db, sourceId: userId, targetId: authorizationPayloadId) + try await followsService.approve(sourceId: userId, targetId: authorizationPayloadId, on: request.db) // Send information to remote server (for remote accounts) server about approved follow. if sourceUser.isLocal == false { @@ -196,7 +196,7 @@ struct FollowRequestsController { } let relationshipsService = request.application.services.relationshipsService - let relationships = try await relationshipsService.relationships(on: request.db, userId: authorizationPayloadId, relatedUserIds: [userId]) + let relationships = try await relationshipsService.relationships(userId: authorizationPayloadId, relatedUserIds: [userId], on: request.db) return relationships.first ?? RelationshipDto( userId: id, following: false, @@ -272,7 +272,7 @@ struct FollowRequestsController { } let followsService = request.application.services.followsService - guard let follow = try await followsService.get(on: request.db, sourceId: userId, targetId: authorizationPayloadId) else { + guard let follow = try await followsService.get(sourceId: userId, targetId: authorizationPayloadId, on: request.db) else { throw FollowRequestError.missingFollowEntity(userId, authorizationPayloadId) } @@ -285,7 +285,7 @@ struct FollowRequestsController { } // Reject in local database. - try await followsService.reject(on: request.db, sourceId: userId, targetId: authorizationPayloadId) + try await followsService.reject(sourceId: userId, targetId: authorizationPayloadId, on: request.db) // Send information to remote server (for remote accounts) server about rejected follow. if sourceUser.isLocal == false { @@ -300,7 +300,7 @@ struct FollowRequestsController { } let relationshipsService = request.application.services.relationshipsService - let relationships = try await relationshipsService.relationships(on: request.db, userId: authorizationPayloadId, relatedUserIds: [userId]) + let relationships = try await relationshipsService.relationships(userId: authorizationPayloadId, relatedUserIds: [userId], on: request.db) return relationships.first ?? RelationshipDto( userId: id, following: false, diff --git a/Sources/VernissageServer/Controllers/HeadersController.swift b/Sources/VernissageServer/Controllers/HeadersController.swift index 88c4ac17..de0052d4 100644 --- a/Sources/VernissageServer/Controllers/HeadersController.swift +++ b/Sources/VernissageServer/Controllers/HeadersController.swift @@ -96,11 +96,11 @@ struct HeadersController { } let usersService = request.application.services.usersService - guard usersService.isSignedInUser(on: request, userName: userName) else { + guard usersService.isSignedInUser(userName: userName, on: request) else { throw EntityForbiddenError.userForbidden } - guard let userFromDb = try await usersService.get(on: request.db, userName: request.userNameNormalized) else { + guard let userFromDb = try await usersService.get(userName: request.userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -112,7 +112,7 @@ struct HeadersController { let temporaryFileService = request.application.services.temporaryFileService let tmpFileUrl = try await temporaryFileService.save(fileName: header.file.filename, byteBuffer: header.file.data, - on: request) + on: request.executionContext) // Create image in the memory. guard let image = Image.create(path: tmpFileUrl) else { @@ -133,12 +133,12 @@ struct HeadersController { } // Save resized image. - let resizedTmpFileUrl = try temporaryFileService.temporaryPath(on: request.application, based: header.file.filename) + let resizedTmpFileUrl = try temporaryFileService.temporaryPath(based: header.file.filename, on: request.executionContext) resized.write(to: resizedTmpFileUrl, quality: Constants.imageQuality) // Update user's header. let storageService = request.application.services.storageService - guard let savedFileName = try await storageService.save(fileName: header.file.filename, url: resizedTmpFileUrl, on: request) else { + guard let savedFileName = try await storageService.save(fileName: header.file.filename, url: resizedTmpFileUrl, on: request.executionContext) else { throw HeaderError.savedFailed } @@ -183,11 +183,11 @@ struct HeadersController { } let usersService = request.application.services.usersService - guard usersService.isSignedInUser(on: request, userName: userName) else { + guard usersService.isSignedInUser(userName: userName, on: request) else { throw EntityForbiddenError.userForbidden } - guard let userFromDb = try await usersService.get(on: request.db, userName: request.userNameNormalized) else { + guard let userFromDb = try await usersService.get(userName: request.userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -197,7 +197,7 @@ struct HeadersController { // Update user's avatar. let storageService = request.application.services.storageService - try await storageService.delete(fileName: headerFileName, on: request) + try await storageService.delete(fileName: headerFileName, on: request.executionContext) // Delete user's avatar. userFromDb.headerFileName = nil diff --git a/Sources/VernissageServer/Controllers/HealthController.swift b/Sources/VernissageServer/Controllers/HealthController.swift index 55f2f05c..0ea6543d 100644 --- a/Sources/VernissageServer/Controllers/HealthController.swift +++ b/Sources/VernissageServer/Controllers/HealthController.swift @@ -114,7 +114,7 @@ struct HealthController { } let storageService = request.application.services.storageService - _ = try await storageService.get(fileName: file.fileName, on: request) + _ = try await storageService.get(fileName: file.fileName, on: request.executionContext) return true } catch let error as S3ErrorType { diff --git a/Sources/VernissageServer/Controllers/IdentityController.swift b/Sources/VernissageServer/Controllers/IdentityController.swift index aad0cb52..09e1445c 100644 --- a/Sources/VernissageServer/Controllers/IdentityController.swift +++ b/Sources/VernissageServer/Controllers/IdentityController.swift @@ -80,7 +80,7 @@ struct IdentityController { let oauthUserFromToken = try await self.getOAuthUser(on: request, from: response, type: authClientFromDb.type) // Check if external user is registered. - let (user, externalUser) = try await externalUsersService.getRegisteredExternalUser(on: request.db, user: oauthUserFromToken) + let (user, externalUser) = try await externalUsersService.getRegisteredExternalUser(user: oauthUserFromToken, on: request.db) // Create user if not exists. let createdUser = try await self.createUserIfNotExists(on: request, userFromDb: user, oauthUser: oauthUserFromToken) @@ -110,9 +110,9 @@ struct IdentityController { let loginRequestDto = try request.content.decode(ExternalLoginRequestDto.self) let usersService = request.application.services.usersService - let user = try await usersService.login(on: request, authenticateToken: loginRequestDto.authenticateToken) + let user = try await usersService.login(authenticateToken: loginRequestDto.authenticateToken, on: request) let tokensService = request.application.services.tokensService - let accessToken = try await tokensService.createAccessTokens(on: request, forUser: user, useCookies: false) + let accessToken = try await tokensService.createAccessTokens(forUser: user, useCookies: false, on: request) return accessToken.toAccessTokenDto() } diff --git a/Sources/VernissageServer/Controllers/InstanceController.swift b/Sources/VernissageServer/Controllers/InstanceController.swift index 4464f579..1d01f856 100644 --- a/Sources/VernissageServer/Controllers/InstanceController.swift +++ b/Sources/VernissageServer/Controllers/InstanceController.swift @@ -111,8 +111,8 @@ struct InstanceController { let usersService = request.application.services.usersService let statusesService = request.application.services.statusesService - let userCount = try await usersService.count(on: request.db, sinceLastLoginDate: nil) - let statusCount = try await statusesService.count(on: request.db, onlyComments: false) + let userCount = try await usersService.count(sinceLastLoginDate: nil, on: request.db) + let statusCount = try await statusesService.count(onlyComments: false, on: request.db) let rules = try await Rule.query(on: request.db).sort(\.$order).all() let contactUser = try await self.getContactUser(appplicationSettings: appplicationSettings, on: request) @@ -150,11 +150,16 @@ struct InstanceController { } let usersService = request.application.services.usersService - guard let user = try await usersService.get(on: request.db, id: contactUserId) else { + guard let user = try await usersService.get(id: contactUserId, on: request.db) else { return nil } - let userDto = await usersService.convertToDto(on: request, user: user, flexiFields: user.flexiFields, roles: nil, attachSensitive: false) + let userDto = await usersService.convertToDto(user: user, + flexiFields: user.flexiFields, + roles: nil, + attachSensitive: false, + on: request.executionContext) + return userDto } } diff --git a/Sources/VernissageServer/Controllers/InvitationsController.swift b/Sources/VernissageServer/Controllers/InvitationsController.swift index e2226eea..10ed5d27 100644 --- a/Sources/VernissageServer/Controllers/InvitationsController.swift +++ b/Sources/VernissageServer/Controllers/InvitationsController.swift @@ -83,7 +83,7 @@ struct InvitationsController { throw Abort(.forbidden) } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let baseAddress = request.application.settings.cached?.baseAddress ?? "" let invitationsFromDatabase = try await Invitation.query(on: request.db) @@ -146,7 +146,7 @@ struct InvitationsController { throw InvitationError.maximumNumberOfInvitationsGenerated } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let baseAddress = request.application.settings.cached?.baseAddress ?? "" let id = request.application.services.snowflakeService.generate() diff --git a/Sources/VernissageServer/Controllers/NodeInfoController.swift b/Sources/VernissageServer/Controllers/NodeInfoController.swift index 72477db6..90761c61 100644 --- a/Sources/VernissageServer/Controllers/NodeInfoController.swift +++ b/Sources/VernissageServer/Controllers/NodeInfoController.swift @@ -98,17 +98,17 @@ struct NodeInfoController { let appplicationSettings = request.application.settings.cached let isRegistrationOpened = appplicationSettings?.isRegistrationOpened ?? false - let baseAddress = appplicationSettings?.baseAddress ?? "http://localhost" - let nodeName = URL(string: baseAddress)?.host ?? "unkonwn" + let nodeName = appplicationSettings?.webTitle ?? "unkonwn" + let nodeDescription = appplicationSettings?.webDescription ?? "unkonwn" let usersService = request.application.services.usersService - let totalUsers = try await usersService.count(on: request.db, sinceLastLoginDate: nil) - let activeMonth = try await usersService.count(on: request.db, sinceLastLoginDate: Date.monthAgo) - let activeHalfyear = try await usersService.count(on: request.db, sinceLastLoginDate: Date.halfYearAgo) + let totalUsers = try await usersService.count(sinceLastLoginDate: nil, on: request.db) + let activeMonth = try await usersService.count(sinceLastLoginDate: Date.monthAgo, on: request.db) + let activeHalfyear = try await usersService.count(sinceLastLoginDate: Date.halfYearAgo, on: request.db) let statusesService = request.application.services.statusesService - let localPosts = try await statusesService.count(on: request.db, onlyComments: false) - let localComments = try await statusesService.count(on: request.db, onlyComments: true) + let localPosts = try await statusesService.count(onlyComments: false, on: request.db) + let localComments = try await statusesService.count(onlyComments: true, on: request.db) let nodeInfoDto = NodeInfoDto(version: "2.0", openRegistrations: isRegistrationOpened, @@ -120,7 +120,8 @@ struct NodeInfoController { activeHalfyear: activeHalfyear), localPosts: localPosts, localComments: localComments), - metadata: NodeInfoMetadataDto(nodeName: nodeName)) + metadata: NodeInfoMetadataDto(nodeName: nodeName, + nodeDescription: nodeDescription)) try? await request.cache.set(nodeInfoCacheKey, to: nodeInfoDto, expiresIn: .minutes(10)) return nodeInfoDto diff --git a/Sources/VernissageServer/Controllers/NotificationsController.swift b/Sources/VernissageServer/Controllers/NotificationsController.swift index b314a2ab..9b7b5ac1 100644 --- a/Sources/VernissageServer/Controllers/NotificationsController.swift +++ b/Sources/VernissageServer/Controllers/NotificationsController.swift @@ -98,14 +98,18 @@ struct NotificationsController { let notificationsService = request.application.services.notificationsService let usersService = request.application.services.usersService - let notifications = try await notificationsService.list(on: request.db, for: authorizationPayloadId, linkableParams: linkableParams) + let notifications = try await notificationsService.list(for: authorizationPayloadId, linkableParams: linkableParams, on: request.db) let notificationDtos = await notifications.asyncMap({ let notificationTypeDto = NotificationTypeDto.from($0.notificationType) - let user = await usersService.convertToDto(on: request, user: $0.byUser, flexiFields: $0.byUser.flexiFields, roles: nil, attachSensitive: false) - let status = await self.getStatus($0.status, on: request) - + let user = await usersService.convertToDto(user: $0.byUser, + flexiFields: $0.byUser.flexiFields, + roles: nil, + attachSensitive: false, + on: request.executionContext) + + let status = await self.getStatus($0.status, on: request) return NotificationDto(id: $0.stringId(), notificationType: notificationTypeDto, byUser: user, status: status) }) @@ -217,6 +221,6 @@ struct NotificationsController { } let statusesService = request.application.services.statusesService - return await statusesService.convertToDto(on: request, status: status, attachments: status.attachments) + return await statusesService.convertToDto(status: status, attachments: status.attachments, on: request.executionContext) } } diff --git a/Sources/VernissageServer/Controllers/ProfileController.swift b/Sources/VernissageServer/Controllers/ProfileController.swift new file mode 100644 index 00000000..d526bda3 --- /dev/null +++ b/Sources/VernissageServer/Controllers/ProfileController.swift @@ -0,0 +1,126 @@ +// +// https://mczachurski.dev +// Copyright © 2024 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Vapor +import Fluent +import ActivityPubKit + +extension ProfileController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + + // Support for: https://example.com/@johndoe. + routes + .grouped(UserAuthenticator()) + .grouped(EventHandlerMiddleware(.usersRead)) + .get(":name", use: read) + } +} + +/// Controller for exposing user profile. +/// +/// The controller is created specificaly for supporting downloading +/// user accounts during search from other fediverse platforms. +/// +/// > Important: Base controller URL: `/:username`. +struct ProfileController { + let activityPubActorsController = ActivityPubActorsController() + + /// Returns user ActivityPub profile. + /// + /// Endpoint for download Activity Pub actor's data. One of the property is public key which should be used to validate requests + /// done (and signed by private key) by the user in all Activity Pub protocol methods. + /// + /// > Important: Endpoint URL: `/api/v1/actors`. + /// + /// **CURL request:** + /// + /// ```bash + /// curl "https://example.com/api/v1/actors/johndoe" \ + /// -X GET \ + /// -H "Content-Type: application/json" + /// ``` + /// + /// **Example response body:** + /// + /// ```json + /// { + /// "@context": [ + /// "https://w3id.org/security/v1", + /// "https://www.w3.org/ns/activitystreams" + /// ], + /// "attachment": [ + /// { + /// "name": "MASTODON", + /// "type": "PropertyValue", + /// "value": "https://mastodon.social/@johndoe" + /// }, + /// { + /// "name": "GITHUB", + /// "type": "PropertyValue", + /// "value": "https://github.com/johndoe" + /// } + /// ], + /// "endpoints": { + /// "sharedInbox": "https://example.com/shared/inbox" + /// }, + /// "followers": "https://example.com/actors/johndoe/followers", + /// "following": "https://example.com/actors/johndoe/following", + /// "icon": { + /// "mediaType": "image/jpeg", + /// "type": "Image", + /// "url": "https://s3.eu-central-1.amazonaws.com/instance/039ebf33d1664d5d849574d0e7191354.jpg" + /// }, + /// "id": "https://example.com/actors/johndoe", + /// "image": { + /// "mediaType": "image/jpeg", + /// "type": "Image", + /// "url": "https://s3.eu-central-1.amazonaws.com/instance/2ef4a0f69d0e410ba002df2212e2b63c.jpg" + /// }, + /// "inbox": "https://example.com/actors/johndoe/inbox", + /// "manuallyApprovesFollowers": false, + /// "name": "John Doe", + /// "outbox": "https://example.com/actors/johndoe/outbox", + /// "preferredUsername": "johndoe", + /// "publicKey": { + /// "id": "https://example.com/actors/johndoe#main-key", + /// "owner": "https://example.com/actors/johndoe", + /// "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nM0Q....AB\n-----END PUBLIC KEY-----" + /// }, + /// "summary": "#iOS/#dotNET developer, #Apple  fanboy, 📷 aspiring photographer", + /// "tag": [ + /// { + /// "href": "https://example.com/tags/Apple", + /// "name": "Apple", + /// "type": "Hashtag" + /// }, + /// { + /// "href": "https://example.com/tags/dotNET", + /// "name": "dotNET", + /// "type": "Hashtag" + /// }, + /// { + /// "href": "https://example.com/tags/iOS", + /// "name": "iOS", + /// "type": "Hashtag" + /// } + /// ], + /// "type": "Person", + /// "url": "https://example.com/@johndoe", + /// "alsoKnownAs": [ + /// "https://test.social/users/marcin" + /// ] + /// } + /// ``` + /// + /// - Parameters: + /// - request: The Vapor request to the endpoint. + /// + /// - Returns: Information about user information. + @Sendable + func read(request: Request) async throws -> Response { + return try await activityPubActorsController.read(request: request) + } +} diff --git a/Sources/VernissageServer/Controllers/RegisterController.swift b/Sources/VernissageServer/Controllers/RegisterController.swift index d82606a9..483e0112 100644 --- a/Sources/VernissageServer/Controllers/RegisterController.swift +++ b/Sources/VernissageServer/Controllers/RegisterController.swift @@ -163,8 +163,8 @@ struct RegisterController { // Validate userName and email. let usersService = request.application.services.usersService - try await usersService.validateUserName(on: request, userName: registerUserDto.userName) - try await usersService.validateEmail(on: request, email: registerUserDto.email) + try await usersService.validateUserName(userName: registerUserDto.userName, on: request) + try await usersService.validateEmail(email: registerUserDto.email, on: request) // Save user in database. let user = try await self.createUser(on: request, registerUserDto: registerUserDto) @@ -175,7 +175,7 @@ struct RegisterController { // When invitation token has been specified we have to mark it as used. if let inviteToken = registerUserDto.inviteToken { let invitationsService = request.application.services.invitationsService - try await invitationsService.use(code: inviteToken, on: request.db, for: user) + try await invitationsService.use(code: inviteToken, for: user, on: request.db) } // Send notification when new who needs approval registered. @@ -224,7 +224,7 @@ struct RegisterController { } let usersService = request.application.services.usersService - let result = try await usersService.isUserNameTaken(on: request, userName: userName) + let result = try await usersService.isUserNameTaken(userName: userName, on: request) return BooleanResponseDto(result: result) } @@ -263,7 +263,7 @@ struct RegisterController { } let usersService = request.application.services.usersService - let result = try await usersService.isEmailConnected(on: request, email: email) + let result = try await usersService.isEmailConnected(email: email, on: request) return BooleanResponseDto(result: result) } @@ -277,7 +277,7 @@ struct RegisterController { } let captchaService = request.application.services.captchaService - let success = try await captchaService.validate(on: request, captchaFormResponse: captchaToken) + let success = try await captchaService.validate(captchaFormResponse: captchaToken, on: request) if !success { throw RegisterError.securityTokenIsInvalid } @@ -333,12 +333,16 @@ struct RegisterController { private func sendNewUserEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { let emailsService = request.application.services.emailsService - try await emailsService.dispatchConfirmAccountEmail(on: request, user: user, redirectBaseUrl: redirectBaseUrl) + try await emailsService.dispatchConfirmAccountEmail(user: user, redirectBaseUrl: redirectBaseUrl, on: request) } private func createNewUserResponse(on request: Request, user: User, flexiFields: [FlexiField]) async throws -> Response { let usersService = request.application.services.usersService - let createdUserDto = await usersService.convertToDto(on: request, user: user, flexiFields: user.flexiFields, roles: nil, attachSensitive: true) + let createdUserDto = await usersService.convertToDto(user: user, + flexiFields: user.flexiFields, + roles: nil, + attachSensitive: true, + on: request.executionContext) var headers = HTTPHeaders() headers.replaceOrAdd(name: .location, value: "/\(UsersController.uri)/@\(user.userName)") @@ -393,7 +397,7 @@ struct RegisterController { let moderators = try await usersService.getModerators(on: request.db) for moderator in moderators { - try await notificationsService.create(type: .adminSignUp, to: moderator, by: user.requireID(), statusId: nil, on: request) + try await notificationsService.create(type: .adminSignUp, to: moderator, by: user.requireID(), statusId: nil, on: request.executionContext) } } } diff --git a/Sources/VernissageServer/Controllers/RelationshipsController.swift b/Sources/VernissageServer/Controllers/RelationshipsController.swift index 220c656a..08433b87 100644 --- a/Sources/VernissageServer/Controllers/RelationshipsController.swift +++ b/Sources/VernissageServer/Controllers/RelationshipsController.swift @@ -99,6 +99,6 @@ struct RelationshipsController { }) let relationshipsService = request.application.services.relationshipsService - return try await relationshipsService.relationships(on: request.db, userId: authorizationPayloadId, relatedUserIds: ids) + return try await relationshipsService.relationships(userId: authorizationPayloadId, relatedUserIds: ids, on: request.db) } } diff --git a/Sources/VernissageServer/Controllers/ReportsController.swift b/Sources/VernissageServer/Controllers/ReportsController.swift index 375bdc0a..dc06348b 100644 --- a/Sources/VernissageServer/Controllers/ReportsController.swift +++ b/Sources/VernissageServer/Controllers/ReportsController.swift @@ -116,7 +116,7 @@ struct ReportsController { /// - Returns: List of paginable reports. @Sendable func list(request: Request) async throws -> PaginableResultDto { - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let baseAddress = request.application.settings.cached?.baseAddress ?? "" let page: Int = request.query["page"] ?? 0 @@ -296,7 +296,7 @@ struct ReportsController { throw EntityNotFoundError.reportNotFound } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let baseAddress = request.application.settings.cached?.baseAddress ?? "" let statusDto = try? await self.getStatusDto(report: reportFromDatabase, on: request) @@ -371,7 +371,7 @@ struct ReportsController { throw EntityNotFoundError.reportNotFound } - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let baseAddress = request.application.settings.cached?.baseAddress ?? "" let statusDto = try? await self.getStatusDto(report: reportFromDatabase, on: request) @@ -388,7 +388,7 @@ struct ReportsController { return nil } - return await statusesService.convertToDto(on: request, status: status, attachments: status.attachments) + return await statusesService.convertToDto(status: status, attachments: status.attachments, on: request.executionContext) } private func sendNotifications(user: User, on request: Request) async throws { @@ -397,7 +397,7 @@ struct ReportsController { let moderators = try await usersService.getModerators(on: request.db) for moderator in moderators { - try await notificationsService.create(type: .adminReport, to: moderator, by: user.requireID(), statusId: nil, on: request) + try await notificationsService.create(type: .adminReport, to: moderator, by: user.requireID(), statusId: nil, on: request.executionContext) } } } diff --git a/Sources/VernissageServer/Controllers/SearchController.swift b/Sources/VernissageServer/Controllers/SearchController.swift index 53e24699..05a4d509 100644 --- a/Sources/VernissageServer/Controllers/SearchController.swift +++ b/Sources/VernissageServer/Controllers/SearchController.swift @@ -100,7 +100,7 @@ struct SearchController { // Execute proper search. let searchService = request.application.services.searchService - return try await searchService.search(query: query, searchType: searchType, request: request) + return try await searchService.search(query: query, searchType: searchType, on: request.executionContext) } private func getSearchType(from typeString: String?) -> SearchTypeDto { diff --git a/Sources/VernissageServer/Controllers/SettingsController.swift b/Sources/VernissageServer/Controllers/SettingsController.swift index b937c3d2..547be595 100644 --- a/Sources/VernissageServer/Controllers/SettingsController.swift +++ b/Sources/VernissageServer/Controllers/SettingsController.swift @@ -605,11 +605,11 @@ struct SettingsController { let secureMethod = try await settingsService.get(.emailSecureMethod, on: request.db) let emailsService = request.application.services.emailsService - emailsService.setServerSettings(on: request.application, - hostName: hostName, + emailsService.setServerSettings(hostName: hostName, port: port, userName: userName, password: password, - secureMethod: secureMethod) + secureMethod: secureMethod, + on: request.application) } } diff --git a/Sources/VernissageServer/Controllers/StatusesController.swift b/Sources/VernissageServer/Controllers/StatusesController.swift index a5a1aa60..475a05b9 100644 --- a/Sources/VernissageServer/Controllers/StatusesController.swift +++ b/Sources/VernissageServer/Controllers/StatusesController.swift @@ -350,10 +350,10 @@ struct StatusesController { try await statusMention.save(on: database) } - try await request.application.services.statusesService.updateStatusCount(on: database, for: authorizationPayloadId) + try await request.application.services.statusesService.updateStatusCount(for: authorizationPayloadId, on: database) } - let statusFromDatabase = try await request.application.services.statusesService.get(on: request.db, id: status.requireID()) + let statusFromDatabase = try await request.application.services.statusesService.get(id: status.requireID(), on: request.db) guard let statusFromDatabase else { throw EntityNotFoundError.statusNotFound } @@ -486,8 +486,8 @@ struct StatusesController { if let authorizationPayloadId { // For signed in users we can return public statuses and all his own statuses. - let linkableStatuses = try await statusesService.statuses(for: authorizationPayloadId, linkableParams: linkableParams, on: request) - let statusDtos = await statusesService.convertToDtos(on: request, statuses: linkableStatuses.data) + let linkableStatuses = try await statusesService.statuses(for: authorizationPayloadId, linkableParams: linkableParams, on: request.executionContext) + let statusDtos = await statusesService.convertToDtos(statuses: linkableStatuses.data, on: request.executionContext) return LinkableResultDto( maxId: linkableStatuses.maxId, @@ -496,8 +496,8 @@ struct StatusesController { ) } else { // For anonymous users we can return only public statuses. - let linkableStatuses = try await statusesService.statuses(linkableParams: linkableParams, on: request) - let statusDtos = await statusesService.convertToDtos(on: request, statuses: linkableStatuses.data) + let linkableStatuses = try await statusesService.statuses(linkableParams: linkableParams, on: request.executionContext) + let statusDtos = await statusesService.convertToDtos(statuses: linkableStatuses.data, on: request.executionContext) return LinkableResultDto( maxId: linkableStatuses.maxId, @@ -643,12 +643,12 @@ struct StatusesController { } let statusServices = request.application.services.statusesService - let canView = try await statusServices.can(view: status, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusServices.can(view: status, authorizationPayloadId: authorizationPayloadId, on: request.executionContext) guard canView else { throw EntityNotFoundError.statusNotFound } - return await statusServices.convertToDto(on: request, status: status, attachments: status.attachments) + return await statusServices.convertToDto(status: status, attachments: status.attachments, on: request.executionContext) } else { let status = try await Status.query(on: request.db) .filter(\.$id == statusId) @@ -673,7 +673,7 @@ struct StatusesController { } let statusServices = request.application.services.statusesService - return await statusServices.convertToDto(on: request, status: status, attachments: status.attachments) + return await statusServices.convertToDto(status: status, attachments: status.attachments, on: request.executionContext) } } @@ -776,7 +776,7 @@ struct StatusesController { // Remove from user's timelines. let statusesService = request.application.services.statusesService - try await statusesService.unlist(on: request.db, statusId: status.requireID()) + try await statusesService.unlist(statusId: status.requireID(), on: request.db) // Remove from remote servers. if status.isLocal { @@ -931,8 +931,8 @@ struct StatusesController { let ancestors = try await statusesService.ancestors(for: statusId, on: request.db) let descendants = try await statusesService.descendants(for: statusId, on: request.db) - let ancestorsDtos = await statusesService.convertToDtos(on: request, statuses: ancestors) - let descendantsDtos = await statusesService.convertToDtos(on: request, statuses: descendants) + let ancestorsDtos = await statusesService.convertToDtos(statuses: ancestors, on: request.executionContext) + let descendantsDtos = await statusesService.convertToDtos(statuses: descendants, on: request.executionContext) return StatusContextDto(ancestors: ancestorsDtos, descendants: descendantsDtos) } @@ -1064,7 +1064,7 @@ struct StatusesController { } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeReblog, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeReblog, authorizationPayloadId: authorizationPayloadId, on: request.executionContext) guard canView else { throw EntityNotFoundError.statusNotFound } @@ -1099,21 +1099,21 @@ struct StatusesController { to: statusFromDatabaseBeforeReblog.user, by: authorizationPayloadId, statusId: statusId, - on: request) + on: request.executionContext) try await request .queues(.statusReblogger) .dispatch(StatusRebloggerJob.self, status.requireID()) // Prepare and return status. - let statusFromDatabaseAfterReblog = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterReblog = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterReblog else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterReblog, - attachments: statusFromDatabaseAfterReblog.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterReblog, + attachments: statusFromDatabaseAfterReblog.attachments, + on: request.executionContext) } /// Unreblog (revert boost) specific status. @@ -1233,7 +1233,7 @@ struct StatusesController { // Download main (reblogged) status. guard let mainStatusId = statusFromDatabaseBeforeUnreblog.$reblog.id, - let mainStatus = try await statusesService.get(on: request.db, id: mainStatusId) else { + let mainStatus = try await statusesService.get(id: mainStatusId, on: request.db) else { throw EntityNotFoundError.statusNotFound } @@ -1263,14 +1263,14 @@ struct StatusesController { .dispatch(StatusUnrebloggerJob.self, activityPubUnreblogDto) // Prepare and return status. - let statusFromDatabaseAfterUnreblog = try await statusesService.get(on: request.db, id: mainStatusId) + let statusFromDatabaseAfterUnreblog = try await statusesService.get(id: mainStatusId, on: request.db) guard let statusFromDatabaseAfterUnreblog else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterUnreblog, - attachments: statusFromDatabaseAfterUnreblog.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterUnreblog, + attachments: statusFromDatabaseAfterUnreblog.attachments, + on: request.executionContext) } /// Users who reblogged status. @@ -1341,11 +1341,13 @@ struct StatusesController { throw StatusError.incorrectStatusId } + let executionContext = request.executionContext + let statusesService = request.application.services.statusesService - let linkableUsers = try await statusesService.reblogged(on: request, statusId: statusId, linkableParams: linkableParams) + let linkableUsers = try await statusesService.reblogged(statusId: statusId, linkableParams: linkableParams, on: executionContext) let usersService = request.application.services.usersService - let userProfiles = await usersService.convertToDtos(on: request, users: linkableUsers.data, attachSensitive: false) + let userProfiles = await usersService.convertToDtos(users: linkableUsers.data, attachSensitive: false, on: executionContext) return LinkableResultDto( maxId: linkableUsers.maxId, @@ -1432,13 +1434,13 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeFavourite = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeFavourite = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeFavourite else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeFavourite, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeFavourite, authorizationPayloadId: authorizationPayloadId, on: request.executionContext) guard canView else { throw EntityNotFoundError.statusNotFound } @@ -1459,7 +1461,7 @@ struct StatusesController { to: statusFromDatabaseBeforeFavourite.user, by: authorizationPayloadId, statusId: statusId, - on: request) + on: request.executionContext) // Send favourite information to remote server. if statusFromDatabaseBeforeFavourite.isLocal == false { @@ -1470,14 +1472,14 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterFavourite = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterFavourite = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterFavourite else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterFavourite, - attachments: statusFromDatabaseAfterFavourite.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterFavourite, + attachments: statusFromDatabaseAfterFavourite.attachments, + on: request.executionContext) } /// Unfavourite specific status. @@ -1556,13 +1558,13 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeUnfavourite = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeUnfavourite = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeUnfavourite else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnfavourite, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnfavourite, authorizationPayloadId: authorizationPayloadId, on: request.executionContext) guard canView else { throw EntityNotFoundError.statusNotFound } @@ -1595,14 +1597,14 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterUnfavourite = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterUnfavourite = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterUnfavourite else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterUnfavourite, - attachments: statusFromDatabaseAfterUnfavourite.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterUnfavourite, + attachments: statusFromDatabaseAfterUnfavourite.attachments, + on: request.executionContext) } /// Users who favourited status. @@ -1673,11 +1675,13 @@ struct StatusesController { throw StatusError.incorrectStatusId } + let executionContext = request.executionContext + let statusesService = request.application.services.statusesService - let linkableUsers = try await statusesService.favourited(on: request, statusId: statusId, linkableParams: linkableParams) + let linkableUsers = try await statusesService.favourited(statusId: statusId, linkableParams: linkableParams, on: executionContext) let usersService = request.application.services.usersService - let userProfiles = await usersService.convertToDtos(on: request, users: linkableUsers.data, attachSensitive: false) + let userProfiles = await usersService.convertToDtos(users: linkableUsers.data, attachSensitive: false, on: executionContext) return LinkableResultDto( maxId: linkableUsers.maxId, @@ -1795,13 +1799,16 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeBookmark = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeBookmark = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeBookmark else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeBookmark, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeBookmark, + authorizationPayloadId: authorizationPayloadId, + on: request.executionContext) + guard canView else { throw EntityNotFoundError.statusNotFound } @@ -1816,12 +1823,14 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterBookmark = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterBookmark = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterBookmark else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, status: statusFromDatabaseAfterBookmark, attachments: statusFromDatabaseAfterBookmark.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterBookmark, + attachments: statusFromDatabaseAfterBookmark.attachments, + on: request.executionContext) } /// Unbookmark specific status. @@ -1931,13 +1940,16 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeUnbookmark = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeUnbookmark = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeUnbookmark else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnbookmark, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnbookmark, + authorizationPayloadId: authorizationPayloadId, + on: request.executionContext) + guard canView else { throw EntityNotFoundError.statusNotFound } @@ -1950,14 +1962,14 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterUnbookmark = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterUnbookmark = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterUnbookmark else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterUnbookmark, - attachments: statusFromDatabaseAfterUnbookmark.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterUnbookmark, + attachments: statusFromDatabaseAfterUnbookmark.attachments, + on: request.executionContext) } /// Feature specific status. @@ -2068,13 +2080,16 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeFeature = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeFeature = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeFeature else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeFeature, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeFeature, + authorizationPayloadId: authorizationPayloadId, + on: request.executionContext) + guard canView else { throw EntityNotFoundError.statusNotFound } @@ -2088,12 +2103,14 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterFeature = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterFeature = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterFeature else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, status: statusFromDatabaseAfterFeature, attachments: statusFromDatabaseAfterFeature.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterFeature, + attachments: statusFromDatabaseAfterFeature.attachments, + on: request.executionContext) } /// Unfeature specific status. @@ -2204,13 +2221,16 @@ struct StatusesController { } let statusesService = request.application.services.statusesService - let statusFromDatabaseBeforeUnfeature = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseBeforeUnfeature = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseBeforeUnfeature else { throw EntityNotFoundError.statusNotFound } // We have to verify if user have access to the status (it's not only for mentioned). - let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnfeature, authorizationPayloadId: authorizationPayloadId, on: request) + let canView = try await statusesService.can(view: statusFromDatabaseBeforeUnfeature, + authorizationPayloadId: authorizationPayloadId, + on: request.executionContext) + guard canView else { throw EntityNotFoundError.statusNotFound } @@ -2222,19 +2242,19 @@ struct StatusesController { } // Prepare and return status. - let statusFromDatabaseAfterUnfeature = try await statusesService.get(on: request.db, id: statusId) + let statusFromDatabaseAfterUnfeature = try await statusesService.get(id: statusId, on: request.db) guard let statusFromDatabaseAfterUnfeature else { throw EntityNotFoundError.statusNotFound } - return await statusesService.convertToDto(on: request, - status: statusFromDatabaseAfterUnfeature, - attachments: statusFromDatabaseAfterUnfeature.attachments) + return await statusesService.convertToDto(status: statusFromDatabaseAfterUnfeature, + attachments: statusFromDatabaseAfterUnfeature.attachments, + on: request.executionContext) } private func createNewStatusResponse(on request: Request, status: Status, attachments: [Attachment]) async throws -> Response { let statusServices = request.application.services.statusesService - let createdStatusDto = await statusServices.convertToDto(on: request, status: status, attachments: attachments) + let createdStatusDto = await statusServices.convertToDto(status: status, attachments: attachments, on: request.executionContext) let response = try await createdStatusDto.encodeResponse(for: request) response.headers.replaceOrAdd(name: .location, value: "/\(StatusesController.uri)/\(status.stringId() ?? "")") diff --git a/Sources/VernissageServer/Controllers/TimelinesController.swift b/Sources/VernissageServer/Controllers/TimelinesController.swift index f7cd972c..50b7ba66 100644 --- a/Sources/VernissageServer/Controllers/TimelinesController.swift +++ b/Sources/VernissageServer/Controllers/TimelinesController.swift @@ -175,10 +175,10 @@ struct TimelinesController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let statuses = try await timelineService.public(on: request.db, linkableParams: linkableParams, onlyLocal: onlyLocal) + let statuses = try await timelineService.public(linkableParams: linkableParams, onlyLocal: onlyLocal, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses) + let statusDtos = await statusesService.convertToDtos(statuses: statuses, on: request.executionContext) return LinkableResultDto( maxId: statuses.last?.stringId(), @@ -320,10 +320,10 @@ struct TimelinesController { } let timelineService = request.application.services.timelineService - let statuses = try await timelineService.category(on: request.db, linkableParams: linkableParams, categoryId: category.requireID(), onlyLocal: onlyLocal) + let statuses = try await timelineService.category(linkableParams: linkableParams, categoryId: category.requireID(), onlyLocal: onlyLocal, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses) + let statusDtos = await statusesService.convertToDtos(statuses: statuses, on: request.executionContext) return LinkableResultDto( maxId: statuses.last?.stringId(), @@ -464,10 +464,10 @@ struct TimelinesController { } let timelineService = request.application.services.timelineService - let statuses = try await timelineService.hashtags(on: request.db, linkableParams: linkableParams, hashtag: hashtag, onlyLocal: onlyLocal) + let statuses = try await timelineService.hashtags(linkableParams: linkableParams, hashtag: hashtag, onlyLocal: onlyLocal, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses) + let statusDtos = await statusesService.convertToDtos(statuses: statuses, on: request.executionContext) return LinkableResultDto( maxId: statuses.last?.stringId(), @@ -598,10 +598,10 @@ struct TimelinesController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let statuses = try await timelineService.featuredStatuses(on: request.db, linkableParams: linkableParams, onlyLocal: onlyLocal) + let statuses = try await timelineService.featuredStatuses(linkableParams: linkableParams, onlyLocal: onlyLocal, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: statuses.data, on: request.executionContext) return LinkableResultDto( maxId: statuses.maxId, @@ -693,10 +693,10 @@ struct TimelinesController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let users = try await timelineService.featuredUsers(on: request.db, linkableParams: linkableParams, onlyLocal: onlyLocal) + let users = try await timelineService.featuredUsers(linkableParams: linkableParams, onlyLocal: onlyLocal, on: request.db) let usersService = request.application.services.usersService - let userDtos = await usersService.convertToDtos(on: request, users: users.data, attachSensitive: false) + let userDtos = await usersService.convertToDtos(users: users.data, attachSensitive: false, on: request.executionContext) return LinkableResultDto( maxId: users.maxId, @@ -825,10 +825,10 @@ struct TimelinesController { let linkableParams = request.linkableParams() let timelineService = request.application.services.timelineService - let statuses = try await timelineService.home(on: request.db, for: authorizationPayloadId, linkableParams: linkableParams) + let statuses = try await timelineService.home(for: authorizationPayloadId, linkableParams: linkableParams, on: request.db) let statusesService = request.application.services.statusesService - let statusDtos = await statusesService.convertToDtos(on: request, statuses: statuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: statuses.data, on: request.executionContext) return LinkableResultDto( maxId: statuses.maxId, diff --git a/Sources/VernissageServer/Controllers/TrendingController.swift b/Sources/VernissageServer/Controllers/TrendingController.swift index 1643d99c..0107f4fe 100644 --- a/Sources/VernissageServer/Controllers/TrendingController.swift +++ b/Sources/VernissageServer/Controllers/TrendingController.swift @@ -162,9 +162,9 @@ struct TrendingController { let statusesService = request.application.services.statusesService let trendingService = request.application.services.trendingService - let trending = try await trendingService.statuses(on: request.db, linkableParams: linkableParams, period: period.translate()) + let trending = try await trendingService.statuses(linkableParams: linkableParams, period: period.translate(), on: request.db) - let statusDtos = await statusesService.convertToDtos(on: request, statuses: trending.data) + let statusDtos = await statusesService.convertToDtos(statuses: trending.data, on: request.executionContext) return LinkableResultDto( maxId: trending.maxId, @@ -257,10 +257,10 @@ struct TrendingController { let linkableParams = request.linkableParams() let trendingService = request.application.services.trendingService - let trending = try await trendingService.users(on: request.db, linkableParams: linkableParams, period: period.translate()) + let trending = try await trendingService.users(linkableParams: linkableParams, period: period.translate(), on: request.db) let usersService = request.application.services.usersService - let userDtos = await usersService.convertToDtos(on: request, users: trending.data, attachSensitive: false) + let userDtos = await usersService.convertToDtos(users: trending.data, attachSensitive: false, on: request.executionContext) return LinkableResultDto( maxId: trending.maxId, @@ -348,7 +348,7 @@ struct TrendingController { let trendingService = request.application.services.trendingService let baseAddress = request.application.settings.cached?.baseAddress ?? "" - let trending = try await trendingService.hashtags(on: request.db, linkableParams: linkableParams, period: period.translate()) + let trending = try await trendingService.hashtags(linkableParams: linkableParams, period: period.translate(), on: request.db) let hashtagDtos = await trending.data.asyncMap { HashtagDto(url: "\(baseAddress)/tags/\($0.hashtag)", name: $0.hashtag, amount: $0.amount) } diff --git a/Sources/VernissageServer/Controllers/UserAliasesController.swift b/Sources/VernissageServer/Controllers/UserAliasesController.swift index c5f9d995..e21c7f45 100644 --- a/Sources/VernissageServer/Controllers/UserAliasesController.swift +++ b/Sources/VernissageServer/Controllers/UserAliasesController.swift @@ -153,7 +153,7 @@ struct UserAliasesController { // Download user activity pub profile. let searchService = request.application.services.searchService - guard let activityPubProfile = await searchService.getRemoteActivityPubProfile(userName: userAliasDto.alias, on: request) else { + guard let activityPubProfile = await searchService.getRemoteActivityPubProfile(userName: userAliasDto.alias, on: request.executionContext) else { throw UserAliasError.cannotVerifyRemoteAccount } diff --git a/Sources/VernissageServer/Controllers/UsersController.swift b/Sources/VernissageServer/Controllers/UsersController.swift index 71f2e9eb..0d5ca439 100644 --- a/Sources/VernissageServer/Controllers/UsersController.swift +++ b/Sources/VernissageServer/Controllers/UsersController.swift @@ -254,7 +254,7 @@ struct UsersController { .paginate(PageRequest(page: page, per: size)) let usersService = request.application.services.usersService - let userDtos = await usersService.convertToDtos(on: request, users: usersFromDatabase.items, attachSensitive: true) + let userDtos = await usersService.convertToDtos(users: usersFromDatabase.items, attachSensitive: true, on: request.executionContext) return PaginableResultDto( data: userDtos, @@ -270,7 +270,7 @@ struct UsersController { /// that can also be accessed by non-logged-in users. You can pass here /// user name or user id. /// - /// > Important: Endpoint URL: `/api/v1/users/:userName`. + /// > Important: Endpoint URL: `/api/v1/users/:userName` or `/:userName` . /// /// **CURL request:** /// @@ -323,12 +323,12 @@ struct UsersController { if userNameOrId.starts(with: "@") || !userNameOrId.isNumber { let userNameNormalized = userNameOrId.deletingPrefix("@").uppercased() - userFromDb = try await usersService.get(on: request.db, userName: userNameNormalized) + userFromDb = try await usersService.get(userName: userNameNormalized, on: request.db) let userNameFromToken = request.auth.get(UserPayload.self)?.userName isProfileOwner = userNameFromToken?.uppercased() == userNameNormalized } else if let userId = Int64(userNameOrId) { - userFromDb = try await usersService.get(on: request.db, id: userId) + userFromDb = try await usersService.get(id: userId, on: request.db) let userIdFromToken = request.auth.get(UserPayload.self)?.id isProfileOwner = userIdFromToken == userNameOrId @@ -338,11 +338,11 @@ struct UsersController { throw EntityNotFoundError.userNotFound } - let userProfile = await usersService.convertToDto(on: request, - user: user, + let userProfile = await usersService.convertToDto(user: user, flexiFields: user.flexiFields, roles: nil, - attachSensitive: isProfileOwner) + attachSensitive: isProfileOwner, + on: request.executionContext) return userProfile } @@ -464,24 +464,24 @@ struct UsersController { let usersService = request.application.services.usersService let flexiFieldService = request.application.services.flexiFieldService - guard usersService.isSignedInUser(on: request, userName: userName) else { + guard usersService.isSignedInUser(userName: userName, on: request) else { throw EntityForbiddenError.userForbidden } let userDto = try request.content.decode(UserDto.self) try UserDto.validate(content: request) - let user = try await usersService.updateUser(on: request, userDto: userDto, userNameNormalized: request.userNameNormalized) - let flexiFields = try await flexiFieldService.getFlexiFields(on: request.db, for: user.requireID()) + let user = try await usersService.updateUser(userDto: userDto, userNameNormalized: request.userNameNormalized, on: request.executionContext) + let flexiFields = try await flexiFieldService.getFlexiFields(for: user.requireID(), on: request.db) // Enqueue job for flexi field URL validator. - try await flexiFieldService.dispatchUrlValidator(on: request, flexiFields: flexiFields) + try await flexiFieldService.dispatchUrlValidator(flexiFields: flexiFields, on: request.executionContext) - let userDtoAfterUpdate = await usersService.convertToDto(on: request, - user: user, + let userDtoAfterUpdate = await usersService.convertToDto(user: user, flexiFields: flexiFields, roles: nil, - attachSensitive: true) + attachSensitive: true, + on: request.executionContext) return userDtoAfterUpdate } @@ -521,7 +521,7 @@ struct UsersController { let userNameNormalized = userName.deletingPrefix("@").uppercased() let usersService = request.application.services.usersService - guard let userFromDb = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let userFromDb = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -591,7 +591,7 @@ struct UsersController { let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let followedUser = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let followedUser = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -614,14 +614,14 @@ struct UsersController { let approved = followedUser.isLocal && followedUser.manuallyApprovesFollowers == false // Save follow in local database. - let followId = try await followsService.follow(on: request.application, - sourceId: sourceUser.requireID(), + let followId = try await followsService.follow(sourceId: sourceUser.requireID(), targetId: followedUser.requireID(), approved: approved, - activityId: nil) + activityId: nil, + on: request.executionContext) - try await usersService.updateFollowCount(on: request.db, for: sourceUser.requireID()) - try await usersService.updateFollowCount(on: request.db, for: followedUser.requireID()) + try await usersService.updateFollowCount(for: sourceUser.requireID(), on: request.db) + try await usersService.updateFollowCount(for: followedUser.requireID(), on: request.db) // Send notification to user about follow. let notificationsService = request.application.services.notificationsService @@ -629,7 +629,7 @@ struct UsersController { to: followedUser, by: sourceUser.requireID(), statusId: nil, - on: request) + on: request.executionContext) // If target user is from remote server, notify remote server about follow. if followedUser.isLocal == false { @@ -646,7 +646,7 @@ struct UsersController { privateKey: privateKey) } - return try await self.relationship(on: request, sourceId: authorizationPayloadId, targetUser: followedUser) + return try await self.relationship(sourceId: authorizationPayloadId, targetUser: followedUser, on: request) } /// Unfollow user. @@ -701,7 +701,7 @@ struct UsersController { let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let followedUser = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let followedUser = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -721,15 +721,15 @@ struct UsersController { } // Delete follow from local database. - let followId = try await followsService.unfollow(on: request.application, sourceId: sourceUser.requireID(), targetId: followedUser.requireID()) + let followId = try await followsService.unfollow(sourceId: sourceUser.requireID(), targetId: followedUser.requireID(), on: request.executionContext) // User doesn't follow other user. guard let followId else { - return try await self.relationship(on: request, sourceId: authorizationPayloadId, targetUser: followedUser) + return try await self.relationship(sourceId: authorizationPayloadId, targetUser: followedUser, on: request) } - try await usersService.updateFollowCount(on: request.db, for: sourceUser.requireID()) - try await usersService.updateFollowCount(on: request.db, for: followedUser.requireID()) + try await usersService.updateFollowCount(for: sourceUser.requireID(), on: request.db) + try await usersService.updateFollowCount(for: followedUser.requireID(), on: request.db) // If target user is from remote server, notify remote server about unfollow (in background job). if followedUser.isLocal == false { @@ -746,7 +746,7 @@ struct UsersController { privateKey: privateKey) } - return try await self.relationship(on: request, sourceId: authorizationPayloadId, targetUser: followedUser) + return try await self.relationship(sourceId: authorizationPayloadId, targetUser: followedUser, on: request) } /// List of followers. @@ -816,12 +816,17 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } - let linkableUsers = try await followsService.follows(on: request, targetId: user.requireID(), onlyApproved: false, linkableParams: linkableParams) - let userProfiles = await usersService.convertToDtos(on: request, users: linkableUsers.data, attachSensitive: false) + let executionContext = request.executionContext + let linkableUsers = try await followsService.follows(targetId: user.requireID(), + onlyApproved: false, + linkableParams: linkableParams, + on: executionContext) + + let userProfiles = await usersService.convertToDtos(users: linkableUsers.data, attachSensitive: false, on: executionContext) return LinkableResultDto( maxId: linkableUsers.maxId, @@ -897,12 +902,17 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } - let linkableUsers = try await followsService.following(on: request, sourceId: user.requireID(), onlyApproved: false, linkableParams: linkableParams) - let userProfiles = await usersService.convertToDtos(on: request, users: linkableUsers.data, attachSensitive: false) + let executionContext = request.executionContext + let linkableUsers = try await followsService.following(sourceId: user.requireID(), + onlyApproved: false, + linkableParams: linkableParams, + on: executionContext) + + let userProfiles = await usersService.convertToDtos(users: linkableUsers.data, attachSensitive: false, on: executionContext) return LinkableResultDto( maxId: linkableUsers.maxId, @@ -975,21 +985,21 @@ struct UsersController { let userMuteRequestDto = try request.content.decode(UserMuteRequestDto.self) let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let mutedUser = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let mutedUser = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } _ = try await userMutesService.mute( - on: request, userId: authorizationPayloadId, mutedUserId: mutedUser.requireID(), muteStatuses: userMuteRequestDto.muteStatuses, muteReblogs: userMuteRequestDto.muteReblogs, muteNotifications: userMuteRequestDto.muteNotifications, - muteEnd: userMuteRequestDto.muteEnd + muteEnd: userMuteRequestDto.muteEnd, + on: request ) - return try await self.relationship(on: request, sourceId: authorizationPayloadId, targetUser: mutedUser) + return try await self.relationship(sourceId: authorizationPayloadId, targetUser: mutedUser, on: request) } /// Unmute specific user. @@ -1043,12 +1053,12 @@ struct UsersController { let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let unmutedUser = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let unmutedUser = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } - try await userMutesService.unmute(on: request, userId: authorizationPayloadId, mutedUserId: unmutedUser.requireID()) - return try await self.relationship(on: request, sourceId: authorizationPayloadId, targetUser: unmutedUser) + try await userMutesService.unmute(userId: authorizationPayloadId, mutedUserId: unmutedUser.requireID(), on: request) + return try await self.relationship(sourceId: authorizationPayloadId, targetUser: unmutedUser, on: request) } /// Enable specific user. @@ -1082,7 +1092,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1123,7 +1133,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1168,7 +1178,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1222,7 +1232,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1270,7 +1280,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1314,7 +1324,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1360,7 +1370,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1368,7 +1378,7 @@ struct UsersController { return HTTPStatus.ok } - _ = await searchService.downloadRemoteUser(activityPubProfile: user.activityPubProfile, on: request) + _ = try? await searchService.downloadRemoteUser(activityPubProfile: user.activityPubProfile, on: request.executionContext) return HTTPStatus.ok } @@ -1491,7 +1501,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1502,7 +1512,7 @@ struct UsersController { if authorizationPayloadId == userId { // For signed in users we have to show all kind of statuses on their own profiles (public/followers/mentioned). let linkableStatuses = try await usersService.ownStatuses(for: userId, linkableParams: linkableParams, on: request) - let statusDtos = await statusesService.convertToDtos(on: request, statuses: linkableStatuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: linkableStatuses.data, on: request.executionContext) return LinkableResultDto( maxId: linkableStatuses.maxId, @@ -1512,7 +1522,7 @@ struct UsersController { } else { // For profiles other users we have to show only public statuses. let linkableStatuses = try await usersService.publicStatuses(for: userId, linkableParams: linkableParams, on: request) - let statusDtos = await statusesService.convertToDtos(on: request, statuses: linkableStatuses.data) + let statusDtos = await statusesService.convertToDtos(statuses: linkableStatuses.data, on: request.executionContext) return LinkableResultDto( maxId: linkableStatuses.maxId, @@ -1581,7 +1591,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1589,7 +1599,7 @@ struct UsersController { throw EntityNotFoundError.userNotFound } - guard let _ = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let _ = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1602,16 +1612,16 @@ struct UsersController { } // Prepare and return user. - let userFromDatabaseAfterFeature = try await usersService.get(on: request.db, id: userId) + let userFromDatabaseAfterFeature = try await usersService.get(id: userId, on: request.db) guard let userFromDatabaseAfterFeature else { throw EntityNotFoundError.statusNotFound } - let userProfile = await usersService.convertToDto(on: request, - user: userFromDatabaseAfterFeature, + let userProfile = await usersService.convertToDto(user: userFromDatabaseAfterFeature, flexiFields: userFromDatabaseAfterFeature.flexiFields, roles: nil, - attachSensitive: false) + attachSensitive: false, + on: request.executionContext) return userProfile } @@ -1670,7 +1680,7 @@ struct UsersController { } let userNameNormalized = userName.deletingPrefix("@").uppercased() - guard let user = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let user = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1678,7 +1688,7 @@ struct UsersController { throw EntityNotFoundError.userNotFound } - guard let _ = try await usersService.get(on: request.db, userName: userNameNormalized) else { + guard let _ = try await usersService.get(userName: userNameNormalized, on: request.db) else { throw EntityNotFoundError.userNotFound } @@ -1689,23 +1699,23 @@ struct UsersController { } // Prepare and return user. - let userFromDatabaseAfterFeature = try await usersService.get(on: request.db, id: userId) + let userFromDatabaseAfterFeature = try await usersService.get(id: userId, on: request.db) guard let userFromDatabaseAfterFeature else { throw EntityNotFoundError.statusNotFound } - let userProfile = await usersService.convertToDto(on: request, - user: userFromDatabaseAfterFeature, + let userProfile = await usersService.convertToDto(user: userFromDatabaseAfterFeature, flexiFields: userFromDatabaseAfterFeature.flexiFields, roles: nil, - attachSensitive: false) + attachSensitive: false, + on: request.executionContext) return userProfile } - private func relationship(on request: Request, sourceId: Int64, targetUser: User) async throws -> RelationshipDto { + private func relationship(sourceId: Int64, targetUser: User, on request: Request) async throws -> RelationshipDto { let targetUserId = try targetUser.requireID() let relationshipsService = request.application.services.relationshipsService - let relationships = try await relationshipsService.relationships(on: request.db, userId: sourceId, relatedUserIds: [targetUserId]) + let relationships = try await relationshipsService.relationships(userId: sourceId, relatedUserIds: [targetUserId], on: request.db) return relationships.first ?? RelationshipDto( userId: "\(targetUserId)", diff --git a/Sources/VernissageServer/Controllers/WellKnownController.swift b/Sources/VernissageServer/Controllers/WellKnownController.swift index 4922d64e..cc87ba71 100644 --- a/Sources/VernissageServer/Controllers/WellKnownController.swift +++ b/Sources/VernissageServer/Controllers/WellKnownController.swift @@ -135,12 +135,14 @@ struct WellKnownController { /// /// - Returns: NodeInfo information. @Sendable - func nodeinfo(request: Request) async throws -> NodeInfoLinkDto { + func nodeinfo(request: Request) async throws -> NodeInfoLinksDto { let appplicationSettings = request.application.settings.cached let baseAddress = appplicationSettings?.baseAddress ?? "" - return NodeInfoLinkDto(rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", + let link = NodeInfoLinkDto(rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", href: "\(baseAddress)/api/v1/nodeinfo/2.0") + + return NodeInfoLinksDto(links: [link]) } /// Exposing host metadata. @@ -209,7 +211,7 @@ struct WellKnownController { private func createUserResponse(for account: String, on request: Request) async throws -> Response { let usersService = request.application.services.usersService - let userFromDb = try await usersService.get(on: request.db, account: account) + let userFromDb = try await usersService.get(account: account, on: request.db) guard let user = userFromDb else { throw EntityNotFoundError.userNotFound diff --git a/Sources/VernissageServer/Extensions/ActicityPubEntities+Content.swift b/Sources/VernissageServer/Extensions/ActicityPubEntities+Content.swift index 399cf45f..caeb10bb 100644 --- a/Sources/VernissageServer/Extensions/ActicityPubEntities+Content.swift +++ b/Sources/VernissageServer/Extensions/ActicityPubEntities+Content.swift @@ -15,6 +15,7 @@ extension ObjectDto: Content { } extension ComplexType: Content { } extension NodeInfoDto: Content { } extension NodeInfoLinkDto: Content { } +extension NodeInfoLinksDto: Content { } extension NodeInfoMetadataDto: Content { } extension NodeInfoServicesDto: Content { } extension NodeInfoSoftwareDto: Content { } diff --git a/Sources/VernissageServer/Extensions/Application+FileIO.swift b/Sources/VernissageServer/Extensions/Application+FileIO.swift index ad86b560..7f30b975 100644 --- a/Sources/VernissageServer/Extensions/Application+FileIO.swift +++ b/Sources/VernissageServer/Extensions/Application+FileIO.swift @@ -20,6 +20,7 @@ extension NonBlockingFileIO { done.whenComplete { _ in try? fd.close() } + return done } catch { return eventLoop.makeFailedFuture(error) diff --git a/Sources/VernissageServer/QueueJobs/ActivityPubSharedInboxJob.swift b/Sources/VernissageServer/QueueJobs/ActivityPubSharedInboxJob.swift index 24dbef3b..a16bb8f3 100644 --- a/Sources/VernissageServer/QueueJobs/ActivityPubSharedInboxJob.swift +++ b/Sources/VernissageServer/QueueJobs/ActivityPubSharedInboxJob.swift @@ -18,35 +18,36 @@ struct ActivityPubSharedInboxJob: AsyncJob { let activityPubService = context.application.services.activityPubService let activityPubSignatureService = context.application.services.activityPubSignatureService + let executionContext = context.executionContext // Validate supported algorithm. - try activityPubSignatureService.validateAlgorith(on: context, activityPubRequest: payload) + try activityPubSignatureService.validateAlgorith(activityPubRequest: payload, on: executionContext) switch payload.activity.type { case .delete: // Signature have to be verified depending on deleting kind of object. - try await activityPubService.delete(on: context, activityPubRequest: payload) + try await activityPubService.delete(activityPubRequest: payload, on: executionContext) case .create: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.create(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.create(activityPubRequest: payload, on: executionContext) case .follow: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.follow(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.follow(activityPubRequest: payload, on: executionContext) case .accept: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.accept(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.accept(activityPubRequest: payload, on: executionContext) case .reject: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.reject(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.reject(activityPubRequest: payload, on: executionContext) case .undo: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.undo(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.undo(activityPubRequest: payload, on: executionContext) case .announce: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.announce(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.announce(activityPubRequest: payload, on: executionContext) case .like: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.like(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.like(activityPubRequest: payload, on: executionContext) default: context.logger.info("Unhandled action type: '\(payload.activity.type)'.") } diff --git a/Sources/VernissageServer/QueueJobs/ActivityPubUserInboxJob.swift b/Sources/VernissageServer/QueueJobs/ActivityPubUserInboxJob.swift index 0480f3b9..6050743d 100644 --- a/Sources/VernissageServer/QueueJobs/ActivityPubUserInboxJob.swift +++ b/Sources/VernissageServer/QueueJobs/ActivityPubUserInboxJob.swift @@ -18,28 +18,29 @@ struct ActivityPubUserInboxJob: AsyncJob { let activityPubService = context.application.services.activityPubService let activityPubSignatureService = context.application.services.activityPubSignatureService + let executionContext = context.executionContext // Validate supported algorithm. - try activityPubSignatureService.validateAlgorith(on: context, activityPubRequest: payload) + try activityPubSignatureService.validateAlgorith(activityPubRequest: payload, on: executionContext) switch payload.activity.type { case .delete: - try await activityPubService.delete(on: context, activityPubRequest: payload) + try await activityPubService.delete(activityPubRequest: payload, on: executionContext) case .follow: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.follow(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.follow(activityPubRequest: payload, on: executionContext) case .accept: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.accept(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.accept(activityPubRequest: payload, on: executionContext) case .reject: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.reject(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.reject(activityPubRequest: payload, on: executionContext) case .undo: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.undo(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.undo(activityPubRequest: payload, on: executionContext) case .like: - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: payload) - try await activityPubService.like(on: context, activityPubRequest: payload) + try await activityPubSignatureService.validateSignature(activityPubRequest: payload, on: executionContext) + try await activityPubService.like(activityPubRequest: payload, on: executionContext) default: context.logger.info("Unhandled action type: '\(payload.activity.type)'.") } diff --git a/Sources/VernissageServer/QueueJobs/StatusDeleterJob.swift b/Sources/VernissageServer/QueueJobs/StatusDeleterJob.swift index 6596ad19..8df983f3 100644 --- a/Sources/VernissageServer/QueueJobs/StatusDeleterJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusDeleterJob.swift @@ -18,7 +18,7 @@ struct StatusDeleterJob: AsyncJob { context.logger.info("StatusDeleterJob deleting status from remote server. Status (id: '\(payload.activityPubStatusId)').") let statusesService = context.application.services.statusesService - try await statusesService.deleteFromRemote(statusActivityPubId: payload.activityPubStatusId, userId: payload.userId, on: context) + try await statusesService.deleteFromRemote(statusActivityPubId: payload.activityPubStatusId, userId: payload.userId, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: StatusDeleteJobDto) async throws { diff --git a/Sources/VernissageServer/QueueJobs/StatusFavouriterJob.swift b/Sources/VernissageServer/QueueJobs/StatusFavouriterJob.swift index b069613a..ccc48b3b 100644 --- a/Sources/VernissageServer/QueueJobs/StatusFavouriterJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusFavouriterJob.swift @@ -17,7 +17,7 @@ struct StatusFavouriterJob: AsyncJob { context.logger.info("StatusFavouriterJob dequeued job. Status favourite (id: '\(payload)').") let statusesService = context.application.services.statusesService - try await statusesService.send(favourite: payload, on: context) + try await statusesService.send(favourite: payload, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: Int64) async throws { diff --git a/Sources/VernissageServer/QueueJobs/StatusRebloggerJob.swift b/Sources/VernissageServer/QueueJobs/StatusRebloggerJob.swift index a4f8040c..79c18ba4 100644 --- a/Sources/VernissageServer/QueueJobs/StatusRebloggerJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusRebloggerJob.swift @@ -17,7 +17,7 @@ struct StatusRebloggerJob: AsyncJob { context.logger.info("StatusRebloggerJob dequeued job. Status (id: '\(payload)').") let statusesService = context.application.services.statusesService - try await statusesService.send(reblog: payload, on: context) + try await statusesService.send(reblog: payload, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: Int64) async throws { diff --git a/Sources/VernissageServer/QueueJobs/StatusSenderJob.swift b/Sources/VernissageServer/QueueJobs/StatusSenderJob.swift index 37d1818a..bdf377ea 100644 --- a/Sources/VernissageServer/QueueJobs/StatusSenderJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusSenderJob.swift @@ -17,7 +17,7 @@ struct StatusSenderJob: AsyncJob { context.logger.info("StatusSenderJob dequeued job. Status (id: '\(payload)').") let statusesService = context.application.services.statusesService - try await statusesService.send(status: payload, on: context) + try await statusesService.send(status: payload, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: Int64) async throws { diff --git a/Sources/VernissageServer/QueueJobs/StatusUnfavouriterJob.swift b/Sources/VernissageServer/QueueJobs/StatusUnfavouriterJob.swift index 9042995f..561cbcc6 100644 --- a/Sources/VernissageServer/QueueJobs/StatusUnfavouriterJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusUnfavouriterJob.swift @@ -17,7 +17,7 @@ struct StatusUnfavouriterJob: AsyncJob { context.logger.info("StatusUnfavouriterJob dequeued job. Status favourite (id: '\(payload.statusFavouriteId)').") let statusesService = context.application.services.statusesService - try await statusesService.send(unfavourite: payload, on: context) + try await statusesService.send(unfavourite: payload, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: StatusUnfavouriteJobDto) async throws { diff --git a/Sources/VernissageServer/QueueJobs/StatusUnrebloggerJob.swift b/Sources/VernissageServer/QueueJobs/StatusUnrebloggerJob.swift index dc5da455..5aeb76dc 100644 --- a/Sources/VernissageServer/QueueJobs/StatusUnrebloggerJob.swift +++ b/Sources/VernissageServer/QueueJobs/StatusUnrebloggerJob.swift @@ -17,7 +17,7 @@ struct StatusUnrebloggerJob: AsyncJob { context.logger.info("StatusUnrebloggerJob dequeued job. Status (statusId: '\(payload.statusId)', orginalStatusId: '\(payload.orginalStatusId)', activityPubReblogId: '\(payload.activityPubReblogStatusId)').") let statusesService = context.application.services.statusesService - try await statusesService.send(unreblog: payload, on: context) + try await statusesService.send(unreblog: payload, on: context.executionContext) } func error(_ context: QueueContext, _ error: Error, _ payload: ActivityPubUnreblogDto) async throws { diff --git a/Sources/VernissageServer/ScheduledJobs/ClearAttachmentsJob.swift b/Sources/VernissageServer/ScheduledJobs/ClearAttachmentsJob.swift index 8353c452..922eb0a9 100644 --- a/Sources/VernissageServer/ScheduledJobs/ClearAttachmentsJob.swift +++ b/Sources/VernissageServer/ScheduledJobs/ClearAttachmentsJob.swift @@ -34,18 +34,20 @@ struct ClearAttachmentsJob: AsyncScheduledJob { context.logger.info("ClearAttachmentsJob old attachments to delete: \(attachments.count).") let storageService = context.application.services.storageService + let executionContext = context.executionContext + for attachment in attachments { do { // Remove files from external storage provider. context.logger.info("ClearAttachmentsJob delete orginal file from storage: \(attachment.originalFile.fileName).") - try await storageService.delete(fileName: attachment.originalFile.fileName, on: context) + try await storageService.delete(fileName: attachment.originalFile.fileName, on: executionContext) context.logger.info("ClearAttachmentsJob delete small file from storage: \(attachment.smallFile.fileName).") - try await storageService.delete(fileName: attachment.smallFile.fileName, on: context) + try await storageService.delete(fileName: attachment.smallFile.fileName, on: executionContext) if let orginalHdrFileName = attachment.originalHdrFile?.fileName { context.logger.info("ClearAttachmentsJob delete orginal HDR file from storage: \(orginalHdrFileName).") - try await storageService.delete(fileName: orginalHdrFileName, on: context) + try await storageService.delete(fileName: orginalHdrFileName, on: executionContext) } // Remove attachment from database. diff --git a/Sources/VernissageServer/Services/ActivityPubService.swift b/Sources/VernissageServer/Services/ActivityPubService.swift index 2c775d40..6bc42bce 100644 --- a/Sources/VernissageServer/Services/ActivityPubService.swift +++ b/Sources/VernissageServer/Services/ActivityPubService.swift @@ -26,32 +26,33 @@ extension Application.Services { @_documentation(visibility: private) protocol ActivityPubServiceType: Sendable { - func delete(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func create(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func follow(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func accept(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func reject(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func undo(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func like(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func announce(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func isDomainBlockedByInstance(on application: Application, actorId: String) async throws -> Bool - func isDomainBlockedByInstance(on application: Application, activity: ActivityDto) async throws -> Bool + func delete(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func create(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func follow(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func accept(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func reject(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func undo(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func like(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func announce(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func isDomainBlockedByInstance(actorId: String, on context: ExecutionContext) async throws -> Bool + func isDomainBlockedByInstance(activity: ActivityDto, on context: ExecutionContext) async throws -> Bool + func downloadStatus(activityPubId: String, on context: ExecutionContext) async throws -> Status } /// Service responsible for consuming requests retrieved on Activity Pub controllers from remote instances. final class ActivityPubService: ActivityPubServiceType { - public func delete(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let statusesService = context.application.services.statusesService - let usersService = context.application.services.usersService - let activityPubSignatureService = context.application.services.activityPubSignatureService + public func delete(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let statusesService = context.services.statusesService + let usersService = context.services.usersService + let activityPubSignatureService = context.services.activityPubSignatureService let objects = activityPubRequest.activity.object.objects() for object in objects { switch object.type { case .some(.note), .some(.tombstone): context.logger.info("Deleting status: '\(object.id)'.") - guard let statusToDelete = try await statusesService.get(on: context.application.db, activityPubId: object.id) else { + guard let statusToDelete = try await statusesService.get(activityPubId: object.id, on: context.db) else { context.logger.info("Deleting status: '\(object.id)'. Status not exists in local database.") continue } @@ -62,14 +63,14 @@ final class ActivityPubService: ActivityPubServiceType { } // Validate signature (also with users downloaded from remote server). - try await activityPubSignatureService.validateSignature(on: context, activityPubRequest: activityPubRequest) + try await activityPubSignatureService.validateSignature(activityPubRequest: activityPubRequest, on: context) // Signature verified, we can delete status. try await statusesService.delete(id: statusToDelete.requireID(), on: context.application.db) context.logger.info("Deleting status: '\(object.id)'. Status deleted from local database successfully.") case .none, .some(.profile): context.logger.info("Deleting user: '\(object.id)'.") - guard let userToDelete = try await usersService.get(on: context.application.db, activityPubProfile: object.id) else { + guard let userToDelete = try await usersService.get(activityPubProfile: object.id, on: context.application.db) else { context.logger.info("Deleting user: '\(object.id)'. User not exists in local database.") continue } @@ -80,7 +81,7 @@ final class ActivityPubService: ActivityPubServiceType { } // Validate signature with local database only (user has been alredy removed from remote). - try await activityPubSignatureService.validateLocalSignature(on: context, activityPubRequest: activityPubRequest) + try await activityPubSignatureService.validateLocalSignature(activityPubRequest: activityPubRequest, on: context) // Signature verified, we have to delete all user's statuses first. try await statusesService.delete(owner: userToDelete.requireID(), on: context) @@ -95,9 +96,9 @@ final class ActivityPubService: ActivityPubServiceType { } } - public func create(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let statusesService = context.application.services.statusesService - let searchService = context.application.services.searchService + public func create(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let statusesService = context.services.statusesService + let searchService = context.services.searchService let activity = activityPubRequest.activity let objects = activity.object.objects() @@ -152,7 +153,7 @@ final class ActivityPubService: ActivityPubServiceType { let statusFromDatabase = try await statusesService.create(basedOn: noteDto, userId: user.requireID(), on: context) // Recalculate numer of user statuses. - try await statusesService.updateStatusCount(on: context.application.db, for: user.requireID()) + try await statusesService.updateStatusCount(for: user.requireID(), on: context.application.db) // Add new status to user's timelines (except comments). if statusFromDatabase.$replyToStatus.id == nil { @@ -165,25 +166,25 @@ final class ActivityPubService: ActivityPubServiceType { } } - public func follow(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { + public func follow(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { let activity = activityPubRequest.activity let actorIds = activity.actor.actorIds() for actorId in actorIds { let objects = activity.object.objects() for object in objects { - let domainIsBlockedByUser = try await self.isDomainBlockedByUser(on: context, actorId: object.id) + let domainIsBlockedByUser = try await self.isDomainBlockedByUser(actorId: object.id, on: context) guard domainIsBlockedByUser == false else { context.logger.notice("Actor's domain: '\(actorId)' is blocked by user's (\(object.id)) domain blocks.") continue } - try await self.follow(sourceProfileUrl: actorId, activityPubObject: object, on: context, activityId: activity.id) + try await self.follow(sourceProfileUrl: actorId, activityPubObject: object, activityId: activity.id, on: context) } } } - public func accept(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { + public func accept(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { let activity = activityPubRequest.activity let actorIds = activity.actor.actorIds() @@ -195,7 +196,7 @@ final class ActivityPubService: ActivityPubServiceType { } } - public func reject(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { + public func reject(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { let activity = activityPubRequest.activity let actorIds = activity.actor.actorIds() @@ -207,7 +208,7 @@ final class ActivityPubService: ActivityPubServiceType { } } - func undo(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { + func undo(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { let activity = activityPubRequest.activity let objects = activity.object.objects() @@ -232,9 +233,9 @@ final class ActivityPubService: ActivityPubServiceType { } } - public func like(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let statusesService = context.application.services.statusesService - let searchService = context.application.services.searchService + public func like(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let statusesService = context.services.statusesService + let searchService = context.services.searchService let activity = activityPubRequest.activity // Download user data (who liked status) to local database. @@ -247,14 +248,14 @@ final class ActivityPubService: ActivityPubServiceType { let objects = activity.object.objects() for object in objects { // Create main status in local database. - let status = try await self.downloadStatus(on: context, activityPubId: object.id) + let status = try await self.downloadStatus(activityPubId: object.id, on: context) let statusId = try status.requireID() let targetUserId = status.$user.id let remoteUserId = try remoteUser.requireID() // Break when status has been already favourited by user. - let statusFavouriteFromDatabase = try await StatusFavourite.query(on: context.application.db) + let statusFavouriteFromDatabase = try await StatusFavourite.query(on: context.db) .filter(\.$status.$id == statusId) .filter(\.$user.$id == remoteUserId) .first() @@ -265,18 +266,18 @@ final class ActivityPubService: ActivityPubServiceType { } // Create favourite. - let id = context.application.services.snowflakeService.generate() + let id = context.services.snowflakeService.generate() let statusFavourite = StatusFavourite(id: id, statusId: statusId, userId: remoteUserId) - try await statusFavourite.create(on: context.application.db) + try await statusFavourite.create(on: context.db) context.logger.info("Recalculating favourites for status '\(statusId)' in local database.") - try await statusesService.updateFavouritesCount(for: statusId, on: context.application.db) + try await statusesService.updateFavouritesCount(for: statusId, on: context.db) // Send notification to user about new like. - let notificationsService = context.application.services.notificationsService - let usersService = context.application.services.usersService + let notificationsService = context.services.notificationsService + let usersService = context.services.usersService - if let targetUser = try await usersService.get(on: context.application.db, id: targetUserId) { + if let targetUser = try await usersService.get(id: targetUserId, on: context.db) { try await notificationsService.create(type: .favourite, to: targetUser, by: remoteUser.requireID(), @@ -286,7 +287,7 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func unlike(sourceActorId: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unlike(sourceActorId: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { guard let annouceDto = activityPubObject.object as? LikeDto, let objects = annouceDto.object?.objects() else { return @@ -297,17 +298,17 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func unlike(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unlike(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { context.logger.info("Unliking status: '\(activityPubObject.id)' by account '\(sourceProfileUrl)' (from remote server).") - let statusesService = context.application.services.statusesService - let usersService = context.application.services.usersService + let statusesService = context.services.statusesService + let usersService = context.services.usersService - guard let user = try await usersService.get(on: context.application.db, activityPubProfile: sourceProfileUrl) else { + guard let user = try await usersService.get(activityPubProfile: sourceProfileUrl, on: context.db) else { context.logger.warning("Cannot find user '\(sourceProfileUrl)' in local database.") return } - guard let status = try await statusesService.get(on: context.application.db, activityPubId: activityPubObject.id) else { + guard let status = try await statusesService.get(activityPubId: activityPubObject.id, on: context.db) else { context.logger.warning("Cannot find orginal status '\(activityPubObject.id)' in local database.") return } @@ -315,7 +316,7 @@ final class ActivityPubService: ActivityPubServiceType { let statusId = try status.requireID() let userId = try user.requireID() - guard let statusFavourite = try await StatusFavourite.query(on: context.application.db) + guard let statusFavourite = try await StatusFavourite.query(on: context.db) .filter(\.$status.$id == statusId) .filter(\.$user.$id == userId) .first() else { @@ -324,15 +325,15 @@ final class ActivityPubService: ActivityPubServiceType { } context.logger.info("Deleting favourite for status '\(statusId)' and user '\(userId)' from local database.") - try await statusFavourite.delete(on: context.application.db) + try await statusFavourite.delete(on: context.db) context.logger.info("Recalculating favourites for status '\(statusId)' in local database.") - try await statusesService.updateFavouritesCount(for: statusId, on: context.application.db) + try await statusesService.updateFavouritesCount(for: statusId, on: context.db) } - public func announce(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let statusesService = context.application.services.statusesService - let usersService = context.application.services.usersService + public func announce(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let statusesService = context.services.statusesService + let usersService = context.services.usersService let activity = activityPubRequest.activity guard let actorActivityPubId = activity.actor.actorIds().first else { @@ -346,7 +347,7 @@ final class ActivityPubService: ActivityPubServiceType { return } - guard let remoteUser = try await usersService.get(on: context.application.db, activityPubProfile: actorActivityPubId) else { + guard let remoteUser = try await usersService.get(activityPubProfile: actorActivityPubId, on: context.db) else { context.logger.warning("User '\(activity.actor.actorIds().first ?? "")' cannot found in the local database (activity: \(activity.id)).") return } @@ -357,10 +358,10 @@ final class ActivityPubService: ActivityPubServiceType { let objects = activity.object.objects() for object in objects { // Create (or get from local database) main status in local database. - let downloadedStatus = try await self.downloadStatus(on: context, activityPubId: object.id) + let downloadedStatus = try await self.downloadStatus(activityPubId: object.id, on: context) // Get full status from database. - guard let mainStatusFromDatabase = try await statusesService.get(on: context.application.db, id: downloadedStatus.requireID()) else { + guard let mainStatusFromDatabase = try await statusesService.get(id: downloadedStatus.requireID(), on: context.db) else { context.logger.warning("Boosted status '\(object.id)' has not been downloaded successfully (activity: \(activity.id)).") continue } @@ -384,8 +385,8 @@ final class ActivityPubService: ActivityPubServiceType { visibility: .public, reblogId: mainStatusFromDatabase.requireID()) - try await reblogStatus.create(on: context.application.db) - try await statusesService.updateReblogsCount(for: mainStatusFromDatabase.requireID(), on: context.application.db) + try await reblogStatus.create(on: context.db) + try await statusesService.updateReblogsCount(for: mainStatusFromDatabase.requireID(), on: context.db) // Add new reblog status to user's timelines. context.logger.info("Connecting status '\(reblogStatus.stringId() ?? "")' to followers of '\(remoteUser.stringId() ?? "")'.") @@ -393,7 +394,7 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func unannounce(sourceActorId: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unannounce(sourceActorId: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { guard let annouceDto = activityPubObject.object as? AnnouceDto, let objects = annouceDto.object?.objects() else { return @@ -404,17 +405,17 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func unannounce(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unannounce(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { context.logger.info("Unannoucing status: '\(activityPubObject.id)' by account '\(sourceProfileUrl)' (from remote server).") - let statusesService = context.application.services.statusesService - let usersService = context.application.services.usersService + let statusesService = context.services.statusesService + let usersService = context.services.usersService - guard let user = try await usersService.get(on: context.application.db, activityPubProfile: sourceProfileUrl) else { + guard let user = try await usersService.get(activityPubProfile: sourceProfileUrl, on: context.db) else { context.logger.warning("Cannot find user '\(sourceProfileUrl)' in local database.") return } - guard let orginalStatus = try await statusesService.get(on: context.application.db, activityPubId: activityPubObject.id) else { + guard let orginalStatus = try await statusesService.get(activityPubId: activityPubObject.id, on: context.db) else { context.logger.warning("Cannot find orginal status '\(activityPubObject.id)' in local database.") return } @@ -422,7 +423,7 @@ final class ActivityPubService: ActivityPubServiceType { let orginalStatusId = try orginalStatus.requireID() let userId = try user.requireID() - guard let status = try await Status.query(on: context.application.db) + guard let status = try await Status.query(on: context.db) .filter(\.$reblog.$id == orginalStatusId) .filter(\.$user.$id == userId) .first() else { @@ -432,13 +433,13 @@ final class ActivityPubService: ActivityPubServiceType { let statusId = try status.requireID() context.logger.info("Deleting status '\(statusId)' (reblog) from local database.") - try await statusesService.delete(id: statusId, on: context.application.db) + try await statusesService.delete(id: statusId, on: context.db) context.logger.info("Recalculating reblogs for orginal status '\(orginalStatusId)' in local database.") - try await statusesService.updateReblogsCount(for: orginalStatusId, on: context.application.db) + try await statusesService.updateReblogsCount(for: orginalStatusId, on: context.db) } - private func unfollow(sourceActorId: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unfollow(sourceActorId: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { guard let followDto = activityPubObject.object as? FollowDto, let objects = followDto.object?.objects() else { return @@ -449,35 +450,35 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func unfollow(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func unfollow(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { context.logger.info("Unfollowing account: '\(activityPubObject.id)' by account '\(sourceProfileUrl)' (from remote server).") - let followsService = context.application.services.followsService - let usersService = context.application.services.usersService + let followsService = context.services.followsService + let usersService = context.services.usersService - let sourceUser = try await usersService.get(on: context.application.db, activityPubProfile: sourceProfileUrl) + let sourceUser = try await usersService.get(activityPubProfile: sourceProfileUrl, on: context.db) guard let sourceUser else { context.logger.warning("Cannot find user '\(sourceProfileUrl)' in local database.") return } - let targetUser = try await usersService.get(on: context.application.db, activityPubProfile: activityPubObject.id) + let targetUser = try await usersService.get(activityPubProfile: activityPubObject.id, on: context.application.db) guard let targetUser else { context.logger.warning("Cannot find user '\(activityPubObject.id)' in local database.") return } - _ = try await followsService.unfollow(on: context.application, sourceId: sourceUser.requireID(), targetId: targetUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: sourceUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: targetUser.requireID()) + _ = try await followsService.unfollow(sourceId: sourceUser.requireID(), targetId: targetUser.requireID(), on: context) + try await usersService.updateFollowCount(for: sourceUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: targetUser.requireID(), on: context.db) } - private func follow(sourceProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext, activityId: String) async throws { + private func follow(sourceProfileUrl: String, activityPubObject: ObjectDto, activityId: String, on context: ExecutionContext) async throws { context.logger.info("Following account: '\(activityPubObject.id)' by account '\(sourceProfileUrl)' (from remote server).") - let searchService = context.application.services.searchService - let followsService = context.application.services.followsService - let usersService = context.application.services.usersService + let searchService = context.services.searchService + let followsService = context.services.followsService + let usersService = context.services.usersService // Download profile from remote server. context.logger.info("Downloading account \(sourceProfileUrl) from remote server.") @@ -488,7 +489,7 @@ final class ActivityPubService: ActivityPubServiceType { return } - let targetUser = try await usersService.get(on: context.application.db, activityPubProfile: activityPubObject.id) + let targetUser = try await usersService.get(activityPubProfile: activityPubObject.id, on: context.db) guard let targetUser else { context.logger.warning("Cannot find local user '\(activityPubObject.id)'.") return @@ -497,17 +498,17 @@ final class ActivityPubService: ActivityPubServiceType { // Relationship is automatically approved when user disabled manual approval. let approved = targetUser.manuallyApprovesFollowers == false - _ = try await followsService.follow(on: context.application, - sourceId: remoteUser.requireID(), + _ = try await followsService.follow(sourceId: remoteUser.requireID(), targetId: targetUser.requireID(), approved: approved, - activityId: activityId) + activityId: activityId, + on: context) - try await usersService.updateFollowCount(on: context.application.db, for: remoteUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: targetUser.requireID()) + try await usersService.updateFollowCount(for: remoteUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: targetUser.requireID(), on: context.db) // Send notification to user about follow. - let notificationsService = context.application.services.notificationsService + let notificationsService = context.services.notificationsService try await notificationsService.create(type: approved ? .follow : .followRequest, to: targetUser, by: remoteUser.requireID(), @@ -516,17 +517,17 @@ final class ActivityPubService: ActivityPubServiceType { // Save into queue information about accepted follow which have to be send to remote instance. if approved { - try await self.respondAccept(on: context, - requesting: remoteUser.activityPubProfile, + try await self.respondAccept(requesting: remoteUser.activityPubProfile, asked: targetUser.activityPubProfile, inbox: remoteUser.userInbox, withId: remoteUser.requireID(), acceptedId: activityId, - privateKey: targetUser.privateKey) + privateKey: targetUser.privateKey, + on: context) } } - private func accept(targetProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func accept(targetProfileUrl: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { guard activityPubObject.type == .follow else { throw ActivityPubError.acceptTypeNotSupported(activityPubObject.type) } @@ -544,30 +545,30 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func accept(sourceProfileUrl: String, targetProfileUrl: String, on context: QueueContext) async throws { + private func accept(sourceProfileUrl: String, targetProfileUrl: String, on context: ExecutionContext) async throws { context.logger.info("Accepting account: '\(sourceProfileUrl)' by account '\(targetProfileUrl)' (from remote server).") - let followsService = context.application.services.followsService - let usersService = context.application.services.usersService + let followsService = context.services.followsService + let usersService = context.services.usersService - let remoteUser = try await usersService.get(on: context.application.db, activityPubProfile: targetProfileUrl) + let remoteUser = try await usersService.get(activityPubProfile: targetProfileUrl, on: context.db) guard let remoteUser else { context.logger.warning("Account '\(targetProfileUrl)' cannot be found in local database.") return } - let sourceUser = try await usersService.get(on: context.application.db, activityPubProfile: sourceProfileUrl) + let sourceUser = try await usersService.get(activityPubProfile: sourceProfileUrl, on: context.db) guard let sourceUser else { context.logger.warning("Account '\(sourceProfileUrl)' cannot be found in local database.") return } - _ = try await followsService.approve(on: context.application.db, sourceId: sourceUser.requireID(), targetId: remoteUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: remoteUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: sourceUser.requireID()) + _ = try await followsService.approve(sourceId: sourceUser.requireID(), targetId: remoteUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: remoteUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: sourceUser.requireID(), on: context.db) } - private func reject(targetProfileUrl: String, activityPubObject: ObjectDto, on context: QueueContext) async throws { + private func reject(targetProfileUrl: String, activityPubObject: ObjectDto, on context: ExecutionContext) async throws { guard activityPubObject.type == .follow else { throw ActivityPubError.rejectTypeNotSupported(activityPubObject.type) } @@ -585,41 +586,41 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func reject(sourceProfileUrl: String, targetProfileUrl: String, on context: QueueContext) async throws { + private func reject(sourceProfileUrl: String, targetProfileUrl: String, on context: ExecutionContext) async throws { context.logger.info("Rejecting account: '\(sourceProfileUrl)' by account '\(targetProfileUrl)' (from remote server).") - let followsService = context.application.services.followsService - let usersService = context.application.services.usersService + let followsService = context.services.followsService + let usersService = context.services.usersService - let remoteUser = try await usersService.get(on: context.application.db, activityPubProfile: targetProfileUrl) + let remoteUser = try await usersService.get(activityPubProfile: targetProfileUrl, on: context.db) guard let remoteUser else { context.logger.warning("Account '\(targetProfileUrl)' cannot be found in local database.") return } - let sourceUser = try await usersService.get(on: context.application.db, activityPubProfile: sourceProfileUrl) + let sourceUser = try await usersService.get(activityPubProfile: sourceProfileUrl, on: context.application.db) guard let sourceUser else { context.logger.warning("Account '\(sourceProfileUrl)' cannot be found in local database.") return } - _ = try await followsService.reject(on: context.application.db, sourceId: sourceUser.requireID(), targetId: remoteUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: remoteUser.requireID()) - try await usersService.updateFollowCount(on: context.application.db, for: sourceUser.requireID()) + _ = try await followsService.reject(sourceId: sourceUser.requireID(), targetId: remoteUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: remoteUser.requireID(), on: context.db) + try await usersService.updateFollowCount(for: sourceUser.requireID(), on: context.db) } - public func isDomainBlockedByInstance(on application: Application, actorId: String) async throws -> Bool { - let instanceBlockedDomainsService = application.services.instanceBlockedDomainsService + public func isDomainBlockedByInstance(actorId: String, on context: ExecutionContext) async throws -> Bool { + let instanceBlockedDomainsService = context.services.instanceBlockedDomainsService guard let url = URL(string: actorId) else { return false } - return try await instanceBlockedDomainsService.exists(on: application.db, url: url) + return try await instanceBlockedDomainsService.exists(url: url, on: context.db) } - public func isDomainBlockedByInstance(on application: Application, activity: ActivityDto) async throws -> Bool { - let instanceBlockedDomainsService = application.services.instanceBlockedDomainsService + public func isDomainBlockedByInstance(activity: ActivityDto, on context: ExecutionContext) async throws -> Bool { + let instanceBlockedDomainsService = context.services.instanceBlockedDomainsService guard let activityPubProfile = activity.actor.actorIds().first else { return false @@ -629,26 +630,26 @@ final class ActivityPubService: ActivityPubServiceType { return false } - return try await instanceBlockedDomainsService.exists(on: application.db, url: url) + return try await instanceBlockedDomainsService.exists(url: url, on: context.db) } - public func isDomainBlockedByUser(on context: QueueContext, actorId: String) async throws -> Bool { - let userBlockedDomainsService = context.application.services.userBlockedDomainsService + public func isDomainBlockedByUser(actorId: String, on context: ExecutionContext) async throws -> Bool { + let userBlockedDomainsService = context.services.userBlockedDomainsService guard let url = URL(string: actorId) else { return false } - return try await userBlockedDomainsService.exists(on: context.application.db, url: url) + return try await userBlockedDomainsService.exists(url: url, on: context.db) } - private func respondAccept(on context: QueueContext, - requesting: String, + private func respondAccept(requesting: String, asked: String, inbox: String?, withId id: Int64, acceptedId: String, - privateKey: String?) async throws { + privateKey: String?, + on context: ExecutionContext) async throws { guard let inbox, let inboxUrl = URL(string: inbox) else { return } @@ -669,19 +670,24 @@ final class ActivityPubService: ActivityPubServiceType { .queues(.apFollowResponder) .dispatch(ActivityPubFollowResponderJob.self, activityPubFollowRespondDto) } - - private func downloadStatus(on context: QueueContext, activityPubId: String) async throws -> Status { - let statusesService = context.application.services.statusesService - let searchService = context.application.services.searchService + + public func downloadStatus(activityPubId: String, on context: ExecutionContext) async throws -> Status { + let statusesService = context.services.statusesService + let searchService = context.services.searchService // When we already have status in database we don't have to downlaod it. - if let status = try await statusesService.get(on: context.application.db, activityPubId: activityPubId) { + if let status = try await statusesService.get(activityPubId: activityPubId, on: context.db) { return status } // Download status JSON from remote server (via ActivityPub endpoints). context.logger.info("Downloading status from remote server: '\(activityPubId)'.") - let noteDto = try await self.downloadRemoteStatus(on: context, activityPubId: activityPubId) + let noteDto = try await self.downloadRemoteStatus(activityPubId: activityPubId, on: context) + + // Verify once again if status not exist in database. + if let status = try await statusesService.get(activityPubId: noteDto.url, on: context.db) { + return status + } if noteDto.attachment?.contains(where: { $0.mediaType.starts(with: "image/") }) == false { context.logger.warning("Object doesn't contain any image media type attachments (status: \(noteDto.id).") @@ -702,20 +708,20 @@ final class ActivityPubService: ActivityPubServiceType { let status = try await statusesService.create(basedOn: noteDto, userId: remoteUser.requireID(), on: context) // Recalculate numer of user statuses. - try await statusesService.updateStatusCount(on: context.application.db, for: remoteUser.requireID()) + try await statusesService.updateStatusCount(for: remoteUser.requireID(), on: context.db) return status } - private func downloadRemoteStatus(on context: QueueContext, activityPubId: String) async throws -> NoteDto { + private func downloadRemoteStatus(activityPubId: String, on context: ExecutionContext) async throws -> NoteDto { do { guard let noteUrl = URL(string: activityPubId) else { await context.logger.store("Invalid URL to note: '\(activityPubId)'.", nil, on: context.application) throw ActivityPubError.invalidNoteUrl(activityPubId) } - let usersService = context.application.services.usersService - guard let defaultSystemUser = try await usersService.getDefaultSystemUser(on: context.application.db) else { + let usersService = context.services.usersService + guard let defaultSystemUser = try await usersService.getDefaultSystemUser(on: context.db) else { throw ActivityPubError.missingInstanceAdminAccount } @@ -731,13 +737,13 @@ final class ActivityPubService: ActivityPubServiceType { } } - private func isRemoteUserFollowedByAnyone(activityPubProfile: String, on context: QueueContext) async throws -> Bool { - let usersService = context.application.services.usersService - guard let user = try await usersService.get(on: context.application.db, activityPubProfile: activityPubProfile) else { + private func isRemoteUserFollowedByAnyone(activityPubProfile: String, on context: ExecutionContext) async throws -> Bool { + let usersService = context.services.usersService + guard let user = try await usersService.get(activityPubProfile: activityPubProfile, on: context.db) else { return false } - let followers = try await Follow.query(on: context.application.db) + let followers = try await Follow.query(on: context.db) .filter(\.$target.$id == user.requireID()) .filter(\.$approved == true) .join(User.self, on: \Follow.$source.$id == \User.$id) @@ -747,13 +753,13 @@ final class ActivityPubService: ActivityPubServiceType { return followers > 0 } - private func isParentStatusInDatabase(replyToActivityPubId: String?, on context: QueueContext) async throws -> Bool { + private func isParentStatusInDatabase(replyToActivityPubId: String?, on context: ExecutionContext) async throws -> Bool { guard let replyToActivityPubId else { return false } - let statusesService = context.application.services.statusesService - guard let _ = try await statusesService.get(on: context.application.db, activityPubId: replyToActivityPubId) else { + let statusesService = context.services.statusesService + guard let _ = try await statusesService.get(activityPubId: replyToActivityPubId, on: context.db) else { return false } diff --git a/Sources/VernissageServer/Services/ActivityPubSignatureService.swift b/Sources/VernissageServer/Services/ActivityPubSignatureService.swift index 8ace2602..a86397ea 100644 --- a/Sources/VernissageServer/Services/ActivityPubSignatureService.swift +++ b/Sources/VernissageServer/Services/ActivityPubSignatureService.swift @@ -25,9 +25,9 @@ extension Application.Services { @_documentation(visibility: private) protocol ActivityPubSignatureServiceType: Sendable { - func validateSignature(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func validateLocalSignature(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws - func validateAlgorith(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) throws + func validateSignature(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func validateLocalSignature(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws + func validateAlgorith(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) throws } /// A service for managing signatures in the ActivityPub protocol. @@ -37,10 +37,10 @@ final class ActivityPubSignatureService: ActivityPubSignatureServiceType { } /// Validate signature. - public func validateSignature(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let searchService = context.application.services.searchService - let cryptoService = context.application.services.cryptoService - let activityPubService = context.application.services.activityPubService + public func validateSignature(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let searchService = context.services.searchService + let cryptoService = context.services.cryptoService + let activityPubService = context.services.activityPubService // Get actor profile URL from signature. let signatureActorId = try self.getSignatureActor(activityPubRequest: activityPubRequest) @@ -49,12 +49,12 @@ final class ActivityPubSignatureService: ActivityPubSignatureServiceType { let payloadActorId = try self.getPayloadActor(activityPubRequest: activityPubRequest) // Check if the actor's domain is blocked by the instance. - if try await activityPubService.isDomainBlockedByInstance(on: context.application, actorId: signatureActorId) { + if try await activityPubService.isDomainBlockedByInstance(actorId: signatureActorId, on: context) { throw ActivityPubError.domainIsBlockedByInstance(signatureActorId) } // Check if the actor's domain is blocked by the instance. - if let payloadActorId, try await activityPubService.isDomainBlockedByInstance(on: context.application, actorId: payloadActorId) { + if let payloadActorId, try await activityPubService.isDomainBlockedByInstance(actorId: payloadActorId, on: context) { throw ActivityPubError.domainIsBlockedByInstance(payloadActorId) } @@ -92,9 +92,9 @@ final class ActivityPubSignatureService: ActivityPubSignatureServiceType { } /// Validate local signature (user is not downloaded from remote). - public func validateLocalSignature(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) async throws { - let usersService = context.application.services.usersService - let cryptoService = context.application.services.cryptoService + public func validateLocalSignature(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) async throws { + let usersService = context.services.usersService + let cryptoService = context.services.cryptoService // Check if request is not old one. try self.verifyTimeWindow(activityPubRequest: activityPubRequest) @@ -109,7 +109,7 @@ final class ActivityPubSignatureService: ActivityPubSignatureServiceType { let actorId = try self.getSignatureActor(activityPubRequest: activityPubRequest) // Download profile from remote server. - guard let user = try await usersService.get(on: context.application.db, activityPubProfile: actorId) else { + guard let user = try await usersService.get(activityPubProfile: actorId, on: context.db) else { throw ActivityPubError.userNotExistsInDatabase(actorId) } @@ -124,7 +124,7 @@ final class ActivityPubSignatureService: ActivityPubSignatureServiceType { } } - public func validateAlgorith(on context: QueueContext, activityPubRequest: ActivityPubRequestDto) throws { + public func validateAlgorith(activityPubRequest: ActivityPubRequestDto, on context: ExecutionContext) throws { guard let signatureHeader = activityPubRequest.headers.keys.first(where: { $0.lowercased() == "signature" }), let signatureHeaderValue = activityPubRequest.headers[signatureHeader] else { throw ActivityPubError.missingSignatureHeader diff --git a/Sources/VernissageServer/Services/AuthenticationClientsService.swift b/Sources/VernissageServer/Services/AuthenticationClientsService.swift index 04cb0399..a30bc6ba 100644 --- a/Sources/VernissageServer/Services/AuthenticationClientsService.swift +++ b/Sources/VernissageServer/Services/AuthenticationClientsService.swift @@ -24,13 +24,13 @@ extension Application.Services { @_documentation(visibility: private) protocol AuthenticationClientsServiceType: Sendable { - func validateUri(on database: Database, uri: String, authClientId: Int64?) async throws + func validate(uri: String, authClientId: Int64?, on database: Database) async throws } /// A website for managing OpenId Connect authorization clients. final class AuthenticationClientsService: AuthenticationClientsServiceType { - func validateUri(on database: Database, uri: String, authClientId: Int64?) async throws { + func validate(uri: String, authClientId: Int64?, on database: Database) async throws { if let unwrapedAuthClientId = authClientId { let authClient = try await AuthClient.query(on: database).group(.and) { verifyUriGroup in verifyUriGroup.filter(\.$uri == uri) diff --git a/Sources/VernissageServer/Services/CaptchaService.swift b/Sources/VernissageServer/Services/CaptchaService.swift index d4c89619..13063e1d 100644 --- a/Sources/VernissageServer/Services/CaptchaService.swift +++ b/Sources/VernissageServer/Services/CaptchaService.swift @@ -24,13 +24,13 @@ extension Application.Services { @_documentation(visibility: private) protocol CaptchaServiceType: Sendable { - func validate(on request: Request, captchaFormResponse: String) async throws -> Bool + func validate(captchaFormResponse: String, on request: Request) async throws -> Bool } /// A service for managing reCaptcha validation. final class CaptchaService: CaptchaServiceType { - public func validate(on request: Request, captchaFormResponse: String) async throws -> Bool { + public func validate(captchaFormResponse: String, on request: Request) async throws -> Bool { let result = try await request.validate(captchaFormResponse: captchaFormResponse) return result } diff --git a/Sources/VernissageServer/Services/EmailsService.swift b/Sources/VernissageServer/Services/EmailsService.swift index 9e80ca71..2c507fef 100644 --- a/Sources/VernissageServer/Services/EmailsService.swift +++ b/Sources/VernissageServer/Services/EmailsService.swift @@ -25,15 +25,15 @@ extension Application.Services { @_documentation(visibility: private) protocol EmailsServiceType: Sendable { - func setServerSettings(on application: Application, hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?) - func dispatchForgotPasswordEmail(on request: Request, user: User, redirectBaseUrl: String) async throws - func dispatchConfirmAccountEmail(on request: Request, user: User, redirectBaseUrl: String) async throws + func setServerSettings(hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?, on application: Application) + func dispatchForgotPasswordEmail(user: User, redirectBaseUrl: String, on request: Request) async throws + func dispatchConfirmAccountEmail(user: User, redirectBaseUrl: String, on request: Request) async throws } /// A website for sending email messages. final class EmailsService: EmailsServiceType { - func setServerSettings(on application: Application, hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?) { + func setServerSettings(hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?, on application: Application) { application.smtp.configuration.hostname = hostName?.value ?? "" if let portValue = port?.value, let portInt = Int(portValue) { @@ -58,7 +58,7 @@ final class EmailsService: EmailsServiceType { } } - func dispatchForgotPasswordEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { + func dispatchForgotPasswordEmail(user: User, redirectBaseUrl: String, on request: Request) async throws { guard let forgotPasswordGuid = user.forgotPasswordGuid else { throw ForgotPasswordError.tokenNotGenerated } @@ -76,11 +76,11 @@ final class EmailsService: EmailsServiceType { ] let localizablesService = request.application.services.localizablesService - let localizedEmailSubject = try await localizablesService.get(on: request.db, code: "email.forgotPassword.subject", locale: user.locale) - let localizedEmailBody = try await localizablesService.get(on: request.db, - code: "email.forgotPassword.body", + let localizedEmailSubject = try await localizablesService.get(code: "email.forgotPassword.subject", locale: user.locale, on: request.db) + let localizedEmailBody = try await localizablesService.get(code: "email.forgotPassword.body", locale: user.locale, - variables: emailVariables) + variables: emailVariables, + on: request.db) let emailAddressDto = EmailAddressDto(address: emailAddress, name: user.name) let email = EmailDto(to: emailAddressDto, @@ -93,7 +93,7 @@ final class EmailsService: EmailsServiceType { .dispatch(EmailJob.self, email, maxRetryCount: 3) } - func dispatchConfirmAccountEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { + func dispatchConfirmAccountEmail(user: User, redirectBaseUrl: String, on request: Request) async throws { guard let userId = user.id else { throw RegisterError.userIdNotExists } @@ -117,11 +117,11 @@ final class EmailsService: EmailsServiceType { ] let localizablesService = request.application.services.localizablesService - let localizedEmailSubject = try await localizablesService.get(on: request.db, code: "email.confirmEmail.subject", locale: user.locale) - let localizedEmailBody = try await localizablesService.get(on: request.db, - code: "email.confirmEmail.body", + let localizedEmailSubject = try await localizablesService.get(code: "email.confirmEmail.subject", locale: user.locale, on: request.db) + let localizedEmailBody = try await localizablesService.get(code: "email.confirmEmail.body", locale: user.locale, - variables: emailVariables) + variables: emailVariables, + on: request.db) let email = EmailDto(to: emailAddressDto, subject: localizedEmailSubject, diff --git a/Sources/VernissageServer/Services/ExecutionContext.swift b/Sources/VernissageServer/Services/ExecutionContext.swift new file mode 100644 index 00000000..9325ea61 --- /dev/null +++ b/Sources/VernissageServer/Services/ExecutionContext.swift @@ -0,0 +1,91 @@ +// +// https://mczachurski.dev +// Copyright © 2024 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +import Vapor +import Fluent +import FluentKit +import Queues + +/// This is a class which wrpas Vapr request or context. +/// Thannks to that class we don't have to duplicate functions. +public final class ExecutionContext: Sendable { + public let request: Request? + public let context: QueueContext? + + public init(request: Request) { + self.request = request + self.context = nil + } + + public init(context: QueueContext) { + self.request = nil + self.context = context + } + + var application: Application { + request?.application + ?? context?.application + ?? Application() // That application should never be returned. + } + + var logger: Logger { + request?.logger + ?? context?.logger + ?? application.logger + } + + var services: Application.Services { + application.services + } + + var settings: Application.Settings { + application.settings + } + + var db: Database { + request?.db + ?? context?.application.db + ?? application.db + } + + var userId: Int64? { + request?.userId + } + + var fileio: FileIO? { + request?.fileio + } + + var client: Client { + request?.client + ?? context?.application.client + ?? application.client + } + + var eventLoop: EventLoop { + request?.eventLoop + ?? context?.eventLoop + ?? application.eventLoopGroup.next() + } + + public func queues(_ queue: QueueName, logger: Logger? = nil) -> any Queue { + request?.queues(queue, logger: logger) + ?? context?.queues(queue) + ?? application.queues.queue(queue, logger: logger) + } +} + +extension Request { + var executionContext: ExecutionContext { + ExecutionContext(request: self) + } +} + +extension QueueContext { + var executionContext: ExecutionContext { + ExecutionContext(context: self) + } +} diff --git a/Sources/VernissageServer/Services/ExternalUsersService.swift b/Sources/VernissageServer/Services/ExternalUsersService.swift index c3118d32..ff8b6535 100644 --- a/Sources/VernissageServer/Services/ExternalUsersService.swift +++ b/Sources/VernissageServer/Services/ExternalUsersService.swift @@ -24,7 +24,7 @@ extension Application.Services { @_documentation(visibility: private) protocol ExternalUsersServiceType: Sendable { - func getRegisteredExternalUser(on database: Database, user: OAuthUser) async throws -> (User?, ExternalUser?) + func getRegisteredExternalUser(user: OAuthUser, on database: Database) async throws -> (User?, ExternalUser?) func getRedirectLocation(authClient: AuthClient, baseAddress: String) throws -> String func getOauthRequest(authClient: AuthClient, baseAddress: String, code: String) -> OAuthRequest } @@ -32,7 +32,7 @@ protocol ExternalUsersServiceType: Sendable { /// A service for managing users created by OpenId Connect. final class ExternalUsersService: ExternalUsersServiceType { - public func getRegisteredExternalUser(on database: Database, user: OAuthUser) async throws -> (User?, ExternalUser?) { + public func getRegisteredExternalUser(user: OAuthUser, on database: Database) async throws -> (User?, ExternalUser?) { let externalUser = try await ExternalUser.query(on: database).with(\.$user).filter(\.$externalId == user.uniqueId).first() if let externalUser = externalUser { diff --git a/Sources/VernissageServer/Services/FlexiFieldService.swift b/Sources/VernissageServer/Services/FlexiFieldService.swift index 85afb116..20c98dc6 100644 --- a/Sources/VernissageServer/Services/FlexiFieldService.swift +++ b/Sources/VernissageServer/Services/FlexiFieldService.swift @@ -25,32 +25,18 @@ extension Application.Services { @_documentation(visibility: private) protocol FlexiFieldServiceType: Sendable { - func getFlexiFields(on database: Database, for userId: Int64) async throws -> [FlexiField] - func dispatchUrlValidator(on request: Request, flexiFields: [FlexiField]) async throws - func dispatchUrlValidator(on context: QueueContext, flexiFields: [FlexiField]) async throws + func getFlexiFields(for userId: Int64, on database: Database) async throws -> [FlexiField] + func dispatchUrlValidator(flexiFields: [FlexiField], on context: ExecutionContext) async throws } /// A service for managing additional user fields. final class FlexiFieldService: FlexiFieldServiceType { - func getFlexiFields(on database: Database, for userId: Int64) async throws -> [FlexiField] { + func getFlexiFields(for userId: Int64, on database: Database) async throws -> [FlexiField] { return try await FlexiField.query(on: database).filter(\.$user.$id == userId).sort(\.$id).all() } - - func dispatchUrlValidator(on request: Request, flexiFields: [FlexiField]) async throws { - for flexiField in flexiFields { - // Process only fields which contains correct urls. - if flexiField.value?.lowercased().contains("https://") == false { - continue - } - - try await request - .queues(.urlValidator) - .dispatch(UrlValidatorJob.self, flexiField, maxRetryCount: 3) - } - } - - func dispatchUrlValidator(on context: QueueContext, flexiFields: [FlexiField]) async throws { + + func dispatchUrlValidator(flexiFields: [FlexiField], on context: ExecutionContext) async throws { for flexiField in flexiFields { // Process only fields which contains correct urls. if flexiField.value?.lowercased().contains("https://") == false { diff --git a/Sources/VernissageServer/Services/FollowsService.swift b/Sources/VernissageServer/Services/FollowsService.swift index d14b1fc2..5fa885db 100644 --- a/Sources/VernissageServer/Services/FollowsService.swift +++ b/Sources/VernissageServer/Services/FollowsService.swift @@ -25,45 +25,45 @@ extension Application.Services { @_documentation(visibility: private) protocol FollowsServiceType: Sendable { /// Get follow information between two users. - func get(on database: Database, sourceId: Int64, targetId: Int64) async throws -> Follow? + func get(sourceId: Int64, targetId: Int64, on database: Database) async throws -> Follow? /// Returns amount of following accounts. - func count(on database: Database, sourceId: Int64) async throws -> Int + func count(sourceId: Int64, on database: Database) async throws -> Int /// Returns list of following accoutns. - func following(on database: Database, sourceId: Int64, onlyApproved: Bool, page: Int, size: Int) async throws -> Page + func following(sourceId: Int64, onlyApproved: Bool, page: Int, size: Int, on database: Database) async throws -> Page /// Returns list of following accoutns. - func following(on request: Request, sourceId: Int64, onlyApproved: Bool, linkableParams: LinkableParams) async throws -> LinkableResult + func following(sourceId: Int64, onlyApproved: Bool, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult /// Returns amount of followers. - func count(on database: Database, targetId: Int64) async throws -> Int + func count(targetId: Int64, on database: Database) async throws -> Int /// Returns list of account that follow account. - func follows(on database: Database, targetId: Int64, onlyApproved: Bool, page: Int, size: Int) async throws -> Page + func follows(targetId: Int64, onlyApproved: Bool, page: Int, size: Int, on database: Database) async throws -> Page /// Returns list of account that follow account. - func follows(on request: Request, targetId: Int64, onlyApproved: Bool, linkableParams: LinkableParams) async throws -> LinkableResult + func follows(targetId: Int64, onlyApproved: Bool, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult /// Follow user. - func follow(on application: Application, sourceId: Int64, targetId: Int64, approved: Bool, activityId: String?) async throws -> Int64 + func follow(sourceId: Int64, targetId: Int64, approved: Bool, activityId: String?, on context: ExecutionContext) async throws -> Int64 /// Unfollow user. - func unfollow(on application: Application, sourceId: Int64, targetId: Int64) async throws -> Int64? + func unfollow(sourceId: Int64, targetId: Int64, on context: ExecutionContext) async throws -> Int64? /// Approve relationship. - func approve(on database: Database, sourceId: Int64, targetId: Int64) async throws + func approve(sourceId: Int64, targetId: Int64, on database: Database) async throws /// Reject relationship. - func reject(on database: Database, sourceId: Int64, targetId: Int64) async throws + func reject(sourceId: Int64, targetId: Int64, on database: Database) async throws /// Relationships that have to be approved. - func toApprove(on request: Request, userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult + func toApprove(userId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult } /// A service for managing user follows. final class FollowsService: FollowsServiceType { - func get(on database: Database, sourceId: Int64, targetId: Int64) async throws -> Follow? { + func get(sourceId: Int64, targetId: Int64, on database: Database) async throws -> Follow? { guard let followFromDatabase = try await Follow.query(on: database) .filter(\.$source.$id == sourceId) .filter(\.$target.$id == targetId) @@ -74,14 +74,14 @@ final class FollowsService: FollowsServiceType { return followFromDatabase } - public func count(on database: Database, sourceId: Int64) async throws -> Int { + public func count(sourceId: Int64, on database: Database) async throws -> Int { return try await Follow.query(on: database).group(.and) { queryGroup in queryGroup.filter(\.$source.$id == sourceId) queryGroup.filter(\.$approved == true) }.count() } - public func following(on database: Database, sourceId: Int64, onlyApproved: Bool, page: Int, size: Int) async throws -> Page { + public func following(sourceId: Int64, onlyApproved: Bool, page: Int, size: Int, on database: Database) async throws -> Page { let queryBuilder = User.query(on: database) .join(Follow.self, on: \User.$id == \Follow.$target.$id) @@ -101,8 +101,8 @@ final class FollowsService: FollowsServiceType { .paginate(PageRequest(page: page, per: size)) } - public func following(on request: Request, sourceId: Int64, onlyApproved: Bool, linkableParams: LinkableParams) async throws -> LinkableResult { - var queryBuilder = Follow.query(on: request.db) + public func following(sourceId: Int64, onlyApproved: Bool, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var queryBuilder = Follow.query(on: context.db) .with(\.$target) { target in target .with(\.$flexiFields) @@ -152,14 +152,14 @@ final class FollowsService: FollowsServiceType { ) } - public func count(on database: Database, targetId: Int64) async throws -> Int { + public func count(targetId: Int64, on database: Database) async throws -> Int { return try await Follow.query(on: database).group(.and) { queryGroup in queryGroup.filter(\.$target.$id == targetId) queryGroup.filter(\.$approved == true) }.count() } - public func follows(on database: Database, targetId: Int64, onlyApproved: Bool, page: Int, size: Int) async throws -> Page { + public func follows(targetId: Int64, onlyApproved: Bool, page: Int, size: Int, on database: Database) async throws -> Page { let queryBuilder = User.query(on: database) .join(Follow.self, on: \User.$id == \Follow.$source.$id) @@ -179,8 +179,8 @@ final class FollowsService: FollowsServiceType { .paginate(PageRequest(page: page, per: size)) } - public func follows(on request: Request, targetId: Int64, onlyApproved: Bool, linkableParams: LinkableParams) async throws -> LinkableResult { - var queryBuilder = Follow.query(on: request.db) + public func follows(targetId: Int64, onlyApproved: Bool, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var queryBuilder = Follow.query(on: context.db) .with(\.$source) { source in source .with(\.$flexiFields) @@ -232,35 +232,35 @@ final class FollowsService: FollowsServiceType { /// At the start following is always not approved (application is waiting from information from remote server). /// After information from remote server (approve/reject, done automatically or manually by the user) relationship is approved. - func follow(on application: Application, sourceId: Int64, targetId: Int64, approved: Bool, activityId: String?) async throws -> Int64 { - if let followFromDatabase = try await Follow.query(on: application.db) + func follow(sourceId: Int64, targetId: Int64, approved: Bool, activityId: String?, on context: ExecutionContext) async throws -> Int64 { + if let followFromDatabase = try await Follow.query(on: context.db) .filter(\.$source.$id == sourceId) .filter(\.$target.$id == targetId) .first() { return try followFromDatabase.requireID() } - let id = application.services.snowflakeService.generate() + let id = context.services.snowflakeService.generate() let follow = Follow(id: id, sourceId: sourceId, targetId: targetId, approved: approved, activityId: activityId) - try await follow.save(on: application.db) + try await follow.save(on: context.db) return try follow.requireID() } - func unfollow(on application: Application, sourceId: Int64, targetId: Int64) async throws -> Int64? { - guard let follow = try await Follow.query(on: application.db) + func unfollow(sourceId: Int64, targetId: Int64, on context: ExecutionContext) async throws -> Int64? { + guard let follow = try await Follow.query(on: context.db) .filter(\.$source.$id == sourceId) .filter(\.$target.$id == targetId) .first() else { return nil } - try await follow.delete(on: application.db) + try await follow.delete(on: context.db) return try follow.requireID() } - func approve(on database: Database, sourceId: Int64, targetId: Int64) async throws { + func approve(sourceId: Int64, targetId: Int64, on database: Database) async throws { guard let followFromDatabase = try await Follow.query(on: database) .filter(\.$source.$id == sourceId) .filter(\.$target.$id == targetId) @@ -272,7 +272,7 @@ final class FollowsService: FollowsServiceType { try await followFromDatabase.save(on: database) } - func reject(on database: Database, sourceId: Int64, targetId: Int64) async throws { + func reject(sourceId: Int64, targetId: Int64, on database: Database) async throws { guard let followFromDatabase = try await Follow.query(on: database) .filter(\.$source.$id == sourceId) .filter(\.$target.$id == targetId) @@ -283,8 +283,8 @@ final class FollowsService: FollowsServiceType { try await followFromDatabase.delete(on: database) } - func toApprove(on request: Request, userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { - var query = Follow.query(on: request.db) + func toApprove(userId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var query = Follow.query(on: context.db) .filter(\.$target.$id == userId) .filter(\.$approved == false) @@ -314,8 +314,8 @@ final class FollowsService: FollowsServiceType { let sortedFollowsToApprove = followsToApprove.sorted(by: { $0.id ?? 0 > $1.id ?? 0 }) let relatedUserIds = sortedFollowsToApprove.map({ $0.$source.id }) - let relationshipsService = request.application.services.relationshipsService - let relationships = try await relationshipsService.relationships(on: request.db, userId: userId, relatedUserIds: relatedUserIds) + let relationshipsService = context.services.relationshipsService + let relationships = try await relationshipsService.relationships(userId: userId, relatedUserIds: relatedUserIds, on: context.db) return LinkableResult( maxId: sortedFollowsToApprove.last?.stringId(), diff --git a/Sources/VernissageServer/Services/InstanceBlockedDomainsService.swift b/Sources/VernissageServer/Services/InstanceBlockedDomainsService.swift index 216aa300..5698c41e 100644 --- a/Sources/VernissageServer/Services/InstanceBlockedDomainsService.swift +++ b/Sources/VernissageServer/Services/InstanceBlockedDomainsService.swift @@ -25,12 +25,12 @@ extension Application.Services { @_documentation(visibility: private) protocol InstanceBlockedDomainsServiceType: Sendable { - func exists(on database: Database, url: URL) async throws -> Bool + func exists(url: URL, on database: Database) async throws -> Bool } /// A service for managing domains blocked by the instance. final class InstanceBlockedDomainsService: InstanceBlockedDomainsServiceType { - public func exists(on database: Database, url: URL) async throws -> Bool { + public func exists(url: URL, on database: Database) async throws -> Bool { guard let host = url.host?.lowercased() else { return false } diff --git a/Sources/VernissageServer/Services/InvitationsService.swift b/Sources/VernissageServer/Services/InvitationsService.swift index e2a48158..b3be1e61 100644 --- a/Sources/VernissageServer/Services/InvitationsService.swift +++ b/Sources/VernissageServer/Services/InvitationsService.swift @@ -25,7 +25,7 @@ extension Application.Services { @_documentation(visibility: private) protocol InvitationsServiceType: Sendable { func get(by code: String, on database: Database) async throws -> Invitation? - func use(code: String, on database: Database, for user: User) async throws + func use(code: String, for user: User, on database: Database) async throws } /// A service for managing invitations to the system. @@ -34,7 +34,7 @@ final class InvitationsService: InvitationsServiceType { return try await Invitation.query(on: database).filter(\.$code == code).first() } - func use(code: String, on database: Database, for user: User) async throws { + func use(code: String, for user: User, on database: Database) async throws { if let invitation = try await self.get(by: code, on: database) { invitation.$invited.id = user.id try await invitation.save(on: database) diff --git a/Sources/VernissageServer/Services/LocalizablesService.swift b/Sources/VernissageServer/Services/LocalizablesService.swift index d96bb0fe..19c51fac 100644 --- a/Sources/VernissageServer/Services/LocalizablesService.swift +++ b/Sources/VernissageServer/Services/LocalizablesService.swift @@ -24,18 +24,18 @@ extension Application.Services { @_documentation(visibility: private) protocol LocalizablesServiceType: Sendable { - func get(on database: Database, code: String, locale: String) async throws -> String - func get(on database: Database, code: String, locale: String, variables: [String:String]?) async throws -> String + func get(code: String, locale: String, on database: Database) async throws -> String + func get(code: String, locale: String, variables: [String:String]?, on database: Database) async throws -> String } /// A service for managing location resources in the system. final class LocalizablesService: LocalizablesServiceType { - func get(on database: Database, code: String, locale: String) async throws -> String { - return try await self.get(on: database, code: code, locale: locale, variables: nil) + func get(code: String, locale: String, on database: Database) async throws -> String { + return try await self.get(code: code, locale: locale, variables: nil, on: database) } - func get(on database: Database, code: String, locale: String, variables: [String:String]?) async throws -> String { + func get(code: String, locale: String, variables: [String:String]?, on database: Database) async throws -> String { let localizable = try await Localizable.query(on: database).group(.and) { localeGroup in localeGroup.filter(\.$code == code) localeGroup.filter(\.$locale == locale) diff --git a/Sources/VernissageServer/Services/NotificationsService.swift b/Sources/VernissageServer/Services/NotificationsService.swift index c976fadc..d6baa3f8 100644 --- a/Sources/VernissageServer/Services/NotificationsService.swift +++ b/Sources/VernissageServer/Services/NotificationsService.swift @@ -26,22 +26,21 @@ extension Application.Services { @_documentation(visibility: private) protocol NotificationsServiceType: Sendable { - func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on request: Request) async throws - func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on context: QueueContext) async throws + func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on context: ExecutionContext) async throws func delete(type: NotificationType, to userId: Int64, by byUserId: Int64, statusId: Int64, on database: Database) async throws - func list(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> [Notification] + func list(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> [Notification] func count(for userId: Int64, on database: Database) async throws -> (count: Int, marker: NotificationMarker?) } /// A service for managing notifications in the system. final class NotificationsService: NotificationsServiceType { - func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on request: Request) async throws { - guard let notification = try await self.create(type: type, to: user, by: byUserId, statusId: statusId, on: request.application) else { + func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on context: ExecutionContext) async throws { + guard let notification = try await self.create(type: type, to: user, by: byUserId, statusId: statusId, on: context) else { return } // When WebPush are disabled we don't have to do nothing more. - guard request.application.settings.cached?.isWebPushEnabled == true else { + guard context.settings.cached?.isWebPushEnabled == true else { return } @@ -49,31 +48,7 @@ final class NotificationsService: NotificationsServiceType { let webPushes = try await self.createWebPushes(for: notification, toUser: user, byUserId: byUserId, - on: request.db) - - // When notifications has been added to database and webpush object has been created we can send it to the user's device. - for webPush in webPushes { - try await request - .queues(.webPush) - .dispatch(WebPushSenderJob.self, webPush, maxRetryCount: 3) - } - } - - func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, on context: QueueContext) async throws { - guard let notification = try await self.create(type: type, to: user, by: byUserId, statusId: statusId, on: context.application) else { - return - } - - // When WebPush are disabled we don't have to do nothing more. - guard context.application.settings.cached?.isWebPushEnabled == true else { - return - } - - // Create object only when user want's to retrieve notification. - let webPushes = try await self.createWebPushes(for: notification, - toUser: user, - byUserId: byUserId, - on: context.application.db) + on: context.db) // When notifications has been added to database and webpush object has been created we can send it to the user's device. for webPush in webPushes { @@ -82,19 +57,19 @@ final class NotificationsService: NotificationsServiceType { .dispatch(WebPushSenderJob.self, webPush, maxRetryCount: 3) } } - + private func create(type: NotificationType, to user: User, by byUserId: Int64, statusId: Int64?, - on application: Application) async throws -> Notification? { + on context: ExecutionContext) async throws -> Notification? { // We have to add new notifications only for local users (remote users cannot sign in here). guard user.isLocal else { return nil } // We can add notifications only when user not muted notifications. - let userMute = try await UserMute.query(on: application.db) + let userMute = try await UserMute.query(on: context.db) .filter(\.$user.$id == user.requireID()) .filter(\.$mutedUser.$id == byUserId) .group(.or) { group in @@ -108,9 +83,9 @@ final class NotificationsService: NotificationsServiceType { } // Save notification to database. - let id = application.services.snowflakeService.generate() + let id = context.services.snowflakeService.generate() let notification = try Notification(id: id, notificationType: type, to: user.requireID(), by: byUserId, statusId: statusId) - try await notification.save(on: application.db) + try await notification.save(on: context.db) return notification } @@ -128,7 +103,7 @@ final class NotificationsService: NotificationsServiceType { try await notification.delete(on: database) } - func list(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> [Notification] { + func list(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> [Notification] { var query = Notification.query(on: database) .filter(\.$user.$id == userId) diff --git a/Sources/VernissageServer/Services/RelationshipsService.swift b/Sources/VernissageServer/Services/RelationshipsService.swift index 218fb927..1b404f93 100644 --- a/Sources/VernissageServer/Services/RelationshipsService.swift +++ b/Sources/VernissageServer/Services/RelationshipsService.swift @@ -24,13 +24,13 @@ extension Application.Services { @_documentation(visibility: private) protocol RelationshipsServiceType: Sendable { - func relationships(on database: Database, userId: Int64, relatedUserIds: [Int64]) async throws -> [RelationshipDto] + func relationships(userId: Int64, relatedUserIds: [Int64], on database: Database) async throws -> [RelationshipDto] } /// A service for managing relationships in the system. final class RelationshipsService: RelationshipsServiceType { - func relationships(on database: Database, userId: Int64, relatedUserIds: [Int64]) async throws -> [RelationshipDto] { + func relationships(userId: Int64, relatedUserIds: [Int64], on database: Database) async throws -> [RelationshipDto] { // Download from database all follows with specified user ids. let follows = try await Follow.query(on: database).group(.or) { group in group diff --git a/Sources/VernissageServer/Services/SearchService.swift b/Sources/VernissageServer/Services/SearchService.swift index f8678866..9bf20586 100644 --- a/Sources/VernissageServer/Services/SearchService.swift +++ b/Sources/VernissageServer/Services/SearchService.swift @@ -28,70 +28,35 @@ extension Application.Services { @_documentation(visibility: private) protocol SearchServiceType: Sendable { - func search(query: String, searchType: SearchTypeDto, request: Request) async throws -> SearchResultDto - func downloadRemoteUser(activityPubProfile: String, on request: Request) async -> SearchResultDto - func downloadRemoteUser(activityPubProfile: String, on context: QueueContext) async throws -> User? - func getRemoteActivityPubProfile(userName: String, on request: Request) async -> String? + func search(query: String, searchType: SearchTypeDto, on context: ExecutionContext) async throws -> SearchResultDto + func downloadRemoteUser(activityPubProfile: String, on context: ExecutionContext) async throws -> User? + func getRemoteActivityPubProfile(userName: String, on context: ExecutionContext) async -> String? } /// A service for searching in the local and remote system. final class SearchService: SearchServiceType { - func search(query: String, searchType: SearchTypeDto, request: Request) async throws -> SearchResultDto { + func search(query: String, searchType: SearchTypeDto, on context: ExecutionContext) async throws -> SearchResultDto { let queryWithoutPrefix = String(query.trimmingPrefix("@")) switch searchType { case .users: - return await self.searchByUsers(query: queryWithoutPrefix, on: request) + return await self.searchByUsers(query: queryWithoutPrefix, on: context) case .statuses: - return await self.searchByStatuses(query: queryWithoutPrefix, on: request) + return await self.searchByStatuses(query: queryWithoutPrefix, tryToDownloadRemote: true, on: context) case .hashtags: - return await self.searchByHashtags(query: queryWithoutPrefix, on: request) + return await self.searchByHashtags(query: queryWithoutPrefix, on: context) } } - func downloadRemoteUser(activityPubProfile: String, on request: Request) async -> SearchResultDto { - guard let personProfile = await self.downloadProfile(activityPubProfile: activityPubProfile, application: request.application) else { - request.logger.warning("ActivityPub profile cannot be downloaded: '\(activityPubProfile)'.") - return SearchResultDto(users: []) - } - - // Download profile icon from remote server. - let profileIconFileName = await self.downloadProfileImage(personProfile: personProfile, on: request) - - // Download profile header from remote server. - let profileImageFileName = await self.downloadHeaderImage(personProfile: personProfile, on: request) - - // Update profile in internal database and return it. - guard let user = await self.update(personProfile: personProfile, - profileIconFileName: profileIconFileName, - profileImageFileName: profileImageFileName, - on: request.application) else { - return SearchResultDto(users: []) - } + func downloadRemoteUser(activityPubProfile: String, on context: ExecutionContext) async throws -> User? { + let usersService = context.services.usersService - let flexiFieldService = request.application.services.flexiFieldService - let usersService = request.application.services.usersService - - let flexiFields = try? await flexiFieldService.getFlexiFields(on: request.db, for: user.requireID()) - let userDto = await usersService.convertToDto(on: request, user: user, flexiFields: flexiFields, roles: nil, attachSensitive: false) - - // Enqueue job for flexi field URL validator. - if let flexiFields { - try? await flexiFieldService.dispatchUrlValidator(on: request, flexiFields: flexiFields) - } - - return SearchResultDto(users: [userDto]) - } - - func downloadRemoteUser(activityPubProfile: String, on context: QueueContext) async throws -> User? { - let usersService = context.application.services.usersService - - let userFromDatabase = try await usersService.get(on: context.application.db, activityPubProfile: activityPubProfile) + let userFromDatabase = try await usersService.get(activityPubProfile: activityPubProfile, on: context.db) if let userFromDatabase, max((userFromDatabase.updatedAt ?? Date.distantPast), (userFromDatabase.createdAt ?? Date.distantPast)) > Date.yesterday { return userFromDatabase } - guard let personProfile = await self.downloadProfile(activityPubProfile: activityPubProfile, application: context.application) else { + guard let personProfile = await self.downloadProfile(activityPubProfile: activityPubProfile, context: context) else { context.logger.warning("ActivityPub profile cannot be downloaded: '\(activityPubProfile)'.") return userFromDatabase } @@ -106,49 +71,49 @@ final class SearchService: SearchServiceType { let user = await self.update(personProfile: personProfile, profileIconFileName: profileIconFileName, profileImageFileName: profileImageFileName, - on: context.application) + on: context) if let user { // Downlaod updated flexi fields. - let flexiFieldService = context.application.services.flexiFieldService - let flexiFields = try? await flexiFieldService.getFlexiFields(on: context.application.db, for: user.requireID()) + let flexiFieldService = context.services.flexiFieldService + let flexiFields = try? await flexiFieldService.getFlexiFields(for: user.requireID(), on: context.db) // Enqueue job for flexi field URL validator. if let flexiFields { - try? await flexiFieldService.dispatchUrlValidator(on: context, flexiFields: flexiFields) + try? await flexiFieldService.dispatchUrlValidator(flexiFields: flexiFields, on: context) } } return user } - func getRemoteActivityPubProfile(userName: String, on request: Request) async -> String? { + func getRemoteActivityPubProfile(userName: String, on context: ExecutionContext) async -> String? { // Get hostname from user query. - guard let baseUrl = self.getBaseUrl(from: userName) else { - request.logger.notice("Base url cannot be parsed from user name: '\(userName)'.") + guard let baseUrl = self.getBaseUrlFrom(query: userName) else { + context.logger.notice("Base url cannot be parsed from user name: '\(userName)'.") return nil } // Url cannot be mentioned in instance blocked domains. - let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: request) + let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: context) guard isBlockedDomain == false else { - request.logger.notice("Base URL is listed in blocked instance domains: '\(userName)'.") + context.logger.notice("Base URL is listed in blocked instance domains: '\(userName)'.") return nil } // Search user profile by remote webfinger. - guard let activityPubProfile = await self.getActivityPubProfile(query: userName, baseUrl: baseUrl, on: request.application) else { - request.logger.warning("ActivityPub profile '\(userName)' cannot be downloaded from: '\(baseUrl)'.") + guard let activityPubProfile = await self.getActivityPubProfile(query: userName, baseUrl: baseUrl, on: context) else { + context.logger.warning("ActivityPub profile '\(userName)' cannot be downloaded from: '\(baseUrl)'.") return nil } return activityPubProfile } - private func downloadProfile(activityPubProfile: String, application: Application) async -> PersonDto? { + private func downloadProfile(activityPubProfile: String, context: ExecutionContext) async -> PersonDto? { do { - let usersService = application.services.usersService - guard let defaultSystemUser = try await usersService.getDefaultSystemUser(on: application.db) else { + let usersService = context.services.usersService + guard let defaultSystemUser = try await usersService.getDefaultSystemUser(on: context.db) else { throw ActivityPubError.missingInstanceAdminAccount } @@ -165,28 +130,28 @@ final class SearchService: SearchServiceType { return userProfile } catch { - await application.logger.store("Error during download profile: '\(activityPubProfile)'.", error, on: application) + await context.logger.store("Error during download profile: '\(activityPubProfile)'.", error, on: context.application) } return nil } - private func searchByUsers(query: String, on request: Request) async -> SearchResultDto { - if self.isLocalSearch(query: query, on: request) { - return await self.searchByLocalUsers(query: query, on: request) + private func searchByUsers(query: String, on context: ExecutionContext) async -> SearchResultDto { + if self.isLocalSearch(query: query, on: context) { + return await self.searchByLocalUsers(query: query, on: context) } else { - return await self.searchByRemoteUsers(query: query, on: request) + return await self.searchByRemoteUsers(query: query, on: context) } } - private func searchByStatuses(query: String, on request: Request) async -> SearchResultDto { + private func searchByStatuses(query: String, tryToDownloadRemote: Bool, on context: ExecutionContext) async -> SearchResultDto { // For empty query we don't have to retrieve anything from database and return empty list. if query.isEmpty { return SearchResultDto(statuses: []) } let id = self.getIdFromQuery(from: query) - let statuses = try? await Status.query(on: request.db) + let statuses = try? await Status.query(on: context.db) .group(.or) { group in group .filter(id: id) @@ -213,24 +178,29 @@ final class SearchService: SearchServiceType { .sort(\.$createdAt, .descending) .paginate(PageRequest(page: 1, per: 20)) + // If the query contains url we can try to download status from remote server. + if tryToDownloadRemote && self.shouldDownloadFromRemote(query: query, on: context) { + return await self.searchByRemoteStatuses(activityPubUrl: query, on: context) + } + guard let statuses else { return SearchResultDto(statuses: []) } - let statusesService = request.application.services.statusesService - let statusesDtos = await statusesService.convertToDtos(on: request, statuses: statuses.items) + let statusesService = context.services.statusesService + let statusesDtos = await statusesService.convertToDtos(statuses: statuses.items, on: context) return SearchResultDto(statuses: statusesDtos) } - private func searchByHashtags(query: String, on request: Request) async -> SearchResultDto { + private func searchByHashtags(query: String, on context: ExecutionContext) async -> SearchResultDto { // For empty query we don't have to retrieve anything from database and return empty list. if query.isEmpty { return SearchResultDto(users: []) } let queryNormalized = query.uppercased() - let hashtags = try? await TrendingHashtag.query(on: request.db) + let hashtags = try? await TrendingHashtag.query(on: context.db) .filter(\.$hashtagNormalized ~~ queryNormalized) .filter(\.$trendingPeriod == .yearly) .sort(\.$createdAt, .descending) @@ -240,7 +210,7 @@ final class SearchService: SearchServiceType { return SearchResultDto(hashtags: []) } - let baseAddress = request.application.settings.cached?.baseAddress ?? "" + let baseAddress = context.settings.cached?.baseAddress ?? "" let hashtagDtos = await hashtags.items.asyncMap { hashtag in HashtagDto(url: "\(baseAddress)/tags/\(hashtag.hashtag)", name: hashtag.hashtag, amount: hashtag.amount) } @@ -248,7 +218,7 @@ final class SearchService: SearchServiceType { return SearchResultDto(hashtags: hashtagDtos) } - private func searchByLocalUsers(query: String, on request: Request) async -> SearchResultDto { + private func searchByLocalUsers(query: String, on context: ExecutionContext) async -> SearchResultDto { // For empty query we don't have to retrieve anything from database and return empty list. if query.isEmpty { return SearchResultDto(users: []) @@ -258,7 +228,7 @@ final class SearchService: SearchServiceType { let userNameNormalized = self.getUserNameFromQuery(from: query) let id = self.getIdFromQuery(from: query) - let users = try? await User.query(on: request.db) + let users = try? await User.query(on: context.db) .group(.or) { group in group .filter(id: id) @@ -272,83 +242,136 @@ final class SearchService: SearchServiceType { .sort(\.$followersCount, .descending) .paginate(PageRequest(page: 1, per: 20)) - // In case of error we have to return empty list. + // If the query contains url we can try to download user from remote server. + if self.shouldDownloadFromRemote(query: query, on: context) { + return await self.searchByRemoteUsers(activityPubProfileUrl: query, on: context) + } + + // In case that we didn't found any user we have to return empty list. guard let users else { - request.logger.notice("Issue during filtering local users.") + context.logger.notice("Issue during filtering local users.") return SearchResultDto(users: []) } - let usersService = request.application.services.usersService - let userDtos = await usersService.convertToDtos(on: request, users: users.items, attachSensitive: false) + let usersService = context.services.usersService + let userDtos = await usersService.convertToDtos(users: users.items, attachSensitive: false, on: context) return SearchResultDto(users: userDtos) } - private func searchByRemoteUsers(query: String, on request: Request) async -> SearchResultDto { + private func searchByRemoteUsers(query: String, on context: ExecutionContext) async -> SearchResultDto { // Get hostname from user query. - guard let baseUrl = self.getBaseUrl(from: query) else { - request.logger.notice("Base url cannot be parsed from user query: '\(query)'.") + guard let baseUrl = self.getBaseUrlFrom(query: query) else { + context.logger.notice("Base url cannot be parsed from user query: '\(query)'.") return SearchResultDto(users: []) } // Url cannot be mentioned in instance blocked domains. - let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: request) + let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: context) guard isBlockedDomain == false else { - request.logger.notice("Base URL is listed in blocked instance domains: '\(query)'.") + context.logger.notice("Base URL is listed in blocked instance domains: '\(query)'.") return SearchResultDto(users: []) } // Search user profile by remote webfinger. - guard let activityPubProfile = await self.getActivityPubProfile(query: query, baseUrl: baseUrl, on: request.application) else { - request.logger.warning("ActivityPub profile '\(query)' cannot be downloaded from: '\(baseUrl)'.") + guard let activityPubProfile = await self.getActivityPubProfile(query: query, baseUrl: baseUrl, on: context) else { + context.logger.warning("ActivityPub profile '\(query)' cannot be downloaded from: '\(baseUrl)'.") return SearchResultDto(users: []) } // Download user profile from remote server. - return await self.downloadRemoteUser(activityPubProfile: activityPubProfile, on: request) + return await self.searchUserOnRemoteServer(activityPubProfile: activityPubProfile, on: context) } - private func downloadProfileImage(personProfile: PersonDto, on request: Request) async -> String? { - guard let icon = personProfile.icon else { - return nil + private func searchByRemoteUsers(activityPubProfileUrl: String, on context: ExecutionContext) async -> SearchResultDto { + // Get hostname from user query. + guard let baseUrl = self.getBaseUrlFrom(url: activityPubProfileUrl) else { + context.logger.notice("Base url cannot be parsed from user query: '\(activityPubProfileUrl)'.") + return SearchResultDto(users: []) } - if icon.url.isEmpty == false { - let storageService = request.application.services.storageService - let fileName = try? await storageService.dowload(url: icon.url, on: request) - request.logger.info("Profile icon has been downloaded and saved: '\(fileName ?? "")'.") - - return fileName + // Url cannot be mentioned in instance blocked domains. + let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: context) + guard isBlockedDomain == false else { + context.logger.notice("Base URL is listed in blocked instance domains: '\(activityPubProfileUrl)'.") + return SearchResultDto(users: []) } - return nil + // Download user profile from remote server. + return await self.searchUserOnRemoteServer(activityPubProfile: activityPubProfileUrl, on: context) } - private func downloadProfileImage(personProfile: PersonDto, on context: QueueContext) async -> String? { - guard let icon = personProfile.icon else { - return nil + private func searchByRemoteStatuses(activityPubUrl: String, on context: ExecutionContext) async -> SearchResultDto { + // Get hostname from user query. + guard let baseUrl = self.getBaseUrlFrom(url: activityPubUrl) else { + context.logger.notice("Base url cannot be parsed from user query: '\(activityPubUrl)'.") + return SearchResultDto(users: []) } - if icon.url.isEmpty == false { - let storageService = context.application.services.storageService - let fileName = try? await storageService.dowload(url: icon.url, on: context) - context.logger.info("Profile icon has been downloaded and saved: '\(fileName ?? "")'.") + // Url cannot be mentioned in instance blocked domains. + let isBlockedDomain = await self.existsInInstanceBlockedList(url: baseUrl, on: context) + guard isBlockedDomain == false else { + context.logger.notice("Base URL is listed in blocked instance domains: '\(activityPubUrl)'.") + return SearchResultDto(users: []) + } + + // Download status from remote server. + do { + let activityPubService = context.services.activityPubService + let downloadedStatus = try await activityPubService.downloadStatus(activityPubId: activityPubUrl, on: context) - return fileName + return await self.searchByStatuses(query: downloadedStatus.activityPubUrl, tryToDownloadRemote: false, on: context) + } + catch { + await context.logger.store("Downloading status '\(activityPubUrl)' from remote server failed.", error, on: context.application) } - return nil + return SearchResultDto(users: []) } - private func downloadHeaderImage(personProfile: PersonDto, on request: Request) async -> String? { - guard let image = personProfile.image else { + private func searchUserOnRemoteServer(activityPubProfile: String, on context: ExecutionContext) async -> SearchResultDto { + guard let personProfile = await self.downloadProfile(activityPubProfile: activityPubProfile, context: context) else { + context.logger.warning("ActivityPub profile cannot be downloaded: '\(activityPubProfile)'.") + return SearchResultDto(users: []) + } + + // Download profile icon from remote server. + let profileIconFileName = await self.downloadProfileImage(personProfile: personProfile, on: context) + + // Download profile header from remote server. + let profileImageFileName = await self.downloadHeaderImage(personProfile: personProfile, on: context) + + // Update profile in internal database and return it. + guard let user = await self.update(personProfile: personProfile, + profileIconFileName: profileIconFileName, + profileImageFileName: profileImageFileName, + on: context) else { + return SearchResultDto(users: []) + } + + let flexiFieldService = context.services.flexiFieldService + let usersService = context.services.usersService + + let flexiFields = try? await flexiFieldService.getFlexiFields(for: user.requireID(), on: context.db) + let userDto = await usersService.convertToDto(user: user, flexiFields: flexiFields, roles: nil, attachSensitive: false, on: context) + + // Enqueue job for flexi field URL validator. + if let flexiFields { + try? await flexiFieldService.dispatchUrlValidator(flexiFields: flexiFields, on: context) + } + + return SearchResultDto(users: [userDto]) + } + + private func downloadProfileImage(personProfile: PersonDto, on context: ExecutionContext) async -> String? { + guard let icon = personProfile.icon else { return nil } - if image.url.isEmpty == false { - let storageService = request.application.services.storageService - let fileName = try? await storageService.dowload(url: image.url, on: request) - request.logger.info("Header image has been downloaded and saved: '\(fileName ?? "")'.") + if icon.url.isEmpty == false { + let storageService = context.services.storageService + let fileName = try? await storageService.dowload(url: icon.url, on: context) + context.logger.info("Profile icon has been downloaded and saved: '\(fileName ?? "")'.") return fileName } @@ -356,13 +379,13 @@ final class SearchService: SearchServiceType { return nil } - private func downloadHeaderImage(personProfile: PersonDto, on context: QueueContext) async -> String? { + private func downloadHeaderImage(personProfile: PersonDto, on context: ExecutionContext) async -> String? { guard let image = personProfile.image else { return nil } if image.url.isEmpty == false { - let storageService = context.application.services.storageService + let storageService = context.services.storageService let fileName = try? await storageService.dowload(url: image.url, on: context) context.logger.info("Header image has been downloaded and saved: '\(fileName ?? "")'.") @@ -372,42 +395,42 @@ final class SearchService: SearchServiceType { return nil } - private func update(personProfile: PersonDto, profileIconFileName: String?, profileImageFileName: String?, on application: Application) async -> User? { + private func update(personProfile: PersonDto, profileIconFileName: String?, profileImageFileName: String?, on context: ExecutionContext) async -> User? { do { - let usersService = application.services.usersService - let userFromDb = try await usersService.get(on: application.db, activityPubProfile: personProfile.id) + let usersService = context.services.usersService + let userFromDb = try await usersService.get(activityPubProfile: personProfile.id, on: context.db) // If user not exist we have to create his account in internal database and return it. if userFromDb == nil { - let newUser = try await usersService.create(on: application, - basedOn: personProfile, + let newUser = try await usersService.create(basedOn: personProfile, withAvatarFileName: profileIconFileName, - withHeaderFileName: profileImageFileName) + withHeaderFileName: profileImageFileName, + on: context) return newUser } else { // If user exist then we have to update uhis account in internal database and return it. let updatedUser = try await usersService.update(user: userFromDb!, - on: application, basedOn: personProfile, withAvatarFileName: profileIconFileName, - withHeaderFileName: profileImageFileName) + withHeaderFileName: profileImageFileName, + on: context) return updatedUser } } catch { - application.logger.warning("Error during creating/updating remote user: '\(personProfile.id)' in local database: '\(error.localizedDescription)'.") + context.logger.warning("Error during creating/updating remote user: '\(personProfile.id)' in local database: '\(error.localizedDescription)'.") return nil } } - private func getActivityPubProfile(query: String, baseUrl: URL, on application: Application) async -> String? { + private func getActivityPubProfile(query: String, baseUrl: URL, on context: ExecutionContext) async -> String? { do { let activityPubClient = ActivityPubClient() // Download link to profile (HostMeta). guard let url = try await self.getActivityPubProfileLink(query: query, baseUrl: baseUrl) else { - application.logger.warning("Error during search user: \(query) on host: \(baseUrl.absoluteString). Cannot calculate user profile.") + context.logger.warning("Error during search user: \(query) on host: \(baseUrl.absoluteString). Cannot calculate user profile.") return nil } @@ -419,7 +442,7 @@ final class SearchService: SearchServiceType { return activityPubProfile } catch { - application.logger.warning("Error during downloading user profile '\(query)' from '\(baseUrl)'. Network error: '\(error.localizedDescription)'.") + context.logger.warning("Error during downloading user profile '\(query)' from '\(baseUrl)'. Network error: '\(error.localizedDescription)'.") return nil } } @@ -455,19 +478,43 @@ final class SearchService: SearchServiceType { return url } - private func existsInInstanceBlockedList(url: URL, on request: Request) async -> Bool { - let instanceBlockedDomainsService = request.application.services.instanceBlockedDomainsService - let exists = try? await instanceBlockedDomainsService.exists(on: request.db, url: url) + private func existsInInstanceBlockedList(url: URL, on context: ExecutionContext) async -> Bool { + let instanceBlockedDomainsService = context.services.instanceBlockedDomainsService + let exists = try? await instanceBlockedDomainsService.exists(url: url, on: context.db) return exists ?? false } - private func getBaseUrl(from query: String) -> URL? { + private func getBaseUrlFrom(query: String) -> URL? { let domainFromQuery = query.split(separator: "@").last ?? "" return URL(string: "https://\(domainFromQuery)") } - private func isLocalSearch(query: String, on request: Request) -> Bool { + private func getBaseUrlFrom(url: String) -> URL? { + let uri = URI(string: url) + guard let domainFromQuery = uri.host?.lowercased() else { + return nil + } + + return URL(string: "https://\(domainFromQuery)") + } + + private func shouldDownloadFromRemote(query: String, on context: ExecutionContext) -> Bool { + let applicationSettings = context.settings.cached + let domain = applicationSettings?.domain ?? "" + + if query.starts(with: "https://\(domain)") { + return false + } + + if query.starts(with: "http://") || query.starts(with: "https://") { + return true + } + + return false + } + + private func isLocalSearch(query: String, on context: ExecutionContext) -> Bool { if query.starts(with: "http://") || query.starts(with: "https://") { return true } @@ -477,7 +524,7 @@ final class SearchService: SearchServiceType { return true } - let applicationSettings = request.application.settings.cached + let applicationSettings = context.settings.cached let domain = applicationSettings?.domain ?? "" if queryParts[1].uppercased() == domain.uppercased() { diff --git a/Sources/VernissageServer/Services/StatusesService.swift b/Sources/VernissageServer/Services/StatusesService.swift index 82cdadd0..882d9252 100644 --- a/Sources/VernissageServer/Services/StatusesService.swift +++ b/Sources/VernissageServer/Services/StatusesService.swift @@ -28,42 +28,42 @@ extension Application.Services { @_documentation(visibility: private) protocol StatusesServiceType: Sendable { - func get(on database: Database, activityPubId: String) async throws -> Status? - func get(on database: Database, id: Int64) async throws -> Status? - func get(on database: Database, ids: [Int64]) async throws -> [Status] - func count(on database: Database, for userId: Int64) async throws -> Int - func count(on database: Database, onlyComments: Bool) async throws -> Int - func note(basedOn status: Status, replyToStatus: Status?, on application: Application) throws -> NoteDto - func updateStatusCount(on database: Database, for userId: Int64) async throws - func send(status statusId: Int64, on context: QueueContext) async throws - func send(reblog statusId: Int64, on context: QueueContext) async throws - func send(unreblog activityPubUnreblog: ActivityPubUnreblogDto, on context: QueueContext) async throws - func send(favourite statusFavouriteId: Int64, on context: QueueContext) async throws - func send(unfavourite statusFavouriteDto: StatusUnfavouriteJobDto, on context: QueueContext) async throws - func create(basedOn noteDto: NoteDto, userId: Int64, on context: QueueContext) async throws -> Status - func createOnLocalTimeline(followersOf userId: Int64, status: Status, on context: QueueContext) async throws - func convertToDto(on request: Request, status: Status, attachments: [Attachment]) async -> StatusDto - func convertToDtos(on request: Request, statuses: [Status]) async -> [StatusDto] - func can(view status: Status, authorizationPayloadId: Int64, on request: Request) async throws -> Bool + func get(activityPubId: String, on database: Database) async throws -> Status? + func get(id: Int64, on database: Database) async throws -> Status? + func get(ids: [Int64], on database: Database) async throws -> [Status] + func count(for userId: Int64, on database: Database) async throws -> Int + func count(onlyComments: Bool, on database: Database) async throws -> Int + func note(basedOn status: Status, replyToStatus: Status?, on context: ExecutionContext) throws -> NoteDto + func updateStatusCount(for userId: Int64, on database: Database) async throws + func send(status statusId: Int64, on context: ExecutionContext) async throws + func send(reblog statusId: Int64, on context: ExecutionContext) async throws + func send(unreblog activityPubUnreblog: ActivityPubUnreblogDto, on context: ExecutionContext) async throws + func send(favourite statusFavouriteId: Int64, on context: ExecutionContext) async throws + func send(unfavourite statusFavouriteDto: StatusUnfavouriteJobDto, on context: ExecutionContext) async throws + func create(basedOn noteDto: NoteDto, userId: Int64, on context: ExecutionContext) async throws -> Status + func createOnLocalTimeline(followersOf userId: Int64, status: Status, on context: ExecutionContext) async throws + func convertToDto(status: Status, attachments: [Attachment], on context: ExecutionContext) async -> StatusDto + func convertToDtos(statuses: [Status], on context: ExecutionContext) async -> [StatusDto] + func can(view status: Status, authorizationPayloadId: Int64, on context: ExecutionContext) async throws -> Bool func getOrginalStatus(id: Int64, on database: Database) async throws -> Status? func getReblogStatus(id: Int64, userId: Int64, on database: Database) async throws -> Status? - func delete(owner userId: Int64, on context: QueueContext) async throws + func delete(owner userId: Int64, on context: ExecutionContext) async throws func delete(id statusId: Int64, on database: Database) async throws - func deleteFromRemote(statusActivityPubId: String, userId: Int64, on context: QueueContext) async throws + func deleteFromRemote(statusActivityPubId: String, userId: Int64, on context: ExecutionContext) async throws func updateReblogsCount(for statusId: Int64, on database: Database) async throws func updateFavouritesCount(for statusId: Int64, on database: Database) async throws - func statuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult - func statuses(linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult + func statuses(for userId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult + func statuses(linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult func ancestors(for statusId: Int64, on database: Database) async throws -> [Status] func descendants(for statusId: Int64, on database: Database) async throws -> [Status] - func reblogged(on request: Request, statusId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult - func favourited(on request: Request, statusId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult - func unlist(on database: Database, statusId: Int64) async throws + func reblogged(statusId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult + func favourited(statusId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult + func unlist(statusId: Int64, on database: Database) async throws } /// A service for managing statuses in the system. final class StatusesService: StatusesServiceType { - func get(on database: Database, activityPubId: String) async throws -> Status? { + func get(activityPubId: String, on database: Database) async throws -> Status? { return try await Status.query(on: database) .with(\.$user) .group(.or) { group in @@ -74,7 +74,7 @@ final class StatusesService: StatusesServiceType { .first() } - func get(on database: Database, id: Int64) async throws -> Status? { + func get(id: Int64, on database: Database) async throws -> Status? { return try await Status.query(on: database) .filter(\.$id == id) .with(\.$user) @@ -94,7 +94,7 @@ final class StatusesService: StatusesServiceType { .first() } - func get(on database: Database, ids: [Int64]) async throws -> [Status] { + func get(ids: [Int64], on database: Database) async throws -> [Status] { return try await Status.query(on: database) .filter(\.$id ~~ ids) .with(\.$user) @@ -114,11 +114,11 @@ final class StatusesService: StatusesServiceType { .all() } - func count(on database: Database, for userId: Int64) async throws -> Int { + func count(for userId: Int64, on database: Database) async throws -> Int { return try await Status.query(on: database).filter(\.$user.$id == userId).count() } - func count(on database: Database, onlyComments: Bool) async throws -> Int { + func count(onlyComments: Bool, on database: Database) async throws -> Int { var query = Status.query(on: database) .filter(\.$reblog.$id == nil) .filter(\.$isLocal == true) @@ -132,10 +132,10 @@ final class StatusesService: StatusesServiceType { return try await query.count() } - func note(basedOn status: Status, replyToStatus: Status?, on application: Application) throws -> NoteDto { - let baseStoragePath = application.services.storageService.getBaseStoragePath(on: application) + func note(basedOn status: Status, replyToStatus: Status?, on context: ExecutionContext) throws -> NoteDto { + let baseStoragePath = context.services.storageService.getBaseStoragePath(on: context) - let appplicationSettings = application.settings.cached + let appplicationSettings = context.settings.cached let baseAddress = appplicationSettings?.baseAddress ?? "" let hashtags = status.hashtags.map({NoteHashtagDto(from: $0, baseAddress: baseAddress)}) @@ -165,7 +165,7 @@ final class StatusesService: StatusesServiceType { return noteDto } - func updateStatusCount(on database: Database, for userId: Int64) async throws { + func updateStatusCount(for userId: Int64, on database: Database) async throws { guard let sql = database as? SQLDatabase else { return } @@ -177,8 +177,8 @@ final class StatusesService: StatusesServiceType { """).run() } - func send(status statusId: Int64, on context: QueueContext) async throws { - guard let status = try await self.get(on: context.application.db, id: statusId) else { + func send(status statusId: Int64, on context: ExecutionContext) async throws { + guard let status = try await self.get(id: statusId, on: context.application.db) else { throw Abort(.notFound) } @@ -192,7 +192,7 @@ final class StatusesService: StatusesServiceType { case .public, .followers: if let replyToStatusId { // Comments have to be send to the same servers where orginal status has been send. - guard let previousStatus = try await self.get(on: context.application.db, id: replyToStatusId) else { + guard let previousStatus = try await self.get(id: replyToStatusId, on: context.application.db) else { break } @@ -229,8 +229,8 @@ final class StatusesService: StatusesServiceType { } } - func send(reblog statusId: Int64, on context: QueueContext) async throws { - guard let status = try await self.get(on: context.application.db, id: statusId) else { + func send(reblog statusId: Int64, on context: ExecutionContext) async throws { + guard let status = try await self.get(id: statusId, on: context.application.db) else { throw Abort(.notFound) } @@ -249,8 +249,8 @@ final class StatusesService: StatusesServiceType { } } - func send(unreblog activityPubUnreblog: ActivityPubUnreblogDto, on context: QueueContext) async throws { - guard let orginalStatus = try await self.get(on: context.application.db, id: activityPubUnreblog.orginalStatusId) else { + func send(unreblog activityPubUnreblog: ActivityPubUnreblogDto, on context: ExecutionContext) async throws { + guard let orginalStatus = try await self.get(id: activityPubUnreblog.orginalStatusId, on: context.db) else { throw Abort(.notFound) } @@ -262,8 +262,8 @@ final class StatusesService: StatusesServiceType { } } - func send(favourite statusFavouriteId: Int64, on context: QueueContext) async throws { - let statusFavourite = try await StatusFavourite.query(on: context.application.db) + func send(favourite statusFavouriteId: Int64, on context: ExecutionContext) async throws { + let statusFavourite = try await StatusFavourite.query(on: context.db) .filter(\.$id == statusFavouriteId) .with(\.$user) .with(\.$status) { status in @@ -284,8 +284,8 @@ final class StatusesService: StatusesServiceType { } } - func send(unfavourite statusFavouriteDto: StatusUnfavouriteJobDto, on context: QueueContext) async throws { - let status = try await Status.query(on: context.application.db) + func send(unfavourite statusFavouriteDto: StatusUnfavouriteJobDto, on context: ExecutionContext) async throws { + let status = try await Status.query(on: context.db) .filter(\.$id == statusFavouriteDto.statusId) .with(\.$user) .first() @@ -294,7 +294,7 @@ final class StatusesService: StatusesServiceType { throw Abort(.notFound) } - let user = try await User.query(on: context.application.db) + let user = try await User.query(on: context.db) .filter(\.$id == statusFavouriteDto.userId) .first() @@ -311,12 +311,12 @@ final class StatusesService: StatusesServiceType { } } - func create(basedOn noteDto: NoteDto, userId: Int64, on context: QueueContext) async throws -> Status { + func create(basedOn noteDto: NoteDto, userId: Int64, on context: ExecutionContext) async throws -> Status { var replyToStatus: Status? = nil if let replyToActivityPubId = noteDto.inReplyTo { context.logger.info("Downloading commented status '\(replyToActivityPubId)' from local database.") - replyToStatus = try await self.get(on: context.application.db, activityPubId: replyToActivityPubId) + replyToStatus = try await self.get(activityPubId: replyToActivityPubId, on: context.application.db) if replyToStatus == nil { context.logger.info("Status '\(replyToActivityPubId)' cannot found in local database. Adding comment has been terminated.") @@ -384,7 +384,7 @@ final class StatusesService: StatusesServiceType { // We can add notification to user about new comment/mention. if let replyToStatus, - let statusFromDatabase = try await self.get(on: context.application.db, id: status.requireID()) { + let statusFromDatabase = try await self.get(id: status.requireID(), on: context.application.db) { let notificationsService = context.application.services.notificationsService try await notificationsService.create(type: .newComment, @@ -399,10 +399,10 @@ final class StatusesService: StatusesServiceType { return status } - func createOnLocalTimeline(followersOf userId: Int64, status: Status, on context: QueueContext) async throws { + func createOnLocalTimeline(followersOf userId: Int64, status: Status, on context: ExecutionContext) async throws { let isReblog = status.$reblog.id != nil - try await Follow.query(on: context.application.db) + try await Follow.query(on: context.db) .filter(\.$target.$id == userId) .filter(\.$approved == true) .join(User.self, on: \Follow.$source.$id == \User.$id) @@ -454,8 +454,8 @@ final class StatusesService: StatusesServiceType { } } - public func reblogged(on request: Request, statusId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { - var queryBuilder = Status.query(on: request.db) + public func reblogged(statusId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var queryBuilder = Status.query(on: context.db) .with(\.$user) { user in user .with(\.$flexiFields) @@ -495,8 +495,8 @@ final class StatusesService: StatusesServiceType { ) } - public func favourited(on request: Request, statusId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { - var queryBuilder = StatusFavourite.query(on: request.db) + public func favourited(statusId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var queryBuilder = StatusFavourite.query(on: context.db) .with(\.$user) { user in user .with(\.$flexiFields) @@ -536,8 +536,8 @@ final class StatusesService: StatusesServiceType { ) } - private func getUserMute(userId: Int64, mutedUserId: Int64, on context: QueueContext) async throws -> UserMute { - let userMute = try await UserMute.query(on: context.application.db) + private func getUserMute(userId: Int64, mutedUserId: Int64, on context: ExecutionContext) async throws -> UserMute { + let userMute = try await UserMute.query(on: context.db) .filter(\.$user.$id == userId) .filter(\.$mutedUser.$id == mutedUserId) .group(.or) { group in @@ -551,17 +551,17 @@ final class StatusesService: StatusesServiceType { return userMute } - let id = context.application.services.snowflakeService.generate() + let id = context.services.snowflakeService.generate() return UserMute(id: id, userId: userId, mutedUserId: mutedUserId, muteStatuses: false, muteReblogs: false, muteNotifications: false) } - private func alreadyExistsInUserTimeline(userId: Int64, status: Status, on context: QueueContext) async -> Bool { + private func alreadyExistsInUserTimeline(userId: Int64, status: Status, on context: ExecutionContext) async -> Bool { guard let orginalStatusId = status.$reblog.id ?? status.id else { return false } // Check if user alredy have orginal status (picture) on timeline (as orginal picture or reblogged). - let statuses = try? await UserStatus.query(on: context.application.db) + let statuses = try? await UserStatus.query(on: context.db) .join(Status.self, on: \UserStatus.$status.$id == \Status.$id) .filter(\.$user.$id == userId) .group(.or) { group in @@ -575,14 +575,14 @@ final class StatusesService: StatusesServiceType { } /// Create notification about new comment to status (for comment/status owner only). - private func notifyOwnerAboutComment(toStatusId: Int64, by userId: Int64, on context: QueueContext) async throws { - guard let status = try await self.get(on: context.application.db, id: toStatusId) else { + private func notifyOwnerAboutComment(toStatusId: Int64, by userId: Int64, on context: ExecutionContext) async throws { + guard let status = try await self.get(id: toStatusId, on: context.db) else { return } - let ancestors = try await self.ancestors(for: toStatusId, on: context.application.db) + let ancestors = try await self.ancestors(for: toStatusId, on: context.db) - let notificationsService = context.application.services.notificationsService + let notificationsService = context.services.notificationsService try await notificationsService.create(type: .newComment, to: status.user, by: userId, @@ -590,9 +590,9 @@ final class StatusesService: StatusesServiceType { on: context) } - private func createMentionNotifications(status: Status, on context: QueueContext) async throws { + private func createMentionNotifications(status: Status, on context: ExecutionContext) async throws { for mention in status.mentions { - let user = try await User.query(on: context.application.db) + let user = try await User.query(on: context.db) .group(.or) { group in group .filter(\.$userNameNormalized == mention.userNameNormalized) @@ -605,7 +605,7 @@ final class StatusesService: StatusesServiceType { } // Create notification for mentioned user. - let notificationsService = context.application.services.notificationsService + let notificationsService = context.services.notificationsService try await notificationsService.create(type: .mention, to: user, by: status.$user.id, @@ -614,8 +614,8 @@ final class StatusesService: StatusesServiceType { } } - private func createFavouriteOnRemoteServer(statusFavourite: StatusFavourite, on context: QueueContext) async throws { - guard let privateKey = try await User.query(on: context.application.db).filter(\.$id == statusFavourite.user.requireID()).first()?.privateKey else { + private func createFavouriteOnRemoteServer(statusFavourite: StatusFavourite, on context: ExecutionContext) async throws { + guard let privateKey = try await User.query(on: context.db).filter(\.$id == statusFavourite.user.requireID()).first()?.privateKey else { context.logger.warning("Favourite: '\(statusFavourite.stringId() ?? "")' cannot be send to shared inbox. Missing private key for user '\(statusFavourite.user.stringId() ?? "")'.") return } @@ -643,8 +643,8 @@ final class StatusesService: StatusesServiceType { private func createUnfavouriteOnRemoteServer(statusFavouriteId: String, user: User, status: Status, - on context: QueueContext) async throws { - guard let privateKey = try await User.query(on: context.application.db).filter(\.$id == user.requireID()).first()?.privateKey else { + on context: ExecutionContext) async throws { + guard let privateKey = try await User.query(on: context.db).filter(\.$id == user.requireID()).first()?.privateKey else { context.logger.warning("Unfavourite: '\(statusFavouriteId)' cannot be send to shared inbox. Missing private key for user '\(user.stringId() ?? "")'.") return } @@ -669,7 +669,7 @@ final class StatusesService: StatusesServiceType { } } - private func createOnRemoteTimeline(status: Status, followersOf userId: Int64, on context: QueueContext) async throws { + private func createOnRemoteTimeline(status: Status, followersOf userId: Int64, on context: ExecutionContext) async throws { guard let privateKey = try await User.query(on: context.application.db).filter(\.$id == status.user.requireID()).first()?.privateKey else { context.logger.warning("Status: '\(status.stringId() ?? "")' cannot be send to shared inbox. Missing private key for user '\(status.user.stringId() ?? "")'.") return @@ -677,10 +677,10 @@ final class StatusesService: StatusesServiceType { var replyToStatus: Status? = nil if let replyToStatusId = status.$replyToStatus.id { - replyToStatus = try await self.get(on: context.application.db, id: replyToStatusId) + replyToStatus = try await self.get(id: replyToStatusId, on: context.application.db) } - let noteDto = try self.note(basedOn: status, replyToStatus: replyToStatus, on: context.application) + let noteDto = try self.note(basedOn: status, replyToStatus: replyToStatus, on: context) let follows = try await Follow.query(on: context.application.db) .filter(\.$target.$id == userId) @@ -709,8 +709,8 @@ final class StatusesService: StatusesServiceType { } } - private func createAnnoucmentsOnRemoteTimeline(status: Status, followersOf userId: Int64, on context: QueueContext) async throws { - guard let privateKey = try await User.query(on: context.application.db).filter(\.$id == status.user.requireID()).first()?.privateKey else { + private func createAnnoucmentsOnRemoteTimeline(status: Status, followersOf userId: Int64, on context: ExecutionContext) async throws { + guard let privateKey = try await User.query(on: context.db).filter(\.$id == status.user.requireID()).first()?.privateKey else { context.logger.warning("Status: '\(status.stringId() ?? "")' cannot be announce to shared inbox. Missing private key for user '\(status.user.stringId() ?? "")'.") return } @@ -720,7 +720,7 @@ final class StatusesService: StatusesServiceType { return } - guard let reblogStatus = try await Status.query(on: context.application.db) + guard let reblogStatus = try await Status.query(on: context.db) .filter(\.$id == reblogStatusId) .with(\.$user) .first() else { @@ -728,7 +728,7 @@ final class StatusesService: StatusesServiceType { return } - let follows = try await Follow.query(on: context.application.db) + let follows = try await Follow.query(on: context.db) .filter(\.$target.$id == userId) .filter(\.$approved == true) .join(User.self, on: \Follow.$source.$id == \User.$id) @@ -760,13 +760,13 @@ final class StatusesService: StatusesServiceType { } } - func deleteAnnoucmentsFromRemoteTimeline(activityPubUnreblog: ActivityPubUnreblogDto, on context: QueueContext) async throws { - guard let privateKey = try await User.query(on: context.application.db).filter(\.$id == activityPubUnreblog.userId).first()?.privateKey else { + func deleteAnnoucmentsFromRemoteTimeline(activityPubUnreblog: ActivityPubUnreblogDto, on context: ExecutionContext) async throws { + guard let privateKey = try await User.query(on: context.db).filter(\.$id == activityPubUnreblog.userId).first()?.privateKey else { context.logger.warning("Status: '\(activityPubUnreblog.activityPubReblogStatusId)' cannot be unannounced from shared inbox. Missing private key for user '\(activityPubUnreblog.activityPubProfile)'.") return } - let follows = try await Follow.query(on: context.application.db) + let follows = try await Follow.query(on: context.db) .filter(\.$target.$id == activityPubUnreblog.userId) .filter(\.$approved == true) .join(User.self, on: \Follow.$source.$id == \User.$id) @@ -798,18 +798,18 @@ final class StatusesService: StatusesServiceType { } } - func convertToDtos(on request: Request, statuses: [Status]) async -> [StatusDto] { - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) - let baseAddress = request.application.settings.cached?.baseAddress ?? "" + func convertToDtos(statuses: [Status], on context: ExecutionContext) async -> [StatusDto] { + let baseStoragePath = context.services.storageService.getBaseStoragePath(on: context) + let baseAddress = context.settings.cached?.baseAddress ?? "" let reblogIds = statuses.compactMap { $0.$reblog.id } - let reblogStatuses = try? await self.get(on: request.db, ids: reblogIds) + let reblogStatuses = try? await self.get(ids: reblogIds, on: context.db) let allStatusIds = statuses.compactMap { $0.id } + reblogIds - let favouritedStatuses = try? await self.statusesAreFavourited(on: request, statusIds: allStatusIds) - let rebloggedStatuses = try? await self.statusesAreReblogged(on: request, statusIds: allStatusIds) - let bookmarkedStatuses = try? await self.statusesAreBookmarked(on: request, statusIds: allStatusIds) - let featuredStatuses = try? await self.statusesAreFeatured(on: request, statusIds: allStatusIds) + let favouritedStatuses = try? await self.statusesAreFavourited(statusIds: allStatusIds, on: context) + let rebloggedStatuses = try? await self.statusesAreReblogged(statusIds: allStatusIds, on: context) + let bookmarkedStatuses = try? await self.statusesAreBookmarked(statusIds: allStatusIds, on: context) + let featuredStatuses = try? await self.statusesAreFeatured(statusIds: allStatusIds, on: context) let statusDtos = await statuses.asyncMap { status in var reblogDto: StatusDto? = nil @@ -841,21 +841,21 @@ final class StatusesService: StatusesServiceType { return statusDtos } - func convertToDto(on request: Request, status: Status, attachments: [Attachment]) async -> StatusDto { - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) - let baseAddress = request.application.settings.cached?.baseAddress ?? "" + func convertToDto(status: Status, attachments: [Attachment], on context: ExecutionContext) async -> StatusDto { + let baseStoragePath = context.services.storageService.getBaseStoragePath(on: context) + let baseAddress = context.settings.cached?.baseAddress ?? "" let attachmentDtos = attachments.map({ AttachmentDto(from: $0, baseStoragePath: baseStoragePath) }) - let isFavourited = try? await self.statusIsFavourited(on: request, statusId: status.requireID()) - let isReblogged = try? await self.statusIsReblogged(on: request, statusId: status.requireID()) - let isBookmarked = try? await self.statusIsBookmarked(on: request, statusId: status.requireID()) - let isFeatured = try? await self.statusIsFeatured(on: request, statusId: status.requireID()) + let isFavourited = try? await self.statusIsFavourited(statusId: status.requireID(), on: context) + let isReblogged = try? await self.statusIsReblogged(statusId: status.requireID(), on: context) + let isBookmarked = try? await self.statusIsBookmarked(statusId: status.requireID(), on: context) + let isFeatured = try? await self.statusIsFeatured(statusId: status.requireID(), on: context) var reblogDto: StatusDto? if let reblogId = status.$reblog.id, - let reblog = try? await self.get(on: request.db, id: reblogId) { - reblogDto = await self.convertToDto(on: request, status: reblog, attachments: reblog.attachments) + let reblog = try? await self.get(id: reblogId, on: context.db) { + reblogDto = await self.convertToDto(status: reblog, attachments: reblog.attachments, on: context) } return StatusDto(from: status, @@ -869,7 +869,7 @@ final class StatusesService: StatusesServiceType { isFeatured: isFeatured ?? false) } - func can(view status: Status, authorizationPayloadId: Int64, on request: Request) async throws -> Bool { + func can(view status: Status, authorizationPayloadId: Int64, on context: ExecutionContext) async throws -> Bool { // When user is owner of the status. if status.user.id == authorizationPayloadId { return true @@ -881,7 +881,7 @@ final class StatusesService: StatusesServiceType { } // For mentioned visibility we have to check if user has been connected with status. - if try await UserStatus.query(on: request.db) + if try await UserStatus.query(on: context.db) .filter(\.$status.$id == status.requireID()) .filter(\.$user.$id == authorizationPayloadId) .first() != nil { @@ -892,7 +892,7 @@ final class StatusesService: StatusesServiceType { } func getOrginalStatus(id: Int64, on database: Database) async throws -> Status? { - let status = try await self.get(on: database, id: id) + let status = try await self.get(id: id, on: database) guard let status else { return nil } @@ -901,7 +901,7 @@ final class StatusesService: StatusesServiceType { return status } - return try await self.get(on: database, id: reblogId) + return try await self.get(id: reblogId, on: database) } func getReblogStatus(id: Int64, userId: Int64, on database: Database) async throws -> Status? { @@ -912,7 +912,7 @@ final class StatusesService: StatusesServiceType { // We have already reblog status Id. if let status, status.$reblog.id != nil { - return try await self.get(on: database, id: status.requireID()) + return try await self.get(id: status.requireID(), on: database) } // If not we have to get status which reblogs status by the user. @@ -925,7 +925,7 @@ final class StatusesService: StatusesServiceType { return nil } - return try await self.get(on: database, id: reblog.requireID()) + return try await self.get(id: reblog.requireID(), on: database) } func updateReblogsCount(for statusId: Int64, on database: Database) async throws { @@ -952,8 +952,8 @@ final class StatusesService: StatusesServiceType { """).run() } - func delete(owner userId: Int64, on context: QueueContext) async throws { - let statuses = try await Status.query(on: context.application.db) + func delete(owner userId: Int64, on context: ExecutionContext) async throws { + let statuses = try await Status.query(on: context.db) .filter(\.$user.$id == userId) .field(\.$id) .all() @@ -961,7 +961,7 @@ final class StatusesService: StatusesServiceType { var errorOccurred = false for status in statuses { do { - try await self.delete(id: status.requireID(), on: context.application.db) + try await self.delete(id: status.requireID(), on: context.db) } catch { errorOccurred = true await context.logger.store("Failed to delete status: '\(status.stringId() ?? "")'.", error, on: context.application) @@ -1074,8 +1074,8 @@ final class StatusesService: StatusesServiceType { } } - func deleteFromRemote(statusActivityPubId: String, userId: Int64, on context: QueueContext) async throws { - guard let user = try await User.query(on: context.application.db) + func deleteFromRemote(statusActivityPubId: String, userId: Int64, on context: ExecutionContext) async throws { + guard let user = try await User.query(on: context.db) .filter(\.$id == userId) .withDeleted() .first() else { @@ -1088,7 +1088,7 @@ final class StatusesService: StatusesServiceType { return } - let users = try await User.query(on: context.application.db) + let users = try await User.query(on: context.db) .filter(\.$isLocal == false) .field(\.$sharedInbox) .unique() @@ -1112,8 +1112,8 @@ final class StatusesService: StatusesServiceType { } } - func statuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult { - var query = Status.query(on: request.db) + func statuses(for userId: Int64, linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var query = Status.query(on: context.db) .group(.or) { group in group .filter(\.$visibility ~~ [.public]) @@ -1161,8 +1161,8 @@ final class StatusesService: StatusesServiceType { ) } - func statuses(linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult { - var query = Status.query(on: request.db) + func statuses(linkableParams: LinkableParams, on context: ExecutionContext) async throws -> LinkableResult { + var query = Status.query(on: context.db) .filter(\.$visibility ~~ [.public]) .sort(\.$createdAt, .descending) .with(\.$attachments) { attachment in @@ -1224,7 +1224,7 @@ final class StatusesService: StatusesServiceType { var currentReplyToStatusId: Int64? = replyToStatusId while let currentStatudId = currentReplyToStatusId { - if let ancestor = try await self.get(on: database, id: currentStatudId) { + if let ancestor = try await self.get(id: currentStatudId, on: database) { list.insert(ancestor, at: 0) currentReplyToStatusId = ancestor.$replyToStatus.id } else { @@ -1257,18 +1257,18 @@ final class StatusesService: StatusesServiceType { .all() } - func unlist(on database: Database, statusId: Int64) async throws { + func unlist(statusId: Int64, on database: Database) async throws { try await UserStatus.query(on: database) .filter(\.$status.$id == statusId) .delete() } - private func statusIsReblogged(on request: Request, statusId: Int64) async throws -> Bool { - guard let authorizationPayloadId = request.userId else { + private func statusIsReblogged(statusId: Int64, on context: ExecutionContext) async throws -> Bool { + guard let authorizationPayloadId = context.userId else { return false } - let amountOfStatuses = try await Status.query(on: request.db) + let amountOfStatuses = try await Status.query(on: context.db) .filter(\.$reblog.$id == statusId) .filter(\.$user.$id == authorizationPayloadId) .count() @@ -1276,12 +1276,12 @@ final class StatusesService: StatusesServiceType { return amountOfStatuses > 0 } - private func statusesAreReblogged(on request: Request, statusIds: [Int64]) async throws -> [Int64] { - guard let authorizationPayloadId = request.userId else { + private func statusesAreReblogged(statusIds: [Int64], on context: ExecutionContext) async throws -> [Int64] { + guard let authorizationPayloadId = context.userId else { return [] } - let rebloggedStatuses = try await Status.query(on: request.db) + let rebloggedStatuses = try await Status.query(on: context.db) .filter(\.$reblog.$id ~~ statusIds) .filter(\.$user.$id == authorizationPayloadId) .field(\.$reblog.$id) @@ -1290,12 +1290,12 @@ final class StatusesService: StatusesServiceType { return rebloggedStatuses.compactMap({ $0.$reblog.id }) } - private func statusIsFavourited(on request: Request, statusId: Int64) async throws -> Bool { - guard let authorizationPayloadId = request.userId else { + private func statusIsFavourited(statusId: Int64, on context: ExecutionContext) async throws -> Bool { + guard let authorizationPayloadId = context.userId else { return false } - let amountOfFavourites = try await StatusFavourite.query(on: request.db) + let amountOfFavourites = try await StatusFavourite.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$status.$id == statusId) .count() @@ -1303,12 +1303,12 @@ final class StatusesService: StatusesServiceType { return amountOfFavourites > 0 } - private func statusesAreFavourited(on request: Request, statusIds: [Int64]) async throws -> [Int64] { - guard let authorizationPayloadId = request.userId else { + private func statusesAreFavourited(statusIds: [Int64], on context: ExecutionContext) async throws -> [Int64] { + guard let authorizationPayloadId = context.userId else { return [] } - let favouritedStatuses = try await StatusFavourite.query(on: request.db) + let favouritedStatuses = try await StatusFavourite.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$status.$id ~~ statusIds) .field(\.$status.$id) @@ -1317,12 +1317,12 @@ final class StatusesService: StatusesServiceType { return favouritedStatuses.map({ $0.$status.id }) } - private func statusIsBookmarked(on request: Request, statusId: Int64) async throws -> Bool { - guard let authorizationPayloadId = request.userId else { + private func statusIsBookmarked(statusId: Int64, on context: ExecutionContext) async throws -> Bool { + guard let authorizationPayloadId = context.userId else { return false } - let amountOfBookmarks = try await StatusBookmark.query(on: request.db) + let amountOfBookmarks = try await StatusBookmark.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$status.$id == statusId) .count() @@ -1330,12 +1330,12 @@ final class StatusesService: StatusesServiceType { return amountOfBookmarks > 0 } - private func statusesAreBookmarked(on request: Request, statusIds: [Int64]) async throws -> [Int64] { - guard let authorizationPayloadId = request.userId else { + private func statusesAreBookmarked(statusIds: [Int64], on context: ExecutionContext) async throws -> [Int64] { + guard let authorizationPayloadId = context.userId else { return [] } - let bookmarkedStatuses = try await StatusBookmark.query(on: request.db) + let bookmarkedStatuses = try await StatusBookmark.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$status.$id ~~ statusIds) .field(\.$status.$id) @@ -1344,20 +1344,20 @@ final class StatusesService: StatusesServiceType { return bookmarkedStatuses.map({ $0.$status.id }) } - private func statusIsFeatured(on request: Request, statusId: Int64) async throws -> Bool { - let amount = try await FeaturedStatus.query(on: request.db) + private func statusIsFeatured(statusId: Int64, on context: ExecutionContext) async throws -> Bool { + let amount = try await FeaturedStatus.query(on: context.db) .filter(\.$status.$id == statusId) .count() return amount > 0 } - private func statusesAreFeatured(on request: Request, statusIds: [Int64]) async throws -> [Int64] { - guard let authorizationPayloadId = request.userId else { + private func statusesAreFeatured(statusIds: [Int64], on context: ExecutionContext) async throws -> [Int64] { + guard let authorizationPayloadId = context.userId else { return [] } - let featuredStatuses = try await FeaturedStatus.query(on: request.db) + let featuredStatuses = try await FeaturedStatus.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$status.$id ~~ statusIds) .field(\.$status.$id) @@ -1366,11 +1366,11 @@ final class StatusesService: StatusesServiceType { return featuredStatuses.map({ $0.$status.id }) } - private func getMentionedUsers(for status: Status, on context: QueueContext) async throws -> [Int64] { + private func getMentionedUsers(for status: Status, on context: ExecutionContext) async throws -> [Int64] { var userIds: [Int64] = [] for mention in status.mentions { - let user = try await User.query(on: context.application.db) + let user = try await User.query(on: context.db) .group(.or) { group in group .filter(\.$userNameNormalized == mention.userNameNormalized) @@ -1408,13 +1408,13 @@ final class StatusesService: StatusesServiceType { return categoryHashtag?.category } - private func saveAttachment(attachment: MediaAttachmentDto, userId: Int64, on context: QueueContext) async throws -> Attachment? { + private func saveAttachment(attachment: MediaAttachmentDto, userId: Int64, on context: ExecutionContext) async throws -> Attachment? { guard attachment.mediaType.starts(with: "image/") else { return nil } - let temporaryFileService = context.application.services.temporaryFileService - let storageService = context.application.services.storageService + let temporaryFileService = context.services.temporaryFileService + let storageService = context.services.storageService // Save image to temp folder. context.logger.info("Saving attachment '\(attachment.url)' to temporary folder.") @@ -1437,7 +1437,7 @@ final class StatusesService: StatusesServiceType { // Save resized image in temp folder. context.logger.info("Saving resized image '\(fileName)' in temporary folder.") - let tmpSmallFileUrl = try temporaryFileService.temporaryPath(on: context.application, based: fileName) + let tmpSmallFileUrl = try temporaryFileService.temporaryPath(based: fileName, on: context) resized.write(to: tmpSmallFileUrl, quality: Constants.imageQuality) // Save original image. @@ -1521,13 +1521,13 @@ final class StatusesService: StatusesServiceType { return attachmentEntity } - private func downloadHdrOriginalImage(attachment: MediaAttachmentDto, on context: QueueContext) async throws -> String? { + private func downloadHdrOriginalImage(attachment: MediaAttachmentDto, on context: ExecutionContext) async throws -> String? { guard let hdrImageUrl = attachment.hdrImageUrl else { return nil } - let temporaryFileService = context.application.services.temporaryFileService - let storageService = context.application.services.storageService + let temporaryFileService = context.services.temporaryFileService + let storageService = context.services.storageService context.logger.info("Saving attachment HDR image '\(hdrImageUrl)' to temporary folder.") let tmpOriginalHdrFileUrl = try await temporaryFileService.save(url: hdrImageUrl, on: context) diff --git a/Sources/VernissageServer/Services/StorageService.swift b/Sources/VernissageServer/Services/StorageService.swift index 5be05906..641639a5 100644 --- a/Sources/VernissageServer/Services/StorageService.swift +++ b/Sources/VernissageServer/Services/StorageService.swift @@ -35,18 +35,12 @@ extension Application.Services { @_documentation(visibility: private) protocol StorageServiceType: Sendable { - func getBaseStoragePath(on application: Application) -> String - func get(fileName: String, on request: Request) async throws -> ByteBuffer - - func save(fileName: String, byteBuffer: ByteBuffer, on request: Request) async throws -> String? - func save(fileName: String, url: URL, on request: Request) async throws -> String? - func save(fileName: String, url: URL, on context: QueueContext) async throws -> String? - - func dowload(url: String, on request: Request) async throws -> String? - func dowload(url: String, on context: QueueContext) async throws -> String? - - func delete(fileName: String, on request: Request) async throws - func delete(fileName: String, on context: QueueContext) async throws + func getBaseStoragePath(on context: ExecutionContext) -> String + func get(fileName: String, on context: ExecutionContext) async throws -> ByteBuffer + func save(fileName: String, byteBuffer: ByteBuffer, on context: ExecutionContext) async throws -> String? + func save(fileName: String, url: URL, on context: ExecutionContext) async throws -> String? + func dowload(url: String, on context: ExecutionContext) async throws -> String? + func delete(fileName: String, on context: ExecutionContext) async throws } /// A service for managing resource files in the system. @@ -80,103 +74,82 @@ extension StorageServiceType { /// Service responsible for saving file in local file storage. fileprivate final class LocalFileStorageService: StorageServiceType { - func get(fileName: String, on request: Request) async throws -> ByteBuffer { - return try await self.getFileFromLocalFileSystem(fileName: fileName, on: request) + func get(fileName: String, on context: ExecutionContext) async throws -> ByteBuffer { + return try await self.getFileFromLocalFileSystem(fileName: fileName, on: context) } - func dowload(url: String, on request: Request) async throws -> String? { - let byteBuffer = try await downloadRemoteResources(url: url, on: request.client) - return try await self.saveFileToLocalFileSystem(byteBuffer: byteBuffer, fileUri: url, on: request) - } - - func dowload(url: String, on context: QueueContext) async throws -> String? { - let byteBuffer = try await downloadRemoteResources(url: url, on: context.application.client) + func dowload(url: String, on context: ExecutionContext) async throws -> String? { + let byteBuffer = try await downloadRemoteResources(url: url, on: context.client) return try await self.saveFileToLocalFileSystem(byteBuffer: byteBuffer, fileUri: url, on: context) } - - func save(fileName: String, byteBuffer: ByteBuffer, on request: Request) async throws -> String? { - return try await self.saveFileToLocalFileSystem(byteBuffer: byteBuffer, fileUri: fileName, on: request) - } - - func save(fileName: String, url: URL, on request: Request) async throws -> String? { - return try await self.saveFileToLocalFileSystem(url: url, fileUri: fileName, on: request) + + func save(fileName: String, byteBuffer: ByteBuffer, on context: ExecutionContext) async throws -> String? { + return try await self.saveFileToLocalFileSystem(byteBuffer: byteBuffer, fileUri: fileName, on: context) } - func save(fileName: String, url: URL, on context: QueueContext) async throws -> String? { + func save(fileName: String, url: URL, on context: ExecutionContext) async throws -> String? { return try await self.saveFileToLocalFileSystem(url: url, fileUri: fileName, on: context) } - func delete(fileName: String, on request: Request) async throws { - try await self.deleteFileFromFileSystem(fileName: fileName, on: request) - } - - func delete(fileName: String, on context: QueueContext) async throws { + func delete(fileName: String, on context: ExecutionContext) async throws { try await self.deleteFileFromFileSystem(fileName: fileName, on: context) } - func getBaseStoragePath(on application: Application) -> String { - let appplicationSettings = application.settings.cached + func getBaseStoragePath(on context: ExecutionContext) -> String { + let appplicationSettings = context.application.settings.cached return (appplicationSettings?.baseAddress ?? "").finished(with: "/") + "storage" } - private func getFileFromLocalFileSystem(fileName: String, on request: Request) async throws -> ByteBuffer { - let publicFolderPath = request.application.directory.publicDirectory + private func getFileFromLocalFileSystem(fileName: String, on context: ExecutionContext) async throws -> ByteBuffer { + let publicFolderPath = context.application.directory.publicDirectory let path = publicFolderPath.finished(with: "/") + "storage/" + fileName - return try await request.fileio.collectFile(at: path) - } - - private func saveFileToLocalFileSystem(byteBuffer: ByteBuffer, fileUri: String, on request: Request) async throws -> String { - let publicFolderPath = request.application.directory.publicDirectory - let fileName = self.generateFileName(url: fileUri) - let path = publicFolderPath.finished(with: "/") + "storage/" + fileName + // First we can try to download via request file IO. + if let fileConent = try await context.fileio?.collectFile(at: path) { + return fileConent + } - try await request.fileio.writeFile(byteBuffer, at: path) - return fileName + // If we are not in the request context then we can try to download via application file IO. + return try await context.application.fileio.collectFile(at: path, allocator: ByteBufferAllocator(), eventLoop: context.eventLoop) } - private func saveFileToLocalFileSystem(byteBuffer: ByteBuffer, fileUri: String, on context: QueueContext) async throws -> String { + private func saveFileToLocalFileSystem(byteBuffer: ByteBuffer, fileUri: String, on context: ExecutionContext) async throws -> String { let publicFolderPath = context.application.directory.publicDirectory let fileName = self.generateFileName(url: fileUri) let path = publicFolderPath.finished(with: "/") + "storage/" + fileName - try await context.application.fileio.writeFile(byteBuffer, at: path, eventLoop: context.eventLoop) - return fileName - } - - private func saveFileToLocalFileSystem(url: URL, fileUri: String, on request: Request) async throws -> String { - let publicFolderPath = request.application.directory.publicDirectory - let fileName = self.generateFileName(url: fileUri) - let path = publicFolderPath.finished(with: "/") + "storage/" + fileName - - let byteBuffer = try await request.fileio.collectFile(at: url.absoluteString) - try await request.fileio.writeFile(byteBuffer, at: path) + if let fileio = context.fileio { + try await fileio.writeFile(byteBuffer, at: path) + } else { + try await context.application.fileio.writeFile(byteBuffer, at: path, eventLoop: context.eventLoop) + } return fileName } - - private func saveFileToLocalFileSystem(url: URL, fileUri: String, on context: QueueContext) async throws -> String { + + private func saveFileToLocalFileSystem(url: URL, fileUri: String, on context: ExecutionContext) async throws -> String { let publicFolderPath = context.application.directory.publicDirectory let fileName = self.generateFileName(url: fileUri) let path = publicFolderPath.finished(with: "/") + "storage/" + fileName - let byteBuffer = try await context.application.fileio.collectFile(at: url.absoluteString, - allocator: context.application.allocator, - eventLoop: context.eventLoop) - try await context.application.fileio.writeFile(byteBuffer, at: path, eventLoop: context.eventLoop) - + // Read the file. + let byteBuffer = if let fileio = context.fileio { + try await fileio.collectFile(at: url.absoluteString) + } else { + try await context.application.fileio.collectFile(at: path, allocator: ByteBufferAllocator(), eventLoop: context.eventLoop) + } + + // Write file. + if let fileio = context.fileio { + try await fileio.writeFile(byteBuffer, at: path) + } else { + try await context.application.fileio.writeFile(byteBuffer, at: path, eventLoop: context.eventLoop) + } + return fileName } - - private func deleteFileFromFileSystem(fileName: String, on request: Request) async throws { - let publicFolderPath = request.application.directory.publicDirectory - let path = publicFolderPath.finished(with: "/") + "storage/" + fileName - // Remove file from storage. - try await request.application.fileio.remove(path: path, eventLoop: request.eventLoop).get() - } - - private func deleteFileFromFileSystem(fileName: String, on context: QueueContext) async throws { + private func deleteFileFromFileSystem(fileName: String, on context: ExecutionContext) async throws { let publicFolderPath = context.application.directory.publicDirectory let path = publicFolderPath.finished(with: "/") + "storage/" + fileName @@ -187,61 +160,45 @@ fileprivate final class LocalFileStorageService: StorageServiceType { /// Service responsible for saving files in S3 compatible object storage. fileprivate final class S3StorageService: StorageServiceType { - func get(fileName: String, on request: Request) async throws -> ByteBuffer { - return try await self.getFileFromObjectStorage(fileName: fileName, on: request.application) - } - - func dowload(url: String, on request: Request) async throws -> String? { - let byteBuffer = try await downloadRemoteResources(url: url, on: request.client) - return try await self.saveFileToObjectStorage(byteBuffer: byteBuffer, fileUri: url, on: request.application) + func get(fileName: String, on context: ExecutionContext) async throws -> ByteBuffer { + return try await self.getFileFromObjectStorage(fileName: fileName, on: context) } - func dowload(url: String, on context: QueueContext) async throws -> String? { - let byteBuffer = try await downloadRemoteResources(url: url, on: context.application.client) - return try await self.saveFileToObjectStorage(byteBuffer: byteBuffer, fileUri: url, on: context.application) + func dowload(url: String, on context: ExecutionContext) async throws -> String? { + let byteBuffer = try await downloadRemoteResources(url: url, on: context.client) + return try await self.saveFileToObjectStorage(byteBuffer: byteBuffer, fileUri: url, on: context) } - - func save(fileName: String, byteBuffer: ByteBuffer, on request: Request) async throws -> String? { - return try await self.saveFileToObjectStorage(byteBuffer: byteBuffer, fileUri: fileName, on: request.application) - } - - func save(fileName: String, url: URL, on request: Request) async throws -> String? { - return try await self.saveFileToObjectStorage(url: url, fileUri: fileName, on: request) + + func save(fileName: String, byteBuffer: ByteBuffer, on context: ExecutionContext) async throws -> String? { + return try await self.saveFileToObjectStorage(byteBuffer: byteBuffer, fileUri: fileName, on: context) } - func save(fileName: String, url: URL, on context: QueueContext) async throws -> String? { + func save(fileName: String, url: URL, on context: ExecutionContext) async throws -> String? { return try await self.saveFileToObjectStorage(url: url, fileUri: fileName, on: context) } - - func delete(fileName: String, on request: Request) async throws { - try await self.deleteFileFromObjectStorage(fileName: fileName, on: request) - } - func delete(fileName: String, on context: QueueContext) async throws { + func delete(fileName: String, on context: ExecutionContext) async throws { try await self.deleteFileFromObjectStorage(fileName: fileName, on: context) } - func getBaseStoragePath(on application: Application) -> String { - let s3Address = application.settings.cached?.s3Address ?? "" - let s3Bucket = application.settings.cached?.s3Bucket ?? "" + func getBaseStoragePath(on context: ExecutionContext) -> String { + let s3Address = context.application.settings.cached?.s3Address ?? "" + let s3Bucket = context.application.settings.cached?.s3Bucket ?? "" return "\(s3Address)/\(s3Bucket)" } - private func getFileFromObjectStorage(fileName: String, on application: Application) async throws -> ByteBuffer { - guard let s3 = application.objectStorage.s3 else { - application.logger.warning("File cannot be stored. S3 object storage is not configured!") + private func getFileFromObjectStorage(fileName: String, on context: ExecutionContext) async throws -> ByteBuffer { + guard let s3 = context.application.objectStorage.s3 else { + context.logger.warning("File cannot be stored. S3 object storage is not configured!") throw StorageError.s3StorageNotConfigured } - guard let bucket = application.settings.cached?.s3Bucket else { - application.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") + guard let bucket = context.settings.cached?.s3Bucket else { + context.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") throw StorageError.s3StorageNotConfigured } - - // let baseStoragePath = self.getBaseStoragePath(on: application) - // let fileUrl = baseStoragePath.finished(with: "/") + fileName - + let getObjectRequest = S3.GetObjectRequest( bucket: bucket, key: fileName @@ -251,14 +208,14 @@ fileprivate final class S3StorageService: StorageServiceType { return try await result.body.collect(upTo: 10_000_000) } - private func saveFileToObjectStorage(byteBuffer: ByteBuffer, fileUri: String, on application: Application) async throws -> String { - guard let s3 = application.objectStorage.s3 else { - application.logger.warning("File cannot be stored. S3 object storage is not configured!") + private func saveFileToObjectStorage(byteBuffer: ByteBuffer, fileUri: String, on context: ExecutionContext) async throws -> String { + guard let s3 = context.application.objectStorage.s3 else { + context.logger.warning("File cannot be stored. S3 object storage is not configured!") throw StorageError.s3StorageNotConfigured } - guard let bucket = application.settings.cached?.s3Bucket else { - application.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") + guard let bucket = context.settings.cached?.s3Bucket else { + context.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") throw StorageError.s3StorageNotConfigured } @@ -277,36 +234,8 @@ fileprivate final class S3StorageService: StorageServiceType { _ = try await s3.with(timeout: .seconds(60)).putObject(putObjectRequest) return fileName } - - private func saveFileToObjectStorage(url: URL, fileUri: String, on request: Request) async throws -> String { - guard let s3 = request.objectStorage.s3 else { - request.logger.warning("File cannot be stored. S3 object storage is not configured!") - throw StorageError.s3StorageNotConfigured - } - guard let bucket = request.application.settings.cached?.s3Bucket else { - request.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") - throw StorageError.s3StorageNotConfigured - } - - let byteBuffer = try await request.fileio.collectFile(at: url.absoluteString) - let fileName = self.generateFileName(url: fileUri) - let contentType = fileUri.mimeType - - let putObjectRequest = S3.PutObjectRequest( - acl: .publicRead, - body: .init(buffer: byteBuffer), - bucket: bucket, - cacheControl: MaxAge.year.rawValue, - contentType: contentType, - key: fileName - ) - - _ = try await s3.with(timeout: .seconds(60)).putObject(putObjectRequest) - return fileName - } - - private func saveFileToObjectStorage(url: URL, fileUri: String, on context: QueueContext) async throws -> String { + private func saveFileToObjectStorage(url: URL, fileUri: String, on context: ExecutionContext) async throws -> String { guard let s3 = context.application.objectStorage.s3 else { context.logger.warning("File cannot be stored. S3 object storage is not configured!") throw StorageError.s3StorageNotConfigured @@ -316,10 +245,14 @@ fileprivate final class S3StorageService: StorageServiceType { context.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") throw StorageError.s3StorageNotConfigured } - - let byteBuffer = try await context.application.fileio.collectFile(at: url.absoluteString, - allocator: context.application.allocator, - eventLoop: context.eventLoop) + + let byteBuffer = if let fileio = context.fileio { + try await fileio.collectFile(at: url.absoluteString) + } else { + try await context.application.fileio.collectFile(at: url.absoluteString, + allocator: context.application.allocator, + eventLoop: context.eventLoop) + } let fileName = self.generateFileName(url: fileUri) let contentType = fileUri.mimeType @@ -336,23 +269,8 @@ fileprivate final class S3StorageService: StorageServiceType { _ = try await s3.with(timeout: .seconds(60)).putObject(putObjectRequest) return fileName } - - private func deleteFileFromObjectStorage(fileName: String, on request: Request) async throws { - guard let s3 = request.objectStorage.s3 else { - request.logger.warning("File cannot be stored. S3 object storage is not configured!") - throw StorageError.s3StorageNotConfigured - } - - guard let bucket = request.application.settings.cached?.s3Bucket else { - request.logger.warning("File cannot be stored. S3 object storage bucket is not configured!") - throw StorageError.s3StorageNotConfigured - } - - let deleteObjectRequest = S3.DeleteObjectRequest(bucket: bucket, key: fileName) - _ = try await s3.deleteObject(deleteObjectRequest) - } - - private func deleteFileFromObjectStorage(fileName: String, on context: QueueContext) async throws { + + private func deleteFileFromObjectStorage(fileName: String, on context: ExecutionContext) async throws { guard let s3 = context.application.objectStorage.s3 else { context.logger.warning("File cannot be stored. S3 object storage is not configured!") throw StorageError.s3StorageNotConfigured diff --git a/Sources/VernissageServer/Services/TemporaryFileService.swift b/Sources/VernissageServer/Services/TemporaryFileService.swift index 59635f6f..3eee0513 100644 --- a/Sources/VernissageServer/Services/TemporaryFileService.swift +++ b/Sources/VernissageServer/Services/TemporaryFileService.swift @@ -25,34 +25,45 @@ extension Application.Services { @_documentation(visibility: private) protocol TemporaryFileServiceType: Sendable { - func temporaryPath(on application: Application, based fileName: String) throws -> URL - func save(fileName: String, byteBuffer: ByteBuffer, on request: Request) async throws -> URL - func save(url: String, on context: QueueContext) async throws -> URL + func temporaryPath(based fileName: String, on context: ExecutionContext) throws -> URL + func save(fileName: String, byteBuffer: ByteBuffer, on context: ExecutionContext) async throws -> URL + func save(url: String, on context: ExecutionContext) async throws -> URL func delete(url: URL, on request: Request) async throws } /// A service for managing temporary files in the system. final class TemporaryFileService: TemporaryFileServiceType { - func save(fileName: String, byteBuffer: ByteBuffer, on request: Request) async throws -> URL { - let temporaryPath = try self.temporaryPath(on: request.application, based: fileName) - try await request.fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString) + func save(fileName: String, byteBuffer: ByteBuffer, on context: ExecutionContext) async throws -> URL { + let temporaryPath = try self.temporaryPath(based: fileName, on: context) + + if let fileio = context.fileio { + try await fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString) + } else { + try await context.application.fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString, eventLoop: context.eventLoop) + } + return temporaryPath } - func save(url: String, on context: QueueContext) async throws -> URL { + func save(url: String, on context: ExecutionContext) async throws -> URL { let fileName = url.fileName - let temporaryPath = try self.temporaryPath(on: context.application, based: fileName) + let temporaryPath = try self.temporaryPath(based: fileName, on: context) // Download file. - let byteBuffer = try await self.downloadRemoteResources(url: url, on: context.application.client) + let byteBuffer = try await self.downloadRemoteResources(url: url, on: context.client) // Save in tmp directory. - try await context.application.fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString, eventLoop: context.eventLoop) + if let fileio = context.fileio { + try await fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString) + } else { + try await context.application.fileio.writeFile(byteBuffer, at: temporaryPath.absoluteString, eventLoop: context.eventLoop) + } + return temporaryPath } - func temporaryPath(on application: Application, based fileName: String) throws -> URL { - let path = application.directory.tempDirectory + func temporaryPath(based fileName: String, on context: ExecutionContext) throws -> URL { + let path = context.application.directory.tempDirectory + String.createRandomString(length: 12) + "-" + fileName.replacingOccurrences(of: " ", with: "+") diff --git a/Sources/VernissageServer/Services/TimelineService.swift b/Sources/VernissageServer/Services/TimelineService.swift index 9434b0f8..2c56862a 100644 --- a/Sources/VernissageServer/Services/TimelineService.swift +++ b/Sources/VernissageServer/Services/TimelineService.swift @@ -24,19 +24,19 @@ extension Application.Services { @_documentation(visibility: private) protocol TimelineServiceType: Sendable { - func home(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult - func bookmarks(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult - func favourites(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult - func `public`(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool) async throws -> [Status] - func category(on database: Database, linkableParams: LinkableParams, categoryId: Int64, onlyLocal: Bool) async throws -> [Status] - func hashtags(on database: Database, linkableParams: LinkableParams, hashtag: String, onlyLocal: Bool) async throws -> [Status] - func featuredStatuses(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool) async throws -> LinkableResult - func featuredUsers(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool) async throws -> LinkableResult + func home(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult + func bookmarks(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult + func favourites(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult + func `public`(linkableParams: LinkableParams, onlyLocal: Bool, on database: Database) async throws -> [Status] + func category(linkableParams: LinkableParams, categoryId: Int64, onlyLocal: Bool, on database: Database) async throws -> [Status] + func hashtags(linkableParams: LinkableParams, hashtag: String, onlyLocal: Bool, on database: Database) async throws -> [Status] + func featuredStatuses(linkableParams: LinkableParams, onlyLocal: Bool, on database: Database) async throws -> LinkableResult + func featuredUsers(linkableParams: LinkableParams, onlyLocal: Bool, on database: Database) async throws -> LinkableResult } /// A service for managing main timelines. final class TimelineService: TimelineServiceType { - func home(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { + func home(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult { var query = UserStatus.query(on: database) .filter(\.$user.$id == userId) @@ -88,7 +88,7 @@ final class TimelineService: TimelineServiceType { ) } - func bookmarks(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { + func bookmarks(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult { var query = StatusBookmark.query(on: database) .filter(\.$user.$id == userId) @@ -140,7 +140,7 @@ final class TimelineService: TimelineServiceType { ) } - func favourites(on database: Database, for userId: Int64, linkableParams: LinkableParams) async throws -> LinkableResult { + func favourites(for userId: Int64, linkableParams: LinkableParams, on database: Database) async throws -> LinkableResult { var query = StatusFavourite.query(on: database) .filter(\.$user.$id == userId) @@ -192,7 +192,7 @@ final class TimelineService: TimelineServiceType { ) } - func `public`(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool = false) async throws -> [Status] { + func `public`(linkableParams: LinkableParams, onlyLocal: Bool = false, on database: Database) async throws -> [Status] { var query = Status.query(on: database) .filter(\.$visibility == .public) @@ -243,7 +243,7 @@ final class TimelineService: TimelineServiceType { return statuses.sorted(by: { $0.id ?? 0 > $1.id ?? 0 }) } - func category(on database: Database, linkableParams: LinkableParams, categoryId: Int64, onlyLocal: Bool = false) async throws -> [Status] { + func category(linkableParams: LinkableParams, categoryId: Int64, onlyLocal: Bool = false, on database: Database) async throws -> [Status] { var query = Status.query(on: database) .filter(\.$visibility == .public) @@ -295,7 +295,7 @@ final class TimelineService: TimelineServiceType { return statuses.sorted(by: { $0.id ?? 0 > $1.id ?? 0 }) } - func hashtags(on database: Database, linkableParams: LinkableParams, hashtag: String, onlyLocal: Bool = false) async throws -> [Status] { + func hashtags(linkableParams: LinkableParams, hashtag: String, onlyLocal: Bool = false, on database: Database) async throws -> [Status] { var query = Status.query(on: database) .join(StatusHashtag.self, on: \Status.$id == \StatusHashtag.$status.$id) @@ -348,7 +348,7 @@ final class TimelineService: TimelineServiceType { return statuses.sorted(by: { $0.id ?? 0 > $1.id ?? 0 }) } - func featuredStatuses(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool = false) async throws -> LinkableResult { + func featuredStatuses(linkableParams: LinkableParams, onlyLocal: Bool = false, on database: Database) async throws -> LinkableResult { var query = FeaturedStatus.query(on: database) .filter(\.$createdAt > Date.yearAgo) .with(\.$status) { status in @@ -399,7 +399,7 @@ final class TimelineService: TimelineServiceType { ) } - func featuredUsers(on database: Database, linkableParams: LinkableParams, onlyLocal: Bool = false) async throws -> LinkableResult { + func featuredUsers(linkableParams: LinkableParams, onlyLocal: Bool = false, on database: Database) async throws -> LinkableResult { var query = FeaturedUser.query(on: database) .filter(\.$createdAt > Date.yearAgo) .with(\.$featuredUser) { featuredUser in diff --git a/Sources/VernissageServer/Services/TokensService.swift b/Sources/VernissageServer/Services/TokensService.swift index a23fad21..bd8cf683 100644 --- a/Sources/VernissageServer/Services/TokensService.swift +++ b/Sources/VernissageServer/Services/TokensService.swift @@ -24,11 +24,11 @@ extension Application.Services { @_documentation(visibility: private) protocol TokensServiceType: Sendable { - func createAccessTokens(on request: Request, forUser user: User, useCookies: Bool?) async throws -> AccessTokens - func updateAccessTokens(on request: Request, forUser user: User, refreshToken: RefreshToken, regenerateRefreshToken: Bool?, useCookies: Bool?) async throws -> AccessTokens - func validateRefreshToken(on request: Request, refreshToken: String) async throws -> RefreshToken - func getUserByRefreshToken(on request: Request, refreshToken: String) async throws -> User - func revokeRefreshTokens(on request: Request, forUser user: User) async throws + func createAccessTokens(forUser user: User, useCookies: Bool?, on request: Request) async throws -> AccessTokens + func updateAccessTokens(forUser user: User, refreshToken: RefreshToken, regenerateRefreshToken: Bool?, useCookies: Bool?, on request: Request) async throws -> AccessTokens + func validateRefreshToken(refreshToken: String, on request: Request) async throws -> RefreshToken + func getUserByRefreshToken(refreshToken: String, on request: Request) async throws -> User + func revokeRefreshTokens(forUser user: User, on request: Request) async throws } /// A service for managing authorization tokens. @@ -37,7 +37,7 @@ final class TokensService: TokensServiceType { private let refreshTokenTime: TimeInterval = 30 * 24 * 60 * 60 // 30 days private let accessTokenTime: TimeInterval = 60 * 60 // 1 hour - public func validateRefreshToken(on request: Request, refreshToken: String) async throws -> RefreshToken { + public func validateRefreshToken(refreshToken: String, on request: Request) async throws -> RefreshToken { let refreshTokenFromDb = try await RefreshToken.query(on: request.db).filter(\.$token == refreshToken).first() guard let refreshToken = refreshTokenFromDb else { @@ -56,14 +56,14 @@ final class TokensService: TokensServiceType { return refreshToken } - public func createAccessTokens(on request: Request, forUser user: User, useCookies: Bool? = false) async throws -> AccessTokens { + public func createAccessTokens(forUser user: User, useCookies: Bool? = false, on request: Request) async throws -> AccessTokens { let accessTokenExpirationDate = Date().addingTimeInterval(TimeInterval(self.accessTokenTime)) let refreshTokenExpirationDate = Date().addingTimeInterval(self.refreshTokenTime) let xsrfToken = self.createXsrfToken() - let userPayload = try await self.createAuthenticationPayload(request: request, forUser: user, with: accessTokenExpirationDate) - let accessToken = try await self.createAccessToken(on: request, forUser: userPayload, with: accessTokenExpirationDate) - let refreshToken = try await self.createRefreshToken(on: request, forUser: user, with: refreshTokenExpirationDate) + let userPayload = try await self.createAuthenticationPayload(forUser: user, with: accessTokenExpirationDate, on: request) + let accessToken = try await self.createAccessToken(forUser: userPayload, with: accessTokenExpirationDate, on: request) + let refreshToken = try await self.createRefreshToken(forUser: user, with: refreshTokenExpirationDate, on: request) return AccessTokens(accessToken: accessToken, refreshToken: refreshToken, @@ -74,21 +74,21 @@ final class TokensService: TokensServiceType { useCookies: useCookies == true) } - public func updateAccessTokens(on request: Request, - forUser user: User, + public func updateAccessTokens(forUser user: User, refreshToken: RefreshToken, regenerateRefreshToken: Bool? = true, - useCookies: Bool? = false) async throws -> AccessTokens { + useCookies: Bool? = false, + on request: Request) async throws -> AccessTokens { let accessTokenExpirationDate = Date().addingTimeInterval(TimeInterval(self.accessTokenTime)) let refreshTokenExpirationDate = Date().addingTimeInterval(self.refreshTokenTime) let xsrfToken = self.createXsrfToken() - let userPayload = try await self.createAuthenticationPayload(request: request, forUser: user, with: accessTokenExpirationDate) - let accessToken = try await self.createAccessToken(on: request, forUser: userPayload, with: accessTokenExpirationDate) - let refreshToken = try await self.updateRefreshToken(on: request, - forToken: refreshToken, + let userPayload = try await self.createAuthenticationPayload(forUser: user, with: accessTokenExpirationDate, on: request) + let accessToken = try await self.createAccessToken(forUser: userPayload, with: accessTokenExpirationDate, on: request) + let refreshToken = try await self.updateRefreshToken(forToken: refreshToken, with: refreshTokenExpirationDate, - regenerate: regenerateRefreshToken == true) + regenerate: regenerateRefreshToken == true, + on: request) return AccessTokens(accessToken: accessToken, refreshToken: refreshToken, @@ -99,7 +99,7 @@ final class TokensService: TokensServiceType { useCookies: useCookies == true) } - public func getUserByRefreshToken(on request: Request, refreshToken: String) async throws -> User { + public func getUserByRefreshToken(refreshToken: String, on request: Request) async throws -> User { let refreshTokenFromDb = try await RefreshToken.query(on: request.db).with(\.$user).filter(\.$token == refreshToken).first() guard let refreshToken = refreshTokenFromDb else { @@ -113,12 +113,12 @@ final class TokensService: TokensServiceType { return refreshToken.user } - private func createAccessToken(on request: Request, forUser authorizationPayload: UserPayload, with expirationDate: Date) async throws -> String { + private func createAccessToken(forUser authorizationPayload: UserPayload, with expirationDate: Date, on request: Request) async throws -> String { let accessToken = try request.jwt.sign(authorizationPayload) return accessToken } - private func createRefreshToken(on request: Request, forUser user: User, with expirationDate: Date) async throws -> String { + private func createRefreshToken(forUser user: User, with expirationDate: Date, on request: Request) async throws -> String { guard let userId = user.id else { throw RefreshTokenError.userIdNotSpecified } @@ -135,7 +135,7 @@ final class TokensService: TokensServiceType { return String.createRandomString(length: 64) } - private func updateRefreshToken(on request: Request, forToken refreshToken: RefreshToken, with expirationDate: Date, regenerate: Bool) async throws -> String { + private func updateRefreshToken(forToken refreshToken: RefreshToken, with expirationDate: Date, regenerate: Bool, on request: Request) async throws -> String { refreshToken.token = regenerate ? String.createRandomString(length: 40) : refreshToken.token refreshToken.expiryDate = expirationDate @@ -143,7 +143,7 @@ final class TokensService: TokensServiceType { return refreshToken.token } - public func revokeRefreshTokens(on request: Request, forUser user: User) async throws { + public func revokeRefreshTokens(forUser user: User, on request: Request) async throws { let refreshTokens = try await RefreshToken.query(on: request.db).filter(\.$user.$id == user.id!).all() try await withThrowingTaskGroup(of: Void.self) { _ in @@ -154,14 +154,14 @@ final class TokensService: TokensServiceType { } } - private func createAuthenticationPayload(request: Request, forUser user: User, with expirationDate: Date) async throws -> UserPayload { + private func createAuthenticationPayload(forUser user: User, with expirationDate: Date, on request: Request) async throws -> UserPayload { guard let userId = user.id else { throw Abort(.unauthorized) } let userFromDb = try await User.query(on: request.db).with(\.$roles).filter(\.$id == userId).first() - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) + let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.executionContext) let avatarUrl = self.getAvatarUrl(user: user, baseStoragePath: baseStoragePath) let headerUrl = self.getHeaderUrl(user: user, baseStoragePath: baseStoragePath) diff --git a/Sources/VernissageServer/Services/TrendingService.swift b/Sources/VernissageServer/Services/TrendingService.swift index 168b1d2f..fa6f3a92 100644 --- a/Sources/VernissageServer/Services/TrendingService.swift +++ b/Sources/VernissageServer/Services/TrendingService.swift @@ -29,9 +29,9 @@ protocol TrendingServiceType: Sendable { func calculateTrendingStatuses(on context: QueueContext) async func calculateTrendingUsers(on context: QueueContext) async func calculateTrendingHashtags(on context: QueueContext) async - func statuses(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult - func users(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult - func hashtags(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult + func statuses(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult + func users(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult + func hashtags(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult } /// A service for managing the most popular entities. @@ -179,7 +179,7 @@ final class TrendingService: TrendingServiceType { } } - func statuses(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult { + func statuses(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult { var query = TrendingStatus.query(on: database) .filter(\.$trendingPeriod == period) @@ -231,7 +231,7 @@ final class TrendingService: TrendingServiceType { ) } - func users(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult { + func users(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult { var query = TrendingUser.query(on: database) .filter(\.$trendingPeriod == period) @@ -273,7 +273,7 @@ final class TrendingService: TrendingServiceType { ) } - func hashtags(on database: Database, linkableParams: LinkableParams, period: TrendingPeriod) async throws -> LinkableResult { + func hashtags(linkableParams: LinkableParams, period: TrendingPeriod, on database: Database) async throws -> LinkableResult { var query = TrendingHashtag.query(on: database) .filter(\.$trendingPeriod == period) diff --git a/Sources/VernissageServer/Services/UserBlockedDomainsService.swift b/Sources/VernissageServer/Services/UserBlockedDomainsService.swift index d2f97dec..765fa2c9 100644 --- a/Sources/VernissageServer/Services/UserBlockedDomainsService.swift +++ b/Sources/VernissageServer/Services/UserBlockedDomainsService.swift @@ -25,12 +25,12 @@ extension Application.Services { @_documentation(visibility: private) protocol UserBlockedDomainsServiceType: Sendable { - func exists(on database: Database, url: URL) async throws -> Bool + func exists(url: URL, on database: Database) async throws -> Bool } /// A service for managing domains blocked by the user. final class UserBlockedDomainsService: UserBlockedDomainsServiceType { - public func exists(on database: Database, url: URL) async throws -> Bool { + public func exists(url: URL, on database: Database) async throws -> Bool { guard let host = url.host?.lowercased() else { return false } diff --git a/Sources/VernissageServer/Services/UserMutesService.swift b/Sources/VernissageServer/Services/UserMutesService.swift index 00f87cad..94b37da2 100644 --- a/Sources/VernissageServer/Services/UserMutesService.swift +++ b/Sources/VernissageServer/Services/UserMutesService.swift @@ -24,14 +24,14 @@ extension Application.Services { @_documentation(visibility: private) protocol UserMutesServiceType: Sendable { - func mute(on request: Request, userId: Int64, mutedUserId: Int64, muteStatuses: Bool, muteReblogs: Bool, muteNotifications: Bool, muteEnd: Date?) async throws -> UserMute - func unmute(on request: Request, userId: Int64, mutedUserId: Int64) async throws + func mute(userId: Int64, mutedUserId: Int64, muteStatuses: Bool, muteReblogs: Bool, muteNotifications: Bool, muteEnd: Date?, on request: Request) async throws -> UserMute + func unmute(userId: Int64, mutedUserId: Int64, on request: Request) async throws } /// A service for managing user mutes. final class UserMutesService: UserMutesServiceType { - func mute(on request: Request, userId: Int64, mutedUserId: Int64, muteStatuses: Bool, muteReblogs: Bool, muteNotifications: Bool, muteEnd: Date? = nil) async throws -> UserMute { + func mute(userId: Int64, mutedUserId: Int64, muteStatuses: Bool, muteReblogs: Bool, muteNotifications: Bool, muteEnd: Date? = nil, on request: Request) async throws -> UserMute { if let userMute = try await UserMute.query(on: request.db) .filter(\.$user.$id == userId) .filter(\.$mutedUser.$id == mutedUserId) @@ -61,7 +61,7 @@ final class UserMutesService: UserMutesServiceType { } } - func unmute(on request: Request, userId: Int64, mutedUserId: Int64) async throws { + func unmute(userId: Int64, mutedUserId: Int64, on request: Request) async throws { guard let userMute = try await UserMute.query(on: request.db) .filter(\.$user.$id == userId) .filter(\.$mutedUser.$id == mutedUserId) diff --git a/Sources/VernissageServer/Services/UsersService.swift b/Sources/VernissageServer/Services/UsersService.swift index e7f1bd84..d4555f35 100644 --- a/Sources/VernissageServer/Services/UsersService.swift +++ b/Sources/VernissageServer/Services/UsersService.swift @@ -28,35 +28,35 @@ extension Application.Services { @_documentation(visibility: private) protocol UsersServiceType: Sendable { - func count(on database: Database, sinceLastLoginDate: Date?) async throws -> Int - func get(on database: Database, id: Int64) async throws -> User? - func get(on database: Database, userName: String) async throws -> User? - func get(on database: Database, account: String) async throws -> User? - func get(on database: Database, activityPubProfile: String) async throws -> User? + func count(sinceLastLoginDate: Date?, on database: Database) async throws -> Int + func get(id: Int64, on database: Database) async throws -> User? + func get(userName: String, on database: Database) async throws -> User? + func get(account: String, on database: Database) async throws -> User? + func get(activityPubProfile: String, on database: Database) async throws -> User? func getModerators(on database: Database) async throws -> [User] func getDefaultSystemUser(on database: Database) async throws -> User? - func convertToDto(on request: Request, user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool) async -> UserDto - func convertToDtos(on request: Request, users: [User], attachSensitive: Bool) async -> [UserDto] - func login(on request: Request, userNameOrEmail: String, password: String, isMachineTrusted: Bool) async throws -> User - func login(on request: Request, authenticateToken: String) async throws -> User - func forgotPassword(on request: Request, email: String) async throws -> User - func confirmForgotPassword(on request: Request, forgotPasswordGuid: String, password: String) async throws - func changePassword(on request: Request, userId: Int64, currentPassword: String, newPassword: String) async throws - func changeEmail(on request: Request, userId: Int64, email: String) async throws - func confirmEmail(on request: Request, userId: Int64, confirmationGuid: String) async throws - func isUserNameTaken(on request: Request, userName: String) async throws -> Bool - func isEmailConnected(on request: Request, email: String) async throws -> Bool - func isSignedInUser(on request: Request, userName: String) -> Bool - func validateUserName(on request: Request, userName: String) async throws - func validateEmail(on request: Request, email: String?) async throws - func updateUser(on request: Request, userDto: UserDto, userNameNormalized: String) async throws -> User - func update(user: User, on application: Application, basedOn person: PersonDto, withAvatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User - func create(on application: Application, basedOn person: PersonDto, withAvatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User + func convertToDto(user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool, on context: ExecutionContext) async -> UserDto + func convertToDtos(users: [User], attachSensitive: Bool, on context: ExecutionContext) async -> [UserDto] + func login(userNameOrEmail: String, password: String, isMachineTrusted: Bool, on request: Request) async throws -> User + func login(authenticateToken: String, on request: Request) async throws -> User + func forgotPassword(email: String, on request: Request) async throws -> User + func confirmForgotPassword(forgotPasswordGuid: String, password: String, on request: Request) async throws + func changePassword(userId: Int64, currentPassword: String, newPassword: String, on request: Request) async throws + func changeEmail(userId: Int64, email: String, on request: Request) async throws + func confirmEmail(userId: Int64, confirmationGuid: String, on request: Request) async throws + func isUserNameTaken(userName: String, on request: Request) async throws -> Bool + func isEmailConnected(email: String, on request: Request) async throws -> Bool + func isSignedInUser(userName: String, on request: Request) -> Bool + func validateUserName(userName: String, on request: Request) async throws + func validateEmail(email: String?, on request: Request) async throws + func updateUser(userDto: UserDto, userNameNormalized: String, on context: ExecutionContext) async throws -> User + func update(user: User, basedOn person: PersonDto, withAvatarFileName: String?, withHeaderFileName headerFileName: String?, on context: ExecutionContext) async throws -> User + func create(basedOn person: PersonDto, withAvatarFileName: String?, withHeaderFileName headerFileName: String?, on context: ExecutionContext) async throws -> User func delete(user: User, force: Bool, on database: Database) async throws func delete(localUser userId: Int64, on context: QueueContext) async throws func delete(remoteUser: User, on database: Database) async throws func createGravatarHash(from email: String) -> String - func updateFollowCount(on database: Database, for userId: Int64) async throws + func updateFollowCount(for userId: Int64, on database: Database) async throws func deleteFromRemote(userId: Int64, on: QueueContext) async throws func ownStatuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult func publicStatuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult @@ -65,7 +65,7 @@ protocol UsersServiceType: Sendable { /// A service for managing users. final class UsersService: UsersServiceType { - func count(on database: Database, sinceLastLoginDate: Date?) async throws -> Int { + func count(sinceLastLoginDate: Date?, on database: Database) async throws -> Int { var query = User.query(on: database) .filter(\.$isLocal == true) @@ -76,7 +76,7 @@ final class UsersService: UsersServiceType { return try await query.count() } - func get(on database: Database, id: Int64) async throws -> User? { + func get(id: Int64, on database: Database) async throws -> User? { return try await User.query(on: database) .filter(\.$id == id) .with(\.$flexiFields) @@ -84,7 +84,7 @@ final class UsersService: UsersServiceType { .first() } - func get(on database: Database, userName: String) async throws -> User? { + func get(userName: String, on database: Database) async throws -> User? { let userNameNormalized = userName.uppercased() return try await User.query(on: database) .filter(\.$userNameNormalized == userNameNormalized) @@ -93,7 +93,7 @@ final class UsersService: UsersServiceType { .first() } - func get(on database: Database, account: String) async throws -> User? { + func get(account: String, on database: Database) async throws -> User? { let accountNormalized = account.uppercased() return try await User.query(on: database) .filter(\.$accountNormalized == accountNormalized) @@ -102,7 +102,7 @@ final class UsersService: UsersServiceType { .first() } - func get(on database: Database, activityPubProfile: String) async throws -> User? { + func get(activityPubProfile: String, on database: Database) async throws -> User? { let activityPubProfileNormalized = activityPubProfile.uppercased() return try await User.query(on: database) .filter(\.$activityPubProfileNormalized == activityPubProfileNormalized) @@ -125,36 +125,36 @@ final class UsersService: UsersServiceType { return moderators.uniqued { user in user.id } } - func convertToDto(on request: Request, user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool) async -> UserDto { - let isFeatured = try? await self.userIsFeatured(on: request, userId: user.requireID()) - - let userProfile = self.getUserProfile(on: request, - user: user, - flexiFields: flexiFields, - roles: roles, - attachSensitive: attachSensitive, - isFeatured: isFeatured ?? false) + func convertToDto(user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool, on context: ExecutionContext) async -> UserDto { + let isFeatured = try? await self.userIsFeatured(userId: user.requireID(), on: context) + + let userProfile = self.getUserProfile(user: user, + flexiFields: flexiFields, + roles: roles, + attachSensitive: attachSensitive, + isFeatured: isFeatured ?? false, + on: context) return userProfile } - func convertToDtos(on request: Request, users: [User], attachSensitive: Bool) async -> [UserDto] { + func convertToDtos(users: [User], attachSensitive: Bool, on context: ExecutionContext) async -> [UserDto] { let userIds = users.compactMap { $0.id } - let featuredUsers = try? await self.usersAreFeatured(on: request, userIds: userIds) + let featuredUsers = try? await self.usersAreFeatured(userIds: userIds, on: context) let userDtos = await users.asyncMap { user in - let userProfile = self.getUserProfile(on: request, - user: user, + let userProfile = self.getUserProfile(user: user, flexiFields: user.flexiFields, roles: user.roles, attachSensitive: attachSensitive, - isFeatured: featuredUsers?.contains(where: { $0 == user.id }) ?? false) + isFeatured: featuredUsers?.contains(where: { $0 == user.id }) ?? false, + on: context) return userProfile } return userDtos } - func login(on request: Request, userNameOrEmail: String, password: String, isMachineTrusted: Bool) async throws -> User { + func login(userNameOrEmail: String, password: String, isMachineTrusted: Bool, on request: Request) async throws -> User { let userNameOrEmailNormalized = userNameOrEmail.uppercased() @@ -209,7 +209,7 @@ final class UsersService: UsersServiceType { return user } - func login(on request: Request, authenticateToken: String) async throws -> User { + func login(authenticateToken: String, on request: Request) async throws -> User { let externalUser = try await ExternalUser .query(on: request.db) .with(\.$user) @@ -240,7 +240,7 @@ final class UsersService: UsersServiceType { return user } - func forgotPassword(on request: Request, email: String) async throws -> User { + func forgotPassword(email: String, on request: Request) async throws -> User { let emailNormalized = email.uppercased() let userFromDb = try await User.query(on: request.db).filter(\.$emailNormalized == emailNormalized).first() @@ -260,7 +260,7 @@ final class UsersService: UsersServiceType { return user } - func confirmForgotPassword(on request: Request, forgotPasswordGuid: String, password: String) async throws { + func confirmForgotPassword(forgotPasswordGuid: String, password: String, on request: Request) async throws { let userFromDb = try await User.query(on: request.db).filter(\.$forgotPasswordGuid == forgotPasswordGuid).first() guard let user = userFromDb else { @@ -296,7 +296,7 @@ final class UsersService: UsersServiceType { } } - func changePassword(on request: Request, userId: Int64, currentPassword: String, newPassword: String) async throws { + func changePassword(userId: Int64, currentPassword: String, newPassword: String, on request: Request) async throws { let userFromDb = try await User.query(on: request.db).filter(\.$id == userId).first() guard let user = userFromDb else { @@ -329,7 +329,7 @@ final class UsersService: UsersServiceType { try await user.update(on: request.db) } - func changeEmail(on request: Request, userId: Int64, email: String) async throws { + func changeEmail(userId: Int64, email: String, on request: Request) async throws { let userFromDb = try await User.find(userId, on: request.db) guard let user = userFromDb else { @@ -344,7 +344,7 @@ final class UsersService: UsersServiceType { try await user.update(on: request.db) } - func confirmEmail(on request: Request, userId: Int64, confirmationGuid: String) async throws { + func confirmEmail(userId: Int64, confirmationGuid: String, on request: Request) async throws { let userFromDb = try await User.find(userId, on: request.db) guard let user = userFromDb else { @@ -359,7 +359,7 @@ final class UsersService: UsersServiceType { try await user.save(on: request.db) } - func isUserNameTaken(on request: Request, userName: String) async throws -> Bool { + func isUserNameTaken(userName: String, on request: Request) async throws -> Bool { let userNameNormalized = userName.uppercased() @@ -371,7 +371,7 @@ final class UsersService: UsersServiceType { return false } - func isEmailConnected(on request: Request, email: String) async throws -> Bool { + func isEmailConnected(email: String, on request: Request) async throws -> Bool { let emailNormalized = email.uppercased() @@ -383,7 +383,7 @@ final class UsersService: UsersServiceType { return false } - func isSignedInUser(on request: Request, userName: String) -> Bool { + func isSignedInUser(userName: String, on request: Request) -> Bool { let userNameNormalized = userName.deletingPrefix("@").uppercased() let userNameFromToken = request.userName @@ -395,7 +395,7 @@ final class UsersService: UsersServiceType { return true } - func validateUserName(on request: Request, userName: String) async throws { + func validateUserName(userName: String, on request: Request) async throws { let userNameNormalized = userName.uppercased() let user = try await User.query(on: request.db).filter(\.$userNameNormalized == userNameNormalized).first() if user != nil { @@ -403,7 +403,7 @@ final class UsersService: UsersServiceType { } } - func validateEmail(on request: Request, email: String?) async throws { + func validateEmail(email: String?, on request: Request) async throws { let emailNormalized = (email ?? "").uppercased() let user = try await User.query(on: request.db).filter(\.$emailNormalized == emailNormalized).first() if user != nil { @@ -421,8 +421,8 @@ final class UsersService: UsersServiceType { } } - func updateUser(on request: Request, userDto: UserDto, userNameNormalized: String) async throws -> User { - let userFromDb = try await self.get(on: request.db, userName: userNameNormalized) + func updateUser(userDto: UserDto, userNameNormalized: String, on context: ExecutionContext) async throws -> User { + let userFromDb = try await self.get(userName: userNameNormalized, on: context.db) guard let user = userFromDb else { throw EntityNotFoundError.userNotFound @@ -438,18 +438,18 @@ final class UsersService: UsersServiceType { } // Save user data. - try await user.update(on: request.db) + try await user.update(on: context.db) // Update flexi-fields. - try await self.update(flexiFields: userDto.fields ?? [], on: request.application, for: user) + try await self.update(flexiFields: userDto.fields ?? [], for: user, on: context) // Update hashtags. - try await self.update(hashtags: userDto.bio, on: request, for: user) + try await self.update(hashtags: userDto.bio, for: user, on: context) return user } - func update(user: User, on application: Application, basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User { + func update(user: User, basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?, on context: ExecutionContext) async throws -> User { let remoteUserName = "\(person.preferredUsername)@\(person.url.host)" user.url = person.url @@ -466,20 +466,20 @@ final class UsersService: UsersServiceType { user.userOutbox = person.outbox // Save user data. - try await user.update(on: application.db) + try await user.update(on: context.db) // Update flexi-fields if let flexiFieldsDto = person.attachment?.map({ FlexiFieldDto(key: $0.name, value: $0.value, baseAddress: "") }) { - try await self.update(flexiFields: flexiFieldsDto, on: application, for: user) + try await self.update(flexiFields: flexiFieldsDto, for: user, on: context) } return user } - func create(on application: Application, basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?) async throws -> User { + func create(basedOn person: PersonDto, withAvatarFileName avatarFileName: String?, withHeaderFileName headerFileName: String?, on context: ExecutionContext) async throws -> User { let remoteUserName = "\(person.preferredUsername)@\(person.url.host)" - let newUserId = application.services.snowflakeService.generate() + let newUserId = context.services.snowflakeService.generate() let user = User(id: newUserId, url: person.url, isLocal: false, @@ -500,11 +500,11 @@ final class UsersService: UsersServiceType { ) // Save user to database. - try await user.save(on: application.db) + try await user.save(on: context.db) // Create flexi-fields if let flexiFieldsDto = person.attachment?.map({ FlexiFieldDto(key: $0.name, value: $0.value, baseAddress: "") }) { - try await self.update(flexiFields: flexiFieldsDto, on: application, for: user) + try await self.update(flexiFields: flexiFieldsDto, for: user, on: context) } return user @@ -518,7 +518,7 @@ final class UsersService: UsersServiceType { let statusesService = context.application.services.statusesService // We have to try to delete all user's statuses from local database. - try await statusesService.delete(owner: userId, on: context) + try await statusesService.delete(owner: userId, on: context.executionContext) // We have to delete all user's follows. let follows = try await Follow.query(on: context.application.db) @@ -622,7 +622,7 @@ final class UsersService: UsersServiceType { // Recalculate user's follows count. try await sourceIds.asyncForEach { sourceId in - try await self.updateFollowCount(on: context.application.db, for: sourceId) + try await self.updateFollowCount(for: sourceId, on: context.application.db) } } @@ -666,7 +666,7 @@ final class UsersService: UsersServiceType { } try await sourceIds.asyncForEach { sourceId in - try await self.updateFollowCount(on: database, for: sourceId) + try await self.updateFollowCount(for: sourceId, on: database) } } @@ -780,8 +780,8 @@ final class UsersService: UsersServiceType { ) } - private func update(flexiFields: [FlexiFieldDto], on application: Application, for user: User) async throws { - let flexiFieldsFromDb = try await user.$flexiFields.get(on: application.db) + private func update(flexiFields: [FlexiFieldDto], for user: User, on context: ExecutionContext) async throws { + let flexiFieldsFromDb = try await user.$flexiFields.get(on: context.db) var fieldsToDelete: [FlexiField] = [] for flexiFieldFromDb in flexiFieldsFromDb { @@ -795,7 +795,7 @@ final class UsersService: UsersServiceType { flexiFieldFromDb.value = flexiFieldDto.value flexiFieldFromDb.isVerified = false - try await flexiFieldFromDb.update(on: application.db) + try await flexiFieldFromDb.update(on: context.db) } } else { // Remember what to delete. @@ -804,7 +804,7 @@ final class UsersService: UsersServiceType { } // Delete from database. - try await fieldsToDelete.delete(on: application.db) + try await fieldsToDelete.delete(on: context.db) // Add new flexi fields. for flexiFieldDto in flexiFields { @@ -813,20 +813,20 @@ final class UsersService: UsersServiceType { } if flexiFieldsFromDb.contains(where: { $0.stringId() == flexiFieldDto.id }) == false { - let id = application.services.snowflakeService.generate() + let id = context.services.snowflakeService.generate() let flexiField = try FlexiField(id: id, key: flexiFieldDto.key, value: flexiFieldDto.value, isVerified: false, userId: user.requireID()) - try await flexiField.save(on: application.db) + try await flexiField.save(on: context.db) } } } - private func update(hashtags bio: String?, on request: Request, for user: User) async throws { + private func update(hashtags bio: String?, for user: User, on context: ExecutionContext) async throws { guard let bio else { - try await user.$hashtags.get(on: request.db).delete(on: request.db) + try await user.$hashtags.get(on: context.db).delete(on: context.db) return } @@ -837,7 +837,7 @@ final class UsersService: UsersServiceType { String(match.tag.trimmingPrefix("#")) } - let tagsFromDatabase = try await user.$hashtags.get(on: request.db) + let tagsFromDatabase = try await user.$hashtags.get(on: context.db) var tagsToDelete: [UserHashtag] = [] for tagFromDatabase in tagsFromDatabase { @@ -847,7 +847,7 @@ final class UsersService: UsersServiceType { } // Delete from database. - try await tagsToDelete.delete(on: request.db) + try await tagsToDelete.delete(on: context.db) // Add new hashtags. for tag in tags { @@ -856,14 +856,14 @@ final class UsersService: UsersServiceType { } if tagsFromDatabase.contains(where: { $0.hashtagNormalized == tag.uppercased() }) == false { - let userHashtagId = request.application.services.snowflakeService.generate() + let userHashtagId = context.services.snowflakeService.generate() let userHashtag = try UserHashtag(id: userHashtagId, userId: user.requireID(), hashtag: tag) - try await userHashtag.save(on: request.db) + try await userHashtag.save(on: context.db) } } } - func updateFollowCount(on database: Database, for userId: Int64) async throws { + func updateFollowCount(for userId: Int64, on database: Database) async throws { guard let sql = database as? SQLDatabase else { return } @@ -937,9 +937,9 @@ final class UsersService: UsersServiceType { return try await User.query(on: database).filter(\.$id == systemUserId).first() } - private func getUserProfile(on request: Request, user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool, isFeatured: Bool) -> UserDto { - let baseStoragePath = request.application.services.storageService.getBaseStoragePath(on: request.application) - let baseAddress = request.application.settings.cached?.baseAddress ?? "" + private func getUserProfile(user: User, flexiFields: [FlexiField]?, roles: [Role]?, attachSensitive: Bool, isFeatured: Bool, on context: ExecutionContext) -> UserDto { + let baseStoragePath = context.services.storageService.getBaseStoragePath(on: context) + let baseAddress = context.settings.cached?.baseAddress ?? "" var userDto = UserDto(from: user, flexiFields: flexiFields, @@ -962,20 +962,20 @@ final class UsersService: UsersServiceType { return userDto } - private func userIsFeatured(on request: Request, userId: Int64) async throws -> Bool { - let amount = try await FeaturedUser.query(on: request.db) + private func userIsFeatured(userId: Int64, on context: ExecutionContext) async throws -> Bool { + let amount = try await FeaturedUser.query(on: context.db) .filter(\.$featuredUser.$id == userId) .count() return amount > 0 } - private func usersAreFeatured(on request: Request, userIds: [Int64]) async throws -> [Int64] { - guard let authorizationPayloadId = request.userId else { + private func usersAreFeatured(userIds: [Int64], on context: ExecutionContext) async throws -> [Int64] { + guard let authorizationPayloadId = context.userId else { return [] } - let featuredUsers = try await FeaturedUser.query(on: request.db) + let featuredUsers = try await FeaturedUser.query(on: context.db) .filter(\.$user.$id == authorizationPayloadId) .filter(\.$featuredUser.$id ~~ userIds) .field(\.$featuredUser.$id) diff --git a/Sources/VernissageServer/VernissageServer.docc/VernissageServer.md b/Sources/VernissageServer/VernissageServer.docc/VernissageServer.md index db5d6836..9b2fb5ad 100644 --- a/Sources/VernissageServer/VernissageServer.docc/VernissageServer.md +++ b/Sources/VernissageServer/VernissageServer.docc/VernissageServer.md @@ -89,6 +89,7 @@ can be added by the system administrator. - ``NodeInfoController`` - ``NotificationsController`` - ``PushSubscriptionsController`` +- ``ProfileController`` - ``RegisterController`` - ``RelationshipsController`` - ``ReportsController`` @@ -359,6 +360,7 @@ can be added by the system administrator. ### Other +- ``ExecutionContext`` - ``Constants`` - ``Entrypoint`` - ``Password`` diff --git a/Tests/VernissageServerTests/AcceptanceTests/ActivityPubActorsController/ActivityPubActorsStatusActionTests.swift b/Tests/VernissageServerTests/AcceptanceTests/ActivityPubActorsController/ActivityPubActorsStatusActionTests.swift index 2033cbb1..39326747 100644 --- a/Tests/VernissageServerTests/AcceptanceTests/ActivityPubActorsController/ActivityPubActorsStatusActionTests.swift +++ b/Tests/VernissageServerTests/AcceptanceTests/ActivityPubActorsController/ActivityPubActorsStatusActionTests.swift @@ -24,7 +24,7 @@ extension ControllersTests { // Arrange. let user = try await application.createUser(userName: "trondfoter") - let (statuses, attachments) = try await application.createStatuses(user: user, notePrefix: "AP note", amount: 1) + let (statuses, attachments) = try await application.createStatuses(user: user, notePrefix: "AP note 1", amount: 1) defer { application.clearFiles(attachments: attachments) } @@ -42,5 +42,30 @@ extension ControllersTests { #expect(noteDto.attributedTo == "http://localhost:8080/actors/trondfoter", "Property 'attributedTo' is not valid.") #expect(noteDto.url == "http://localhost:8080/@trondfoter/\(statuses.first?.stringId() ?? "")", "Property 'url' is not valid.") } + + @Test("Status without api prefix should be returned for unauthorized") + func statusWithoutApiPrefixShouldBeReturnedForUnauthorized() async throws { + + // Arrange. + let user = try await application.createUser(userName: "goronfoter") + let (statuses, attachments) = try await application.createStatuses(user: user, notePrefix: "AP note 1", amount: 1) + defer { + application.clearFiles(attachments: attachments) + } + + // Act. + let noteDto = try application.getResponse( + to: "/statuses/\(statuses.first!.requireID())", + version: .none, + method: .GET, + decodeTo: NoteDto.self + ) + + // Assert. + #expect(noteDto.id == "http://localhost:8080/actors/goronfoter/statuses/\(statuses.first?.stringId() ?? "")", "Property 'id' is not valid.") + #expect(noteDto.attachment?.count == 1, "Property 'attachment' is not valid.") + #expect(noteDto.attributedTo == "http://localhost:8080/actors/goronfoter", "Property 'attributedTo' is not valid.") + #expect(noteDto.url == "http://localhost:8080/@goronfoter/\(statuses.first?.stringId() ?? "")", "Property 'url' is not valid.") + } } } diff --git a/Tests/VernissageServerTests/AcceptanceTests/ProfileController/ProfileReadActionTests.swift b/Tests/VernissageServerTests/AcceptanceTests/ProfileController/ProfileReadActionTests.swift new file mode 100644 index 00000000..3b5413f3 --- /dev/null +++ b/Tests/VernissageServerTests/AcceptanceTests/ProfileController/ProfileReadActionTests.swift @@ -0,0 +1,68 @@ +// +// https://mczachurski.dev +// Copyright © 2024 Marcin Czachurski and the repository contributors. +// Licensed under the Apache License 2.0. +// + +@testable import VernissageServer +import ActivityPubKit +import Vapor +import Testing + +extension ControllersTests { + + @Suite("Profile (GET /:username)", .serialized, .tags(.profile)) + struct ProfileReadActionTests { + var application: Application! + + init() async throws { + self.application = try await ApplicationManager.shared.application() + } + + @Test("Actor profile should be returned for existing actor") + func actorProfileShouldBeReturnedForExistingActor() async throws { + + // Arrange. + let user = try await application.createUser(userName: "trondroxyk") + _ = try await application.createFlexiField(key: "KEY1", value: "VALUE-A", isVerified: true, userId: user.requireID()) + _ = try await application.createFlexiField(key: "KEY2", value: "VALUE-B", isVerified: false, userId: user.requireID()) + + // Act. + let personDto = try application.getResponse( + to: "/@trondroxyk", + version: .none, + decodeTo: PersonDto.self + ) + + // Assert. + #expect(personDto.id == "http://localhost:8080/actors/trondroxyk", "Property 'id' is not valid.") + #expect(personDto.type == "Person", "Property 'type' is not valid.") + #expect(personDto.inbox == "http://localhost:8080/actors/trondroxyk/inbox", "Property 'inbox' is not valid.") + #expect(personDto.outbox == "http://localhost:8080/actors/trondroxyk/outbox", "Property 'outbox' is not valid.") + #expect(personDto.following == "http://localhost:8080/actors/trondroxyk/following", "Property 'inbox' is not valid.") + #expect(personDto.followers == "http://localhost:8080/actors/trondroxyk/followers", "Property 'outbox' is not valid.") + #expect(personDto.preferredUsername == "trondroxyk", "Property 'preferredUsername' is not valid.") + + #expect(personDto.attachment?[0].name == "KEY1", "Property 'fields[0].name' is not valid.") + #expect(personDto.attachment?[1].name == "KEY2", "Property 'fields[1].name' is not valid.") + + #expect(personDto.attachment?[0].value == "VALUE-A", "Property 'fields[0].value' is not valid.") + #expect(personDto.attachment?[1].value == "VALUE-B", "Property 'fields[1].value' is not valid.") + + #expect(personDto.attachment?[0].type == "PropertyValue", "Property 'fields[0].type' is not valid.") + #expect(personDto.attachment?[1].type == "PropertyValue", "Property 'fields[1].type' is not valid.") + } + + @Test("Actor profile should not be returned for not existing actor") + func actorProfileShouldNotBeReturnedForNotExistingActor() throws { + + // Act. + let response = try application.sendRequest(to: "/@unknown", + version: .none, + method: .GET) + + // Assert. + #expect(response.status == HTTPResponseStatus.notFound, "Response http status code should be not found (404).") + } + } +} diff --git a/Tests/VernissageServerTests/AcceptanceTests/StatusesController/StatusesReadActionTests.swift b/Tests/VernissageServerTests/AcceptanceTests/StatusesController/StatusesReadActionTests.swift index 52bfa485..5f0277a8 100644 --- a/Tests/VernissageServerTests/AcceptanceTests/StatusesController/StatusesReadActionTests.swift +++ b/Tests/VernissageServerTests/AcceptanceTests/StatusesController/StatusesReadActionTests.swift @@ -44,7 +44,7 @@ extension ControllersTests { #expect(status.note == statusDto.note, "Status note should be returned.") #expect(statusDto.user.userName == "robinhoower", "User should be returned.") } - + @Test("Other user private status should not be returned") func otherUserPrivateStatusShouldNotBeReturned() async throws { diff --git a/Tests/VernissageServerTests/AcceptanceTests/Tags.swift b/Tests/VernissageServerTests/AcceptanceTests/Tags.swift index f17ae706..5aa3b741 100644 --- a/Tests/VernissageServerTests/AcceptanceTests/Tags.swift +++ b/Tests/VernissageServerTests/AcceptanceTests/Tags.swift @@ -30,6 +30,7 @@ extension Tag { @Tag static var nodeinfo: Tag @Tag static var notifications: Tag @Tag static var pushSubscriptions: Tag + @Tag static var profile: Tag @Tag static var register: Tag @Tag static var relationships: Tag @Tag static var reports: Tag diff --git a/Tests/VernissageServerTests/AcceptanceTests/UsersController/UsersReadActionTests.swift b/Tests/VernissageServerTests/AcceptanceTests/UsersController/UsersReadActionTests.swift index 206ee319..70c12b6f 100644 --- a/Tests/VernissageServerTests/AcceptanceTests/UsersController/UsersReadActionTests.swift +++ b/Tests/VernissageServerTests/AcceptanceTests/UsersController/UsersReadActionTests.swift @@ -41,7 +41,7 @@ extension ControllersTests { #expect(userDto.name == user.name, "Property 'name' should be equal.") #expect(userDto.bio == user.bio, "Property 'bio' should be equal.") } - + @Test("User profile should be returned for existing user by user id") func userProfileShouldBeReturnedForExistingUserByUserId() async throws { diff --git a/Tests/VernissageServerTests/AcceptanceTests/WellKnownController/WellKnownNodeInfoActionTests.swift b/Tests/VernissageServerTests/AcceptanceTests/WellKnownController/WellKnownNodeInfoActionTests.swift index 6546c927..aedd5747 100644 --- a/Tests/VernissageServerTests/AcceptanceTests/WellKnownController/WellKnownNodeInfoActionTests.swift +++ b/Tests/VernissageServerTests/AcceptanceTests/WellKnownController/WellKnownNodeInfoActionTests.swift @@ -24,15 +24,15 @@ extension ControllersTests { func nodeInfoShouldBeReturnedInCorrectFormat() throws { // Act. - let nodeInfoLinkDto = try application.getResponse( + let nodeInfoLinksDto = try application.getResponse( to: "/.well-known/nodeinfo", version: .none, - decodeTo: NodeInfoLinkDto.self + decodeTo: NodeInfoLinksDto.self ) // Assert. - #expect(nodeInfoLinkDto.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0", "Property 'rel' should conatin protocol version.") - #expect(nodeInfoLinkDto.href == "http://localhost:8080/api/v1/nodeinfo/2.0", "Property 'href' should contain link to nodeinfo.") + #expect(nodeInfoLinksDto.links.first?.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0", "Property 'rel' should conatin protocol version.") + #expect(nodeInfoLinksDto.links.first?.href == "http://localhost:8080/api/v1/nodeinfo/2.0", "Property 'href' should contain link to nodeinfo.") } } } diff --git a/Tests/VernissageServerTests/Helpers/Mocks/MockEmailsService.swift b/Tests/VernissageServerTests/Helpers/Mocks/MockEmailsService.swift index cf707d43..2340a210 100644 --- a/Tests/VernissageServerTests/Helpers/Mocks/MockEmailsService.swift +++ b/Tests/VernissageServerTests/Helpers/Mocks/MockEmailsService.swift @@ -8,13 +8,13 @@ import XCTVapor final class MockEmailsService: EmailsServiceType { - func setServerSettings(on application: Application, hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?) { + func setServerSettings(hostName: Setting?, port: Setting?, userName: Setting?, password: Setting?, secureMethod: Setting?, on application: Application) { } - func dispatchForgotPasswordEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { + func dispatchForgotPasswordEmail(user: User, redirectBaseUrl: String, on request: Request) async throws { } - func dispatchConfirmAccountEmail(on request: Request, user: User, redirectBaseUrl: String) async throws { + func dispatchConfirmAccountEmail(user: User, redirectBaseUrl: String, on request: Request) async throws { } } diff --git a/Tests/VernissageServerTests/Helpers/Mocks/MockSearchService.swift b/Tests/VernissageServerTests/Helpers/Mocks/MockSearchService.swift index 56418a14..9b3208b7 100644 --- a/Tests/VernissageServerTests/Helpers/Mocks/MockSearchService.swift +++ b/Tests/VernissageServerTests/Helpers/Mocks/MockSearchService.swift @@ -9,22 +9,17 @@ import XCTVapor import Queues final class MockSearchService: SearchServiceType { - func search(query: String, searchType: VernissageServer.SearchTypeDto, request: Vapor.Request) async throws -> VernissageServer.SearchResultDto { + func search(query: String, searchType: VernissageServer.SearchTypeDto, on context: ExecutionContext) async throws -> VernissageServer.SearchResultDto { let searchService = SearchService() - return try await searchService.search(query: query, searchType: searchType, request: request) + return try await searchService.search(query: query, searchType: searchType, on: context) } - func downloadRemoteUser(activityPubProfile: String, on request: Vapor.Request) async -> VernissageServer.SearchResultDto { - let searchService = SearchService() - return await searchService.downloadRemoteUser(activityPubProfile: activityPubProfile, on: request) - } - - func downloadRemoteUser(activityPubProfile: String, on context: Queues.QueueContext) async throws -> VernissageServer.User? { + func downloadRemoteUser(activityPubProfile: String, on context: ExecutionContext) async throws -> VernissageServer.User? { let searchService = SearchService() return try await searchService.downloadRemoteUser(activityPubProfile: activityPubProfile, on: context) } - func getRemoteActivityPubProfile(userName: String, on request: Vapor.Request) async -> String? { + func getRemoteActivityPubProfile(userName: String, on context: ExecutionContext) async -> String? { let name = userName.split(separator: "@").first let domain = userName.split(separator: "@").last