diff --git a/Foundation/Source/Sources/Network/Network.swift b/Foundation/Source/Sources/Network/Network.swift index 74e82fa..0892f2c 100644 --- a/Foundation/Source/Sources/Network/Network.swift +++ b/Foundation/Source/Sources/Network/Network.swift @@ -43,11 +43,13 @@ public class Network { case following_invalidate case users + case users_report_abuse + case users_followers + case users_following case user_show case user_notes - case users_followers - case users_following + case blocking_create case blocking_delete @@ -109,11 +111,13 @@ public class Network { // MARK: - USER .users: .init(path: "/users", method: .post), - .user_show: .init(path: "/users/show", method: .post), - .user_notes: .init(path: "/users/notes", method: .post), + .users_report_abuse: .init(path: "/users/report-abuse", method: .post), .users_followers: .init(path: "/users/followers", method: .post), .users_following: .init(path: "/users/following", method: .post), + .user_show: .init(path: "/users/show", method: .post), + .user_notes: .init(path: "/users/notes", method: .post), + .blocking_create: .init(path: "blocking/create", method: .post), .blocking_delete: .init(path: "blocking/delete", method: .post), @@ -167,7 +171,7 @@ public extension Network { internal var decoder = JSONDecoder() internal var encoder = JSONEncoder() -extension Network { +public extension Network { func obtainEndpointInfo(for target: RequestTarget) -> EndpointInfo { guard let info = endpointInfo[target] else { return .init(path: "/unavailable", method: .undefined) @@ -251,6 +255,15 @@ extension Network { } } + // 💩 + if request.httpMethod?.uppercased() == "POST", + let header = request.value(forHTTPHeaderField: "Content-Type"), + header.lowercased() == "application/json", + request.httpBody?.count ?? 0 <= 0 + { + request.httpBody = "{}".data(using: .utf8) + } + let sem = DispatchSemaphore(value: 0) let task = session.dataTask(with: request) { data, _, error in defer { sem.signal() } diff --git a/Foundation/Source/Sources/Network/Request/Request+User.swift b/Foundation/Source/Sources/Network/Request/Request+User.swift index fb930a8..9b98195 100644 --- a/Foundation/Source/Sources/Network/Request/Request+User.swift +++ b/Foundation/Source/Sources/Network/Request/Request+User.swift @@ -257,4 +257,11 @@ public extension Network { } return decodeRequest(with: responseData) ?? [] } + + func requestForReportAbuse(userId: String, comment: String) { + var request = prepareRequest(for: .users_report_abuse) + injectBodyForPost(for: &request, with: ["userId": userId]) + injectBodyForPost(for: &request, with: ["comment": comment]) + makeRequest(with: request) { _ in } + } } diff --git a/Foundation/Source/Sources/Source/LoginChallenge/LoginChallenge.swift b/Foundation/Source/Sources/Source/LoginChallenge/LoginChallenge.swift index 785cfcd..5f10bbb 100644 --- a/Foundation/Source/Sources/Source/LoginChallenge/LoginChallenge.swift +++ b/Foundation/Source/Sources/Source/LoginChallenge/LoginChallenge.swift @@ -92,6 +92,8 @@ public struct LoginChallenge { timeoutInterval: 10 ) checkerRequest.httpMethod = "POST" + checkerRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + checkerRequest.httpBody = "{}".data(using: .utf8) requestRecipeCheck = checkerRequest } diff --git a/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift b/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift index fede6e5..7b0e829 100644 --- a/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift +++ b/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift @@ -187,4 +187,13 @@ public extension Source.NetworkWrapper { let check = requestNoteState(withID: note) return !check.isFavorited ? check : nil } + + func requestForReportAbuse(note: NoteID) { + guard let ctx else { return } + guard let note = ctx.notes.retain(note) else { return } + guard let link = note.url ?? URL(string: "https://\(ctx.user.host)/notes/\(note.noteId)") + else { return } + let comment = "Note: \(link.absoluteString)\n-----\n" + ctx.network.requestForReportAbuse(userId: note.userId, comment: comment) + } } diff --git a/Foundation/Source/Sources/Source/Network/Users/NetworkWrapper+Users.swift b/Foundation/Source/Sources/Source/Network/Users/NetworkWrapper+Users.swift index c4262fa..24d89ac 100644 --- a/Foundation/Source/Sources/Source/Network/Users/NetworkWrapper+Users.swift +++ b/Foundation/Source/Sources/Source/Network/Users/NetworkWrapper+Users.swift @@ -111,4 +111,9 @@ public extension Source.NetworkWrapper { .converting($0, defaultHost: ctx.user.host) } } + + func requestReportUser(userId: String) { + guard let ctx else { return } + ctx.network.requestForReportAbuse(userId: userId, comment: "The user was reported by \(ctx.user.absoluteUsername)") + } } diff --git a/Foundation/Source/Tests/SourceTest/SourceTest+Meta.swift b/Foundation/Source/Tests/SourceTest/SourceTest+Meta.swift index 35ee8cb..cda1771 100644 --- a/Foundation/Source/Tests/SourceTest/SourceTest+Meta.swift +++ b/Foundation/Source/Tests/SourceTest/SourceTest+Meta.swift @@ -61,6 +61,7 @@ private extension Network.RequestTarget { case .following_invalidate: return SourceTest.test_204_api_following.self case .users: return SourceTest.test_204_api_following.self + case .users_report_abuse: return SourceTest.test_215_report_abuse.self case .user_show: return SourceTest.test_204_api_following.self case .users_followers: return SourceTest.test_204_api_following.self case .users_following: return SourceTest.test_204_api_following.self @@ -92,11 +93,11 @@ private extension Network.RequestTarget { case .notes_polls_vote: return SourceTest.test_212_notes_polls_vote.self case .notes_search: return SourceTest.test_211_notes_search.self - case .hashtags_trend: return SourceTest.test_212_hashtag_trand.self + case .hashtags_trend: return SourceTest.test_213_hashtag_trand.self - case .drive_files: return SourceTest.test_213_drive_file.self - case .drive_files_create: return SourceTest.test_213_drive_file.self - case .drive_files_update: return SourceTest.test_213_drive_file.self + case .drive_files: return SourceTest.test_214_drive_file.self + case .drive_files_create: return SourceTest.test_214_drive_file.self + case .drive_files_update: return SourceTest.test_214_drive_file.self // @unknown default: return nil } diff --git a/Foundation/Source/Tests/SourceTest/SourceTest.swift b/Foundation/Source/Tests/SourceTest/SourceTest.swift index f5a69e6..eb3ce0a 100644 --- a/Foundation/Source/Tests/SourceTest/SourceTest.swift +++ b/Foundation/Source/Tests/SourceTest/SourceTest.swift @@ -111,14 +111,18 @@ class SourceTest: XCTestCase { checkApi_NotesPollVote() } - func test_212_hashtag_trand() { + func test_213_hashtag_trand() { checkApi_Trend() } - func test_213_drive_file() { + func test_214_drive_file() { checkApi_DriveFile() } + func test_215_report_abuse() { + checkApi_UserReportAbuse() + } + // MARK: - Tear Down override class func tearDown() { diff --git a/Foundation/Source/Tests/SourceTest/Tests/User.swift b/Foundation/Source/Tests/SourceTest/Tests/User.swift index 09c93e1..9e48859 100644 --- a/Foundation/Source/Tests/SourceTest/Tests/User.swift +++ b/Foundation/Source/Tests/SourceTest/Tests/User.swift @@ -248,4 +248,38 @@ extension SourceTest { } } } + + func checkApi_UserReportAbuse() { + var user2: UserProfile? + dispatchAndWait { + let ans = source.network.requestForUserDetails(userIdOrName: "@test2") + unwrapOrFail(ans.result) { + user2 = .converting($0, defaultHost: "localhost") + } + } + guard let user2 else { + XCTFail("unable to locate test users") + return + } + dispatchAndWait { + let comment = UUID().uuidString + source.network.requestForReportAbuse(userId: user2.userId, comment: comment) + let url = source.network.base + .appendingPathComponent("api") + .appendingPathComponent("admin") + .appendingPathComponent("abuse-user-reports") + requestAndWait( + url: url.absoluteString, + allowFailure: false, + data: "{\"i\":\"\(source.receipt.token)\"}".data(using: .utf8), + method: "POST" + ) { data in + guard let data, let str = String(data: data, encoding: .utf8) else { + XCTFail("failed to get json str") + return + } + XCTAssert(str.contains(comment)) + } + } + } } diff --git a/Kimis.xcodeproj/project.pbxproj b/Kimis.xcodeproj/project.pbxproj index 4480f44..c19ab6f 100644 --- a/Kimis.xcodeproj/project.pbxproj +++ b/Kimis.xcodeproj/project.pbxproj @@ -1861,7 +1861,7 @@ CODE_SIGN_ENTITLEMENTS = Kimis/Kimis.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 6CMYQQFFT8; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1879,7 +1879,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.kimis.inhouse; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1900,7 +1900,7 @@ CODE_SIGN_ENTITLEMENTS = Kimis/Kimis.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 70; DEVELOPMENT_TEAM = 6CMYQQFFT8; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1918,7 +1918,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = as.wiki.qaq.kimis; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+More.swift b/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+More.swift index d6fe1ad..601cb47 100644 --- a/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+More.swift +++ b/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+More.swift @@ -98,6 +98,27 @@ extension NoteOperationStrip { }, ]) } + if note.userId != source.user.userId { + actions.append([ + UIAction(title: "Report Abuse", image: .init(systemName: "exclamationmark.bubble"), attributes: .destructive) { [weak self] _ in + let alert = UIAlertController(title: "⚠️", message: "Are you sure you want to report this note?", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Report", style: .destructive, handler: { _ in + alert.dismiss(animated: true) { + let alert = UIAlertController(title: "⏳", message: "Sending Request", preferredStyle: .alert) + DispatchQueue.global().async { + defer { withMainActor { + alert.dismiss(animated: true) + } } + source.req.requestForReportAbuse(note: note.noteId) + } + self?.anchor?.parentViewController?.present(alert, animated: true) + } + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + self?.anchor?.parentViewController?.present(alert, animated: true) + }, + ]) + } if note.userId == source.user.userId { actions.append([ UIAction(title: "Delete", image: .init(systemName: "trash"), attributes: .destructive) { [weak self] _ in diff --git a/Kimis/Interface/Controller/SafariController/MisskeySafariController.swift b/Kimis/Interface/Controller/SafariController/MisskeySafariController.swift index 55c2334..f49cde1 100644 --- a/Kimis/Interface/Controller/SafariController/MisskeySafariController.swift +++ b/Kimis/Interface/Controller/SafariController/MisskeySafariController.swift @@ -20,13 +20,42 @@ class MisskeySafariController: ViewController, WKNavigationDelegate { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() let contentController = WKUserContentController() - let js = "localStorage['account'] = JSON.stringify({'token' : '\(source?.receipt.token ?? "")'})" - let userScript = WKUserScript( - source: js, + + let tokenInjector = """ + localStorage['account'] = JSON.stringify({ + 'token' : '\(source?.receipt.token ?? "")' + }) + """ + let tokenInjectorScript = WKUserScript( + source: tokenInjector, injectionTime: .atDocumentStart, forMainFrameOnly: false ) - contentController.addUserScript(userScript) + contentController.addUserScript(tokenInjectorScript) + + let langOverride = """ + Object.defineProperties(Navigator.prototype, { + language: { + value: 'en', + configurable: false, + enumerable: true, + writable: false + }, + languages: { + value: ['en'], + configurable: false, + enumerable: true, + writable: false + } + }); + """ + let langOverrideScript = WKUserScript( + source: langOverride, + injectionTime: .atDocumentStart, + forMainFrameOnly: false + ) + contentController.addUserScript(langOverrideScript) + config.userContentController = contentController let cookie = HTTPCookie(properties: [ .domain: source?.receipt.host ?? "localhost", diff --git a/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController+ProfileButton.swift b/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController+ProfileButton.swift index 48f996b..2163345 100644 --- a/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController+ProfileButton.swift +++ b/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController+ProfileButton.swift @@ -367,6 +367,25 @@ extension UserViewController.ProfileView.ProfileButton { } qualification: { source, profile in profile.isFollowed && profile.absoluteUsername.lowercased() != source.user.absoluteUsername.lowercased() }, + UserMenuAction(title: "Report", image: "exclamationmark.bubble", attributes: [.destructive]) { source, profile, anchor in + let name = TextParser().trimToPlainText(from: profile.name) + let alert = UIAlertController(title: "⚠️", message: "Are you sure you want to report \(name)?", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Report", style: .destructive, handler: { _ in + let progress = UIAlertController(title: "⏳", message: "Sending Request", preferredStyle: .alert) + anchor.parentViewController?.present(progress, animated: true) + DispatchQueue.global().async { + defer { withMainActor { + progress.dismiss(animated: true) + UserViewController.reload(userId: profile.userId) + }} + source.req.requestReportUser(userId: profile.userId) + } + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + anchor.parentViewController?.present(alert, animated: true) + } qualification: { source, profile in + profile.absoluteUsername.lowercased() != source.user.absoluteUsername.lowercased() + }, ], ] diff --git a/Resource/ApiTest/Docker-Env/wait.sh b/Resource/ApiTest/Docker-Env/wait.sh index 7464c0b..5da8490 100755 --- a/Resource/ApiTest/Docker-Env/wait.sh +++ b/Resource/ApiTest/Docker-Env/wait.sh @@ -8,6 +8,7 @@ ENDPOINT="http://127.0.0.1:3555/" echo "[+] waiting for docker container to be ready" echo "[+] check $ENDPOINT for the status" +COUNT=0 while true; do STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" $ENDPOINT) if [ $STATUS_CODE -eq 200 ]; then @@ -16,4 +17,9 @@ while true; do fi echo "[i] docker container is not ready $STATUS_CODE, retry in 10s" sleep 10 + COUNT=$((COUNT+1)) + if [ $COUNT -eq 18 ]; then + echo "[!] docker container is not ready after 3 min, exit" + exit 1 + fi done \ No newline at end of file