Skip to content

Commit

Permalink
Search improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski committed Oct 13, 2024
1 parent 0e3c0c2 commit e4fbaac
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 32 deletions.
4 changes: 4 additions & 0 deletions Sources/VernissageServer/Controllers/UsersController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ struct UsersController {
userDtoAfterUpdate.email = user.email
userDtoAfterUpdate.emailWasConfirmed = user.emailWasConfirmed
userDtoAfterUpdate.locale = user.locale
userDtoAfterUpdate.twoFactorEnabled = user.twoFactorEnabled
userDtoAfterUpdate.manuallyApprovesFollowers = user.manuallyApprovesFollowers

return userDtoAfterUpdate
}
Expand Down Expand Up @@ -1531,6 +1533,8 @@ struct UsersController {
userDto.email = user.email
userDto.locale = user.locale
userDto.emailWasConfirmed = user.emailWasConfirmed
userDto.twoFactorEnabled = user.twoFactorEnabled
userDto.manuallyApprovesFollowers = user.manuallyApprovesFollowers
}

return userDto
Expand Down
13 changes: 9 additions & 4 deletions Sources/VernissageServer/DataTransferObjects/UserDto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ struct UserDto: Codable {
var createdAt: Date?
var updatedAt: Date?
var roles: [String]?
var twoFactorEnabled: Bool
var twoFactorEnabled: Bool?
var manuallyApprovesFollowers: Bool?

enum CodingKeys: String, CodingKey {
case id
Expand All @@ -57,6 +58,7 @@ struct UserDto: Codable {
case updatedAt
case roles
case twoFactorEnabled
case manuallyApprovesFollowers
}

init(id: String? = nil,
Expand All @@ -73,7 +75,8 @@ struct UserDto: Codable {
statusesCount: Int,
followersCount: Int,
followingCount: Int,
twoFactorEnabled: Bool = false,
twoFactorEnabled: Bool? = nil,
manuallyApprovesFollowers: Bool? = nil,
activityPubProfile: String = "",
fields: [FlexiFieldDto]? = nil,
roles: [String]? = nil,
Expand All @@ -100,8 +103,9 @@ struct UserDto: Codable {
self.createdAt = createdAt
self.updatedAt = updatedAt
self.roles = roles
self.twoFactorEnabled = twoFactorEnabled

self.manuallyApprovesFollowers = manuallyApprovesFollowers
self.twoFactorEnabled = twoFactorEnabled
self.email = nil
self.emailWasConfirmed = nil
self.locale = nil
Expand Down Expand Up @@ -132,6 +136,7 @@ struct UserDto: Codable {
updatedAt = try values.decodeIfPresent(Date.self, forKey: .updatedAt)
roles = try values.decodeIfPresent([String].self, forKey: .roles)
twoFactorEnabled = try values.decodeIfPresent(Bool.self, forKey: .twoFactorEnabled) ?? false
manuallyApprovesFollowers = try values.decodeIfPresent(Bool.self, forKey: .manuallyApprovesFollowers) ?? false
}

func encode(to encoder: Encoder) throws {
Expand Down Expand Up @@ -160,6 +165,7 @@ struct UserDto: Codable {
try container.encodeIfPresent(updatedAt, forKey: .updatedAt)
try container.encodeIfPresent(roles, forKey: .roles)
try container.encodeIfPresent(twoFactorEnabled, forKey: .twoFactorEnabled)
try container.encodeIfPresent(manuallyApprovesFollowers, forKey: .manuallyApprovesFollowers)
}
}

Expand All @@ -181,7 +187,6 @@ extension UserDto {
statusesCount: user.statusesCount,
followersCount: user.followersCount,
followingCount: user.followingCount,
twoFactorEnabled: user.twoFactorEnabled,
activityPubProfile: user.activityPubProfile,
fields: flexiFields?.map({ FlexiFieldDto(from: $0, baseAddress: baseAddress, isLocalUser: user.isLocal) }),
roles: roles?.map({ $0.code }),
Expand Down
18 changes: 18 additions & 0 deletions Sources/VernissageServer/Extensions/QueryBuilder+Status.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// https://mczachurski.dev
// Copyright © 2024 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//

import Vapor
import Fluent

extension QueryBuilder<Status> {
func filter(id: Int64?) -> Self {
guard let id else {
return self
}

return self.filter(\.$id == id)
}
}
26 changes: 26 additions & 0 deletions Sources/VernissageServer/Extensions/QueryBuilder+User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// https://mczachurski.dev
// Copyright © 2024 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//

import Vapor
import Fluent

extension QueryBuilder<User> {
func filter(id: Int64?) -> Self {
guard let id else {
return self
}

return self.filter(\.$id == id)
}

func filter(userName: String?) -> Self {
guard let userName else {
return self
}

return self.filter(\.$userNameNormalized == userName)
}
}
79 changes: 63 additions & 16 deletions Sources/VernissageServer/Services/SearchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protocol SearchServiceType: Sendable {
final class SearchService: SearchServiceType {
func search(query: String, searchType: SearchTypeDto, request: Request) async throws -> SearchResultDto {
let queryWithoutPrefix = String(query.trimmingPrefix("@"))

switch searchType {
case .users:
return await self.searchByUsers(query: queryWithoutPrefix, on: request)
Expand All @@ -57,7 +57,7 @@ final class SearchService: SearchServiceType {

// 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)

Expand All @@ -74,7 +74,7 @@ final class SearchService: SearchServiceType {

let baseStoragePath = storageService.getBaseStoragePath(on: request.application)
let baseAddress = request.application.settings.cached?.baseAddress ?? ""

let flexiFields = try? await flexiFieldService.getFlexiFields(on: request.db, for: user.requireID())
let userDto = UserDto(from: user, flexiFields: flexiFields, baseStoragePath: baseStoragePath, baseAddress: baseAddress)

Expand All @@ -85,7 +85,7 @@ final class SearchService: SearchServiceType {

return SearchResultDto(users: [userDto])
}

func downloadRemoteUser(activityPubProfile: String, on context: QueueContext) async throws -> User? {
let usersService = context.application.services.usersService

Expand All @@ -101,7 +101,7 @@ final class SearchService: SearchServiceType {

// 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)

Expand Down Expand Up @@ -154,23 +154,23 @@ final class SearchService: SearchServiceType {
guard let defaultSystemUser = try await usersService.getDefaultSystemUser(on: application.db) else {
throw ActivityPubError.missingInstanceAdminAccount
}

guard let privateKey = defaultSystemUser.privateKey else {
throw ActivityPubError.missingInstanceAdminPrivateKey
}

guard let activityPubProfileUrl = URL(string: activityPubProfile) else {
throw ActivityPubError.unrecognizedActivityPubProfileUrl
}

let activityPubClient = ActivityPubClient(privatePemKey: privateKey, userAgent: Constants.userAgent, host: activityPubProfileUrl.host)
let userProfile = try await activityPubClient.person(id: activityPubProfile, activityPubProfile: defaultSystemUser.activityPubProfile)

return userProfile
} catch {
application.logger.error("Error during download profile: '\(activityPubProfile)'. Error: \(error.localizedDescription).")
}

return nil
}

Expand All @@ -188,8 +188,15 @@ final class SearchService: SearchServiceType {
return SearchResultDto(statuses: [])
}

let id = self.getIdFromQuery(from: query)
let statuses = try? await Status.query(on: request.db)
.filter(\.$note ~~ query)
.group(.or) { group in
group
.filter(id: id)
.filter(\.$note ~~ query)
.filter(\.$activityPubId == query)
.filter(\.$activityPubUrl == query)
}
.filter(\.$visibility == .public)
.filter(\.$replyToStatus.$id == nil)
.with(\.$user)
Expand All @@ -207,17 +214,17 @@ final class SearchService: SearchServiceType {
.with(\.$category)
.sort(\.$createdAt, .descending)
.paginate(PageRequest(page: 1, per: 20))

guard let statuses else {
return SearchResultDto(statuses: [])
}

let statusesService = request.application.services.statusesService
let statusesDtos = await statusesService.convertToDtos(on: request, statuses: statuses.items)

return SearchResultDto(statuses: statusesDtos)
}

private func searchByHashtags(query: String, on request: Request) async -> SearchResultDto {
// For empty query we don't have to retrieve anything from database and return empty list.
if query.isEmpty {
Expand All @@ -244,15 +251,29 @@ final class SearchService: SearchServiceType {
}

private func searchByLocalUsers(query: String, on request: Request) async -> SearchResultDto {
let usersService = request.application.services.usersService

// 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 userNameNormalized = self.getUserNameFromQuery(from: query)
let id = self.getIdFromQuery(from: query)

let users = try? await User.query(on: request.db)
.group(.or) { group in
group
.filter(id: id)
.filter(userName: userNameNormalized)
.filter(\.$queryNormalized ~~ queryNormalized)
.filter(\.$activityPubProfile == query)
.filter(\.$url == query)
}
.sort(\.$followersCount, .descending)
.paginate(PageRequest(page: 1, per: 20))

// In case of error we have to return empty list.
guard let users = try? await usersService.search(query: query, on: request, page: 1, size: 20) else {
guard let users else {
request.logger.notice("Issue during filtering local users.")
return SearchResultDto(users: [])
}
Expand Down Expand Up @@ -453,6 +474,10 @@ final class SearchService: SearchServiceType {
}

private func isLocalSearch(query: String, on request: Request) -> Bool {
if query.starts(with: "http://") || query.starts(with: "https://") {
return true
}

let queryParts = query.split(separator: "@")
if queryParts.count <= 1 {
return true
Expand Down Expand Up @@ -498,4 +523,26 @@ final class SearchService: SearchServiceType {

return anyTemplate
}



private func getIdFromQuery(from query: String) -> Int64? {
let components = query.components(separatedBy: "/")
guard let stringId = components.last else {
return nil
}

return Int64(stringId)
}

private func getUserNameFromQuery(from query: String) -> String? {
let components = query.components(separatedBy: "/")
guard let userName = components.last else {
return nil
}

return userName
.trimmingCharacters(in: .init(charactersIn: "@"))
.uppercased()
}
}
4 changes: 2 additions & 2 deletions Sources/VernissageServer/Services/TrendingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ final class TrendingService: TrendingServiceType {
AND \(ident: "s").\(ident: "reblogId") IS NULL
AND \(ident: "s").\(ident: "replyToStatusId") IS NULL
GROUP BY \(ident: "s").\(ident: "userId")
ORDER BY COUNT(\(ident: "s").\(ident: "userId"))
ORDER BY COUNT(\(ident: "s").\(ident: "userId")) DESC
LIMIT 1000
""").all(decoding: TrendingAmount.self)

Expand All @@ -362,7 +362,7 @@ final class TrendingService: TrendingServiceType {
AND \(ident: "s").\(ident: "reblogId") IS NULL
AND \(ident: "s").\(ident: "replyToStatusId") IS NULL
GROUP BY \(ident: "st").\(ident: "hashtagNormalized")
ORDER BY COUNT(\(ident: "st").\(ident: "hashtagNormalized"))
ORDER BY COUNT(\(ident: "st").\(ident: "hashtagNormalized")) DESC
LIMIT 1000
""").all(decoding: TrendingHashtagAmount.self)

Expand Down
11 changes: 1 addition & 10 deletions Sources/VernissageServer/Services/UsersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ protocol UsersServiceType: Sendable {
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 search(query: String, on request: Request, page: Int, size: Int) async throws -> Page<User>
func updateFollowCount(on database: Database, for userId: Int64) async throws
func deleteFromRemote(userId: Int64, on: QueueContext) async throws
func ownStatuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult<Status>
Expand Down Expand Up @@ -385,6 +384,7 @@ final class UsersService: UsersServiceType {
// Update filds in user entity.
user.name = userDto.name
user.bio = userDto.bio
user.manuallyApprovesFollowers = userDto.manuallyApprovesFollowers ?? false

if let locale = userDto.locale {
user.locale = locale
Expand Down Expand Up @@ -633,15 +633,6 @@ final class UsersService: UsersServiceType {
return ""
}

func search(query: String, on request: Request, page: Int, size: Int) async throws -> Page<User> {
let queryNormalized = query.uppercased()

return try await User.query(on: request.db)
.filter(\.$queryNormalized ~~ queryNormalized)
.sort(\.$followersCount, .descending)
.paginate(PageRequest(page: page, per: size))
}

func ownStatuses(for userId: Int64, linkableParams: LinkableParams, on request: Request) async throws -> LinkableResult<Status> {
var query = Status.query(on: request.db)
.filter(\.$user.$id == userId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ extension ControllersTests {
statusesCount: 0,
followersCount: 0,
followingCount: 0,
manuallyApprovesFollowers: true,
baseAddress: "http://localhost:8080")

// Act.
Expand All @@ -51,6 +52,7 @@ extension ControllersTests {
#expect(updatedUserDto.email == user.email, "Property 'email' should not be changed.")
#expect(updatedUserDto.name == userDto.name, "Property 'name' should be changed.")
#expect(updatedUserDto.bio == userDto.bio, "Property 'bio' should be changed.")
#expect(updatedUserDto.manuallyApprovesFollowers == true, "Property 'manuallyApprovesFollowers' should be changed.")
}

@Test("Flexi field should be added to existing account")
Expand Down

0 comments on commit e4fbaac

Please sign in to comment.