Skip to content

Commit

Permalink
monorepo: rework filter suspension extra monitoring
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredh159 committed Sep 8, 2023
1 parent e4795c3 commit d3c7523
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 89 deletions.
47 changes: 47 additions & 0 deletions api/Sources/Api/Extend/Enums+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,53 @@ extension AdminVerifiedNotificationMethod.Config {
}
}

extension DecideFilterSuspensionRequest.Decision {
private struct _NamedCase: Codable {
var `case`: String
static func extract(from decoder: Decoder) throws -> String {
let container = try decoder.singleValueContainer()
return try container.decode(_NamedCase.self).case
}
}

private struct _TypeScriptDecodeError: Error {
var message: String
}

private struct _CaseAccepted: Codable {
var `case` = "accepted"
var durationInSeconds: Int
var extraMonitoring: String?
}

func encode(to encoder: Encoder) throws {
switch self {
case .accepted(let durationInSeconds, let extraMonitoring):
try _CaseAccepted(durationInSeconds: durationInSeconds, extraMonitoring: extraMonitoring)
.encode(to: encoder)
case .rejected:
try _NamedCase(case: "rejected").encode(to: encoder)
}
}

init(from decoder: Decoder) throws {
let caseName = try _NamedCase.extract(from: decoder)
let container = try decoder.singleValueContainer()
switch caseName {
case "accepted":
let value = try container.decode(_CaseAccepted.self)
self = .accepted(
durationInSeconds: value.durationInSeconds,
extraMonitoring: value.extraMonitoring
)
case "rejected":
self = .rejected
default:
throw _TypeScriptDecodeError(message: "Unexpected case name: `\(caseName)`")
}
}
}

public extension UserActivity.Item {
private struct _NamedCase: Codable {
var `case`: String
Expand Down
80 changes: 80 additions & 0 deletions api/Sources/Api/Models/User/User+ExtraMonitoringOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Gertie

extension FilterSuspensionDecision.ExtraMonitoring {
var magicString: String {
switch self {
case .addKeylogging:
return "k"
case .setScreenshotFreq(let frequency):
return "@\(frequency)"
case .addKeyloggingAndSetScreenshotFreq(let frequency):
return "@\(frequency)+k"
}
}

init?(magicString: String) {
var input = magicString
if input == "k" {
self = .addKeylogging
} else if input.hasPrefix("@") {
input.removeFirst()
var withKeylogging = false
if input.hasSuffix("+k") {
withKeylogging = true
input.removeLast(2)
}
guard let frequency = Int(input) else {
return nil
}
if withKeylogging {
self = .addKeyloggingAndSetScreenshotFreq(frequency)
} else {
self = .setScreenshotFreq(frequency)
}
} else {
return nil
}
}
}

extension User {
var extraMonitoringOptions: [FilterSuspensionDecision.ExtraMonitoring: String] {
var opts: [FilterSuspensionDecision.ExtraMonitoring: String] = [:]
if !keyloggingEnabled {
opts[.addKeylogging] = "keylogging"
}

if !screenshotsEnabled {
opts[.setScreenshotFreq(120)] = "Screenshot every 2m"
opts[.setScreenshotFreq(90)] = "Screenshot every 90s"
opts[.setScreenshotFreq(60)] = "Screenshot every 60s"
opts[.setScreenshotFreq(30)] = "Screenshot every 30s"

if !keyloggingEnabled {
opts[.addKeyloggingAndSetScreenshotFreq(120)] = "Screenshot every 2m + keylogging"
opts[.addKeyloggingAndSetScreenshotFreq(90)] = "Screenshot every 90s + keylogging"
opts[.addKeyloggingAndSetScreenshotFreq(60)] = "Screenshot every 60s + keylogging"
opts[.addKeyloggingAndSetScreenshotFreq(30)] = "Screenshot every 30s + keylogging"
}
} else {
opts[.setScreenshotFreq(Int(Double(screenshotsFrequency) / 1.5))] = "1.5x screenshots"
opts[.setScreenshotFreq(screenshotsFrequency / 2)] = "2x screenshots"
opts[.setScreenshotFreq(screenshotsFrequency / 3)] = "3x screenshots"

if !keyloggingEnabled {
opts[.addKeyloggingAndSetScreenshotFreq(Int(Double(screenshotsFrequency) / 1.5))] =
"1.5x screenshots + keylogging"
opts[.addKeyloggingAndSetScreenshotFreq(screenshotsFrequency / 2)] =
"2x screenshots + keylogging"
opts[.addKeyloggingAndSetScreenshotFreq(screenshotsFrequency / 3)] =
"3x screenshots + keylogging"
}
}

opts = opts.filter { opt, _ in
opt.screenshotsFrequency ?? Int.max > 10
}

return opts
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import PairQL
struct DecideFilterSuspensionRequest: Pair {
static var auth: ClientAuth = .admin

enum Decision: PairNestable {
case rejected
case accepted(durationInSeconds: Int, extraMonitoring: String?)
}

struct Input: PairInput {
var id: SuspendFilterRequest.Id
var decision: FilterSuspensionDecision
var decision: Decision
var responseComment: String?
}
}
Expand All @@ -19,21 +24,25 @@ extension DecideFilterSuspensionRequest: Resolver {
let userDevice = try await request.userDevice()
try await context.verifiedUser(from: userDevice.userId)

switch input.decision {
case .accepted(let durationInSeconds, _):
request.duration = .init(durationInSeconds)
request.responseComment = input.responseComment
request.responseComment = input.responseComment
let decision = input.decision.filterSuspensionDecision

switch decision {
case .accepted(let duration, _):
request.duration = duration
request.status = .accepted
case .rejected:
request.responseComment = input.responseComment
request.status = .rejected
}

try await request.save()

if Semver(userDevice.appVersion)! >= .init("2.1.0")! {
try await Current.connectedApps
.notify(.suspendFilterRequestDecided(userDevice.id, input.decision))
try await Current.connectedApps.notify(.suspendFilterRequestDecided(
userDevice.id,
decision,
input.responseComment
))
} else {
try await Current.connectedApps.notify(.suspendFilterRequestUpdated(.init(
userDeviceId: userDevice.id,
Expand All @@ -47,3 +56,20 @@ extension DecideFilterSuspensionRequest: Resolver {
return .success
}
}

// extensions

extension DecideFilterSuspensionRequest.Decision {
var filterSuspensionDecision: FilterSuspensionDecision {
switch self {
case .rejected:
return .rejected
case .accepted(let durationInSeconds, let magicString):
return .accepted(
duration: .init(durationInSeconds),
extraMonitoring: magicString
.flatMap(FilterSuspensionDecision.ExtraMonitoring.init(magicString:))
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct GetSuspendFilterRequest: Pair {
var requestedDurationInSeconds: Int
var requestComment: String?
var responseComment: String?
var canDoubleScreenshots: Bool
var extraMonitoringOptions: [String: String]
var createdAt: Date
}
}
Expand All @@ -26,6 +26,10 @@ extension GetSuspendFilterRequest: Resolver {
let request = try await Current.db.find(id)
let userDevice = try await request.userDevice()
let user = try await userDevice.user()
var extraMonitoringOptions: [String: String] = [:]
if Semver(userDevice.appVersion)! >= .init("2.1.0")! {
extraMonitoringOptions = user.extraMonitoringOptions.mapKeys(\.magicString)
}
return Output(
id: id,
deviceId: userDevice.id,
Expand All @@ -34,8 +38,7 @@ extension GetSuspendFilterRequest: Resolver {
requestedDurationInSeconds: request.duration.rawValue,
requestComment: request.requestComment,
responseComment: request.responseComment,
canDoubleScreenshots: Semver(userDevice.appVersion)! >= .init("2.1.0")!
&& user.screenshotsEnabled,
extraMonitoringOptions: extraMonitoringOptions,
createdAt: request.createdAt
)
}
Expand Down
1 change: 0 additions & 1 deletion api/Sources/Api/Routes/DashboardTsCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ enum DashboardTsCodegenRoute {
("AdminSubscriptionStatus", Admin.SubscriptionStatus.self),
("VerifiedNotificationMethod", GetAdmin.VerifiedNotificationMethod.self),
("AdminNotification", GetAdmin.Notification.self),
("FilterSuspensionDecision", FilterSuspensionDecision.self),
]
}

Expand Down
7 changes: 5 additions & 2 deletions api/Sources/Api/Services/Websocket/AppConnections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ actor AppConnections {
)
}

case .suspendFilterRequestDecided(let userDeviceId, let decision):
case .suspendFilterRequestDecided(let userDeviceId, let decision, let comment):
try await currentConnections
.filter { $0.ids.userDevice == userDeviceId }
.asyncForEach {
try await $0.ws.send(codable: OutgoingMessage.filterSuspensionRequestDecided(decision))
try await $0.ws.send(
codable:
OutgoingMessage.filterSuspensionRequestDecided(decision: decision, comment: comment)
)
}

case .suspendFilterRequestUpdated(let payload):
Expand Down
2 changes: 1 addition & 1 deletion api/Sources/Api/Services/Websocket/AppEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import TaggedTime

enum AppEvent: Equatable {
case keychainUpdated(Keychain.Id)
case suspendFilterRequestDecided(UserDevice.Id, FilterSuspensionDecision)
case suspendFilterRequestDecided(UserDevice.Id, FilterSuspensionDecision, String?)
case unlockRequestUpdated(UnlockRequestUpdated)
case userUpdated(User.Id)
case userDeleted(User.Id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,15 +428,15 @@ final class AuthedAdminResolverTests: ApiTestCase {
}

func testDecideSuspendFilterRequest_Accepted() async throws {
let user = try await Entities.user().withDevice { $0.appVersion = "2.0.0" } // <-- old event
let user = try await Entities.user().withDevice { $0.appVersion = "2.1.2" } // <-- new event
let request = try await Current.db.create(SuspendFilterRequest.random {
$0.userDeviceId = user.device.id
$0.status = .pending
})

let decision: FilterSuspensionDecision = .accepted(
let decision: DecideFilterSuspensionRequest.Decision = .accepted(
durationInSeconds: 333,
doubledScreenshots: nil
extraMonitoring: "@55+k"
)

let output = try await DecideFilterSuspensionRequest.resolve(
Expand All @@ -452,18 +452,19 @@ final class AuthedAdminResolverTests: ApiTestCase {
expect(retrieved.status).toEqual(.accepted)

expect(sent.appEvents).toEqual([
.suspendFilterRequestUpdated(.init( // <-- old event
userDeviceId: user.device.id,
status: .accepted,
duration: 333,
requestComment: request.requestComment,
responseComment: "ok"
)),
.suspendFilterRequestDecided( // <-- new event
user.device.id,
.accepted(
duration: 333,
extraMonitoring: .addKeyloggingAndSetScreenshotFreq(55)
),
"ok"
),
])
}

func testDecideSuspendFilterRequest_Rejected() async throws {
let user = try await Entities.user().withDevice { $0.appVersion = "2.1.2" } // <-- new event
let user = try await Entities.user().withDevice { $0.appVersion = "2.0.2" } // <-- old event
let request = try await Current.db.create(SuspendFilterRequest.random {
$0.duration = .init(100)
$0.userDeviceId = user.device.id
Expand All @@ -482,7 +483,13 @@ final class AuthedAdminResolverTests: ApiTestCase {
expect(retrieved.status).toEqual(.rejected)

expect(sent.appEvents).toEqual([
.suspendFilterRequestDecided(user.device.id, .rejected), // <-- new event
.suspendFilterRequestUpdated(.init( // <-- old event
userDeviceId: user.device.id,
status: .rejected,
duration: 100,
requestComment: request.requestComment,
responseComment: nil
)),
])
}

Expand Down
Loading

0 comments on commit d3c7523

Please sign in to comment.