diff --git a/Threaded/Data/Instance.swift b/Threaded/Data/Instance.swift index 7e2f195..5abd85c 100644 --- a/Threaded/Data/Instance.swift +++ b/Threaded/Data/Instance.swift @@ -3,6 +3,45 @@ import Foundation public struct Instance: Codable, Sendable { + static let blocklistUrl: URL? = URL(string: "https://codeberg.org/oliphant/blocklists/raw/branch/main/blocklists/_unified_tier2_blocklist.csv") + + @MainActor + static func getBlocklist() -> [String] { + var final: [String] = [] + //locate the file you want to use + guard let filelink = Instance.blocklistUrl else { + return [] + } + + //convert that file into one long string + var data = "" + do { + data = try String(contentsOf: filelink) + } catch { + print(error) + return [] + } + + //now split that string into an array of "rows" of data. Each row is a string. + var rows = data.components(separatedBy: "\n") + + //if you have a header row, remove it here + rows.removeFirst() + + //now loop around each row, and split it into each of its columns + for row in rows { + let columns = row.components(separatedBy: ",") + + //check that we have enough columns + if columns.count > 0 { + let instanceUrl = columns[0] + final.append(instanceUrl) + } + } + + return final + } + public struct Stats: Codable, Sendable { public let userCount: Int public let statusCount: Int diff --git a/Threaded/Localizable.xcstrings b/Threaded/Localizable.xcstrings index 6053454..fa52ed4 100644 --- a/Threaded/Localizable.xcstrings +++ b/Threaded/Localizable.xcstrings @@ -383,6 +383,22 @@ } } }, + "activity.messages.read" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mark as read" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Marquer comme lu" + } + } + } + }, "activity.no-notifications" : { "localizations" : { "en" : { @@ -743,6 +759,70 @@ } } }, + "login.instance.unsafe" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This instance is dangerous" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette instance est dangereuse" + } + } + } + }, + "login.instance.unsafe.agree" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "I agree" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je suis d'accord" + } + } + } + }, + "login.instance.unsafe.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The instance has been detected to be dangerous, Threaded does not take responsibility for any content posted or seen on this instance and will not provide support. Do you agree to take FULL responsibility?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette instance a été détectée comme étant dangereuse, Threaded ne prend pas la responsabilité pour tout contenu posté ou vu dans cette instance et ne fournira pas d'aide. Êtes-vous d'accord pour prendre la TOTALE responsabilité ?" + } + } + } + }, + "login.instance.unsafe.disagree" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "I disagree" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je suis pas d'accord" + } + } + } + }, "login.mastodon" : { "localizations" : { "en" : { @@ -1479,6 +1559,70 @@ } } }, + "status.action.bookmark" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bookmark" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Placer un signet" + } + } + } + }, + "status.action.like" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Like" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aimer" + } + } + } + }, + "status.action.unbookmark" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbookmark" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enlever le signet" + } + } + } + }, + "status.action.unlike" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unlike" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plus aimer" + } + } + } + }, "status.editing" : { "localizations" : { "en" : { diff --git a/Threaded/Views/AddInstanceView.swift b/Threaded/Views/AddInstanceView.swift index 088530d..57fce7b 100644 --- a/Threaded/Views/AddInstanceView.swift +++ b/Threaded/Views/AddInstanceView.swift @@ -9,9 +9,16 @@ struct AddInstanceView: View { // Instance URL and verify @State private var instanceUrl: String = "" + @State private var verifying: Bool = false @State private var verified: Bool = false @State private var verifyError: Bool = false + + @State private var blockList: [String] = [] + @State private var responsability: Bool = false + @State private var showingResponsability: Bool = false + @State private var agreedResponsability: Bool = false + @State private var instanceInfo: Instance? @State private var signInClient: Client? @@ -87,6 +94,36 @@ struct AddInstanceView: View { } } } + .task { + withAnimation { + verifying = true + } + + blockList = Instance.getBlocklist() + + withAnimation { + verifying = false + } + } + .alert("login.instance.unsafe", isPresented: $showingResponsability, actions: { + Button(role: .destructive) { + responsability = true + agreedResponsability = true + showingResponsability.toggle() + } label: { + Text("login.instance.unsafe.agree") + } + + Button(role: .cancel) { + responsability = true + agreedResponsability = false + showingResponsability.toggle() + } label: { + Text("login.instance.unsafe.disagree") + } + }, message: { + Text("login.instance.unsafe.description") + }) .scrollContentBackground(.hidden) .background(Color.appBackground) .onChange(of: instanceUrl) { _, newValue in @@ -106,6 +143,38 @@ struct AddInstanceView: View { .replacingOccurrences(of: "http://", with: "") .replacingOccurrences(of: "https://", with: "") + if !isInstanceSafe() { + if responsability == false && agreedResponsability == false { + responsability = true + agreedResponsability = false + showingResponsability = true + + withAnimation { + verifying = false + verified = false + verifyError = false + } + + return + } else if responsability == true && agreedResponsability == true { + showingResponsability = false + } else if responsability == true && agreedResponsability == false { + showingResponsability = true + + withAnimation { + verifying = false + verified = false + verifyError = false + } + + return + } + } else { + responsability = false + agreedResponsability = false + UserDefaults.standard.removeObject(forKey: "unsafe") + } + let client = Client(server: cleanInstance) Task { @@ -147,6 +216,12 @@ struct AddInstanceView: View { return } + if agreedResponsability && responsability { + UserDefaults.standard.setValue(true, forKey: "unsafe") + } else { + UserDefaults.standard.removeObject(forKey: "unsafe") + } + do { let oauthToken = try await client.continueOauthFlow(url: url) let client = Client(server: client.server, oauthToken: oauthToken) @@ -163,6 +238,13 @@ struct AddInstanceView: View { print(error) } } + + /// Is the user input instance URL a safe instance + /// - returns: True, if the instance isn't consider as dangerous + private func isInstanceSafe() -> Bool { + let unsafe = blockList.contains(instanceUrl.trimmingCharacters(in: .whitespacesAndNewlines)) + return !unsafe + } } #Preview {