mirror of https://github.com/lukechilds/damus.git
27 changed files with 1219 additions and 369 deletions
@ -0,0 +1,25 @@ |
|||
// |
|||
// FollowNotify.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-05-24. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
|
|||
enum FollowTarget { |
|||
case pubkey(String) |
|||
case contact(NostrEvent) |
|||
|
|||
var pubkey: String { |
|||
switch self { |
|||
case .pubkey(let pk): |
|||
return pk |
|||
case .contact(let ev): |
|||
return ev.pubkey |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
@ -0,0 +1,322 @@ |
|||
// |
|||
// HomeModel.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-05-24. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
|
|||
class HomeModel: ObservableObject { |
|||
var damus_state: DamusState |
|||
|
|||
var has_events: Set<String> = Set() |
|||
var last_event_of_kind: [String: [Int: NostrEvent]] = [:] |
|||
var done_init: Bool = false |
|||
|
|||
let damus_home_subid = UUID().description |
|||
let damus_contacts_subid = UUID().description |
|||
let damus_init_subid = UUID().description |
|||
|
|||
@Published var new_notifications: Bool = false |
|||
@Published var notifications: [NostrEvent] = [] |
|||
@Published var events: [NostrEvent] = [] |
|||
@Published var signal: SignalModel = SignalModel() |
|||
|
|||
init() { |
|||
self.damus_state = DamusState.empty |
|||
} |
|||
|
|||
init(damus_state: DamusState) { |
|||
self.damus_state = damus_state |
|||
} |
|||
|
|||
var pool: RelayPool { |
|||
return damus_state.pool |
|||
} |
|||
|
|||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) { |
|||
if has_events.contains(ev.id) { |
|||
return |
|||
} |
|||
|
|||
has_events.insert(ev.id) |
|||
let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind) |
|||
if last_k == nil || ev.created_at > last_k!.created_at { |
|||
last_event_of_kind[relay_id]?[ev.kind] = ev |
|||
} |
|||
if ev.kind == 1 { |
|||
handle_text_event(ev) |
|||
} else if ev.kind == 0 { |
|||
handle_metadata_event(ev) |
|||
} else if ev.kind == 6 { |
|||
handle_boost_event(ev) |
|||
} else if ev.kind == 7 { |
|||
handle_like_event(ev) |
|||
} else if ev.kind == 3 { |
|||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) |
|||
} |
|||
} |
|||
|
|||
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) { |
|||
load_our_contacts(contacts: self.damus_state.contacts, our_pubkey: self.damus_state.pubkey, ev: ev) |
|||
|
|||
if sub_id == damus_init_subid { |
|||
pool.send(.unsubscribe(damus_init_subid), to: [relay_id]) |
|||
if !done_init { |
|||
done_init = true |
|||
send_home_filters(relay_id: nil) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func handle_boost_event(_ ev: NostrEvent) { |
|||
var boost_ev_id = ev.last_refid()?.ref_id |
|||
|
|||
// CHECK SIGS ON THESE |
|||
if let inner_ev = ev.inner_event { |
|||
boost_ev_id = inner_ev.id |
|||
|
|||
if inner_ev.kind == 1 { |
|||
handle_text_event(ev) |
|||
} |
|||
} |
|||
|
|||
guard let e = boost_ev_id else { |
|||
return |
|||
} |
|||
|
|||
switch self.damus_state.boosts.add_event(ev, target: e) { |
|||
case .already_counted: |
|||
break |
|||
case .success(let n): |
|||
let boosted = Counted(event: ev, id: e, total: n) |
|||
notify(.boosted, boosted) |
|||
} |
|||
} |
|||
|
|||
func handle_like_event(_ ev: NostrEvent) { |
|||
guard let e = ev.last_refid() else { |
|||
// no id ref? invalid like event |
|||
return |
|||
} |
|||
|
|||
// CHECK SIGS ON THESE |
|||
|
|||
switch damus_state.likes.add_event(ev, target: e.ref_id) { |
|||
case .already_counted: |
|||
break |
|||
case .success(let n): |
|||
let liked = Counted(event: ev, id: e.ref_id, total: n) |
|||
notify(.liked, liked) |
|||
} |
|||
} |
|||
|
|||
|
|||
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { |
|||
switch conn_event { |
|||
case .ws_event(let ev): |
|||
|
|||
/* |
|||
if let wsev = ws_nostr_event(relay: relay_id, ev: ev) { |
|||
wsev.flags |= 1 |
|||
self.events.insert(wsev, at: 0) |
|||
} |
|||
*/ |
|||
|
|||
|
|||
switch ev { |
|||
case .connected: |
|||
if !done_init { |
|||
send_initial_filters(relay_id: relay_id) |
|||
} else { |
|||
send_home_filters(relay_id: relay_id) |
|||
} |
|||
case .error(let merr): |
|||
let desc = merr.debugDescription |
|||
if desc.contains("Software caused connection abort") { |
|||
pool.reconnect(to: [relay_id]) |
|||
} |
|||
case .disconnected: fallthrough |
|||
case .cancelled: |
|||
pool.reconnect(to: [relay_id]) |
|||
case .reconnectSuggested(let t): |
|||
if t { |
|||
pool.reconnect(to: [relay_id]) |
|||
} |
|||
default: |
|||
break |
|||
} |
|||
|
|||
update_signal_from_pool(signal: self.signal, pool: self.pool) |
|||
|
|||
print("ws_event \(ev)") |
|||
|
|||
case .nostr_event(let ev): |
|||
switch ev { |
|||
case .event(let sub_id, let ev): |
|||
// globally handle likes |
|||
let always_process = sub_id == damus_contacts_subid || sub_id == damus_home_subid || sub_id == damus_init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata |
|||
if !always_process { |
|||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we? |
|||
return |
|||
} |
|||
|
|||
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev) |
|||
case .notice(let msg): |
|||
//self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0) |
|||
print(msg) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
/// Send the initial filters, just our contact list mostly |
|||
func send_initial_filters(relay_id: String) { |
|||
var filter = NostrFilter.filter_contacts |
|||
filter.authors = [self.damus_state.pubkey] |
|||
filter.limit = 1 |
|||
|
|||
pool.send(.subscribe(.init(filters: [filter], sub_id: damus_init_subid)), to: [relay_id]) |
|||
} |
|||
|
|||
func send_home_filters(relay_id: String?) { |
|||
// TODO: since times should be based on events from a specific relay |
|||
// perhaps we could mark this in the relay pool somehow |
|||
|
|||
var friends = damus_state.contacts.get_friend_list() |
|||
friends.append(damus_state.pubkey) |
|||
|
|||
var contacts_filter = NostrFilter.filter_kinds([0,3]) |
|||
contacts_filter.authors = damus_state.contacts.get_friendosphere() |
|||
|
|||
// TODO: separate likes? |
|||
var home_filter = NostrFilter.filter_kinds([ |
|||
NostrKind.text.rawValue, |
|||
NostrKind.like.rawValue, |
|||
NostrKind.boost.rawValue, |
|||
]) |
|||
// include our pubkey as well even if we're not technically a friend |
|||
home_filter.authors = friends |
|||
home_filter.limit = 1000 |
|||
|
|||
var home_filters = [home_filter] |
|||
var contacts_filters = [contacts_filter] |
|||
let last_of_k = relay_id.flatMap { last_event_of_kind[$0] } ?? [:] |
|||
home_filters = update_filters_with_since(last_of_kind: last_of_k, filters: home_filters) |
|||
contacts_filters = update_filters_with_since(last_of_kind: last_of_k, filters: contacts_filters) |
|||
|
|||
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters]) |
|||
|
|||
if let relay_id = relay_id { |
|||
pool.send(.subscribe(.init(filters: home_filters, sub_id: damus_home_subid)), to: [relay_id]) |
|||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: damus_contacts_subid)), to: [relay_id]) |
|||
} else { |
|||
pool.send(.subscribe(.init(filters: home_filters, sub_id: damus_home_subid))) |
|||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: damus_contacts_subid))) |
|||
} |
|||
} |
|||
|
|||
func handle_metadata_event(_ ev: NostrEvent) { |
|||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { |
|||
return |
|||
} |
|||
|
|||
if let mprof = damus_state.profiles.lookup_with_timestamp(id: ev.pubkey) { |
|||
if mprof.timestamp > ev.created_at { |
|||
// skip if we already have an newer profile |
|||
return |
|||
} |
|||
} |
|||
|
|||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at) |
|||
damus_state.profiles.add(id: ev.pubkey, profile: tprof) |
|||
|
|||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) |
|||
} |
|||
|
|||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { |
|||
guard let m = last_event_of_kind[relay_id] else { |
|||
last_event_of_kind[relay_id] = [:] |
|||
return nil |
|||
} |
|||
|
|||
return m[kind] |
|||
} |
|||
|
|||
func handle_notification(ev: NostrEvent) { |
|||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) { |
|||
return |
|||
} |
|||
|
|||
let last_notified = get_last_notified() |
|||
|
|||
if last_notified == nil || last_notified!.created_at < ev.created_at { |
|||
save_last_notified(ev) |
|||
new_notifications = true |
|||
} |
|||
} |
|||
|
|||
func insert_home_event(_ ev: NostrEvent) -> Bool { |
|||
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at }) |
|||
return ok |
|||
} |
|||
|
|||
func should_hide_event(_ ev: NostrEvent) -> Bool { |
|||
return false |
|||
} |
|||
|
|||
func handle_text_event(_ ev: NostrEvent) { |
|||
if should_hide_event(ev) { |
|||
return |
|||
} |
|||
|
|||
let _ = insert_home_event(ev) |
|||
|
|||
if is_notification(ev: ev, pubkey: self.damus_state.pubkey) { |
|||
handle_notification(ev: ev) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
func update_signal_from_pool(signal: SignalModel, pool: RelayPool) { |
|||
if signal.max_signal != pool.relays.count { |
|||
signal.max_signal = pool.relays.count |
|||
} |
|||
|
|||
if signal.signal != pool.num_connecting { |
|||
signal.signal = signal.max_signal - pool.num_connecting |
|||
} |
|||
} |
|||
|
|||
|
|||
func load_our_contacts(contacts: Contacts, our_pubkey: String, ev: NostrEvent) { |
|||
if ev.pubkey != our_pubkey { |
|||
return |
|||
} |
|||
|
|||
contacts.event = ev |
|||
|
|||
// our contacts |
|||
for tag in ev.tags { |
|||
if tag.count > 1 && tag[0] == "p" { |
|||
// TODO: validate pubkey? |
|||
contacts.add_friend_pubkey(tag[1]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) { |
|||
let relays = relay_id ?? "relays" |
|||
print("connected to \(relays) with filters:") |
|||
for group in groups { |
|||
for filter in group { |
|||
print(filter) |
|||
} |
|||
} |
|||
print("-----") |
|||
} |
@ -0,0 +1,32 @@ |
|||
// |
|||
// SignalModel.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-05-24. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
|
|||
class SignalModel: ObservableObject { |
|||
@Published var signal: Int |
|||
@Published var max_signal: Int |
|||
|
|||
var percentage: Double { |
|||
if max_signal == 0 { |
|||
return 0 |
|||
} |
|||
|
|||
return Double(signal) / Double(max_signal) |
|||
} |
|||
|
|||
init() { |
|||
self.signal = 0 |
|||
self.max_signal = 0 |
|||
} |
|||
|
|||
init(signal: Int, max_signal: Int) { |
|||
self.signal = signal |
|||
self.max_signal = max_signal |
|||
} |
|||
} |
@ -0,0 +1,207 @@ |
|||
// |
|||
// Bech32.swift |
|||
// |
|||
// Modified by William Casarin in 2022 |
|||
// Created by Evolution Group Ltd on 12.02.2018. |
|||
// Copyright © 2018 Evolution Group Ltd. All rights reserved. |
|||
// |
|||
// Base32 address format for native v0-16 witness outputs implementation |
|||
// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki |
|||
// Inspired by Pieter Wuille C++ implementation |
|||
import Foundation |
|||
|
|||
/// Bech32 checksum implementation |
|||
fileprivate let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] |
|||
/// Bech32 checksum delimiter |
|||
fileprivate let checksumMarker: String = "1" |
|||
/// Bech32 character set for encoding |
|||
fileprivate let encCharset: Data = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".data(using: .utf8)! |
|||
/// Bech32 character set for decoding |
|||
fileprivate let decCharset: [Int8] = [ |
|||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
|||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
|||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
|||
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, |
|||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, |
|||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, |
|||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, |
|||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 |
|||
] |
|||
|
|||
/// Find the polynomial with value coefficients mod the generator as 30-bit. |
|||
public func bech32_polymod(_ values: Data) -> UInt32 { |
|||
var chk: UInt32 = 1 |
|||
for v in values { |
|||
let top = (chk >> 25) |
|||
chk = (chk & 0x1ffffff) << 5 ^ UInt32(v) |
|||
for i: UInt8 in 0..<5 { |
|||
chk ^= ((top >> i) & 1) == 0 ? 0 : gen[Int(i)] |
|||
} |
|||
} |
|||
return chk |
|||
} |
|||
|
|||
|
|||
/// Expand a HRP for use in checksum computation. |
|||
func bech32_expand_hrp(_ s: String) -> Data { |
|||
var left: [UInt8] = [] |
|||
var right: [UInt8] = [] |
|||
for x in Array(s) { |
|||
let scalars = String(x).unicodeScalars |
|||
left.append(UInt8(scalars[scalars.startIndex].value) >> 5) |
|||
right.append(UInt8(scalars[scalars.startIndex].value) & 31) |
|||
} |
|||
return Data(left + [0] + right) |
|||
} |
|||
|
|||
/// Verify checksum |
|||
public func bech32_verify(hrp: String, checksum: Data) -> Bool { |
|||
var data = bech32_expand_hrp(hrp) |
|||
data.append(checksum) |
|||
return bech32_polymod(data) == 1 |
|||
} |
|||
|
|||
/// Create checksum |
|||
public func bech32_create_checksum(hrp: String, values: Data) -> Data { |
|||
var enc = bech32_expand_hrp(hrp) |
|||
enc.append(values) |
|||
enc.append(Data(repeating: 0x00, count: 6)) |
|||
let mod: UInt32 = bech32_polymod(enc) ^ 1 |
|||
var ret: Data = Data(repeating: 0x00, count: 6) |
|||
for i in 0..<6 { |
|||
ret[i] = UInt8((mod >> (5 * (5 - i))) & 31) |
|||
} |
|||
return ret |
|||
} |
|||
|
|||
public func bech32_encode(hrp: String, _ input: [UInt8]) -> String { |
|||
let table: [Character] = Array("qpzry9x8gf2tvdw0s3jn54khce6mua7l") |
|||
let bits = eightToFiveBits(input) |
|||
let check_sum = bech32_checksum(hrp: hrp, data: bits) |
|||
let separator = "1" |
|||
return "\(hrp)" + separator + String((bits + check_sum).map { table[Int($0)] }) |
|||
} |
|||
|
|||
func bech32_checksum(hrp: String, data: [UInt8]) -> [UInt8] { |
|||
let values = bech32_expand_hrp(hrp) + data |
|||
let polymod = bech32_polymod(values + [0,0,0,0,0,0]) ^ 1 |
|||
var result: [UInt32] = [] |
|||
for i in (0..<6) { |
|||
result.append((polymod >> (5 * (5 - UInt32(i)))) & 31) |
|||
} |
|||
return result.map { UInt8($0) } |
|||
} |
|||
|
|||
func eightToFiveBits(_ input: [UInt8]) -> [UInt8] { |
|||
guard !input.isEmpty else { return [] } |
|||
|
|||
var outputSize = (input.count * 8) / 5 |
|||
if ((input.count * 8) % 5) != 0 { |
|||
outputSize += 1 |
|||
} |
|||
var outputArray: [UInt8] = [] |
|||
for i in (0..<outputSize) { |
|||
let devision = (i * 5) / 8 |
|||
let reminder = (i * 5) % 8 |
|||
var element = input[devision] << reminder |
|||
element >>= 3 |
|||
|
|||
if (reminder > 3) && (i + 1 < outputSize) { |
|||
element = element | (input[devision + 1] >> (8 - reminder + 3)) |
|||
} |
|||
|
|||
outputArray.append(element) |
|||
} |
|||
|
|||
return outputArray |
|||
} |
|||
|
|||
/// Decode Bech32 string |
|||
public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data) { |
|||
guard let strBytes = str.data(using: .utf8) else { |
|||
throw Bech32Error.nonUTF8String |
|||
} |
|||
guard strBytes.count <= 90 else { |
|||
throw Bech32Error.stringLengthExceeded |
|||
} |
|||
var lower: Bool = false |
|||
var upper: Bool = false |
|||
for c in strBytes { |
|||
// printable range |
|||
if c < 33 || c > 126 { |
|||
throw Bech32Error.nonPrintableCharacter |
|||
} |
|||
// 'a' to 'z' |
|||
if c >= 97 && c <= 122 { |
|||
lower = true |
|||
} |
|||
// 'A' to 'Z' |
|||
if c >= 65 && c <= 90 { |
|||
upper = true |
|||
} |
|||
} |
|||
if lower && upper { |
|||
throw Bech32Error.invalidCase |
|||
} |
|||
guard let pos = str.range(of: checksumMarker, options: .backwards)?.lowerBound else { |
|||
throw Bech32Error.noChecksumMarker |
|||
} |
|||
let intPos: Int = str.distance(from: str.startIndex, to: pos) |
|||
guard intPos >= 1 else { |
|||
throw Bech32Error.incorrectHrpSize |
|||
} |
|||
guard intPos + 7 <= str.count else { |
|||
throw Bech32Error.incorrectChecksumSize |
|||
} |
|||
let vSize: Int = str.count - 1 - intPos |
|||
var values: Data = Data(repeating: 0x00, count: vSize) |
|||
for i in 0..<vSize { |
|||
let c = strBytes[i + intPos + 1] |
|||
let decInt = decCharset[Int(c)] |
|||
if decInt == -1 { |
|||
throw Bech32Error.invalidCharacter |
|||
} |
|||
values[i] = UInt8(decInt) |
|||
} |
|||
let hrp = String(str[..<pos]).lowercased() |
|||
guard bech32_verify(hrp: hrp, checksum: values) else { |
|||
throw Bech32Error.checksumMismatch |
|||
} |
|||
return (hrp, Data(values[..<(vSize-6)])) |
|||
} |
|||
|
|||
public enum Bech32Error: LocalizedError { |
|||
case nonUTF8String |
|||
case nonPrintableCharacter |
|||
case invalidCase |
|||
case noChecksumMarker |
|||
case incorrectHrpSize |
|||
case incorrectChecksumSize |
|||
case stringLengthExceeded |
|||
|
|||
case invalidCharacter |
|||
case checksumMismatch |
|||
|
|||
public var errorDescription: String? { |
|||
switch self { |
|||
case .checksumMismatch: |
|||
return "Checksum doesn't match" |
|||
case .incorrectChecksumSize: |
|||
return "Checksum size too low" |
|||
case .incorrectHrpSize: |
|||
return "Human-readable-part is too small or empty" |
|||
case .invalidCase: |
|||
return "String contains mixed case characters" |
|||
case .invalidCharacter: |
|||
return "Invalid character met on decoding" |
|||
case .noChecksumMarker: |
|||
return "Checksum delimiter not found" |
|||
case .nonPrintableCharacter: |
|||
return "Non printable character in input string" |
|||
case .nonUTF8String: |
|||
return "String cannot be decoded by utf8 decoder" |
|||
case .stringLengthExceeded: |
|||
return "Input string is too long" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,195 @@ |
|||
// |
|||
// LoginView.swift |
|||
// damus |
|||
// |
|||
// Created by William Casarin on 2022-05-22. |
|||
// |
|||
|
|||
import SwiftUI |
|||
|
|||
enum ParsedKey { |
|||
case pub(String) |
|||
case priv(String) |
|||
case hex(String) |
|||
|
|||
var is_pub: Bool { |
|||
if case .pub = self { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
var is_hex: Bool { |
|||
if case .hex = self { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
} |
|||
|
|||
struct LoginView: View { |
|||
@State var key: String = "" |
|||
@State var is_pubkey: Bool = false |
|||
@State var error: String? = nil |
|||
|
|||
func get_error(parsed_key: ParsedKey?) -> String? { |
|||
if self.error != nil { |
|||
return self.error |
|||
} |
|||
|
|||
if !key.isEmpty && parsed_key == nil { |
|||
return "Invalid key" |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
var body: some View { |
|||
ZStack(alignment: .top) { |
|||
DamusGradient() |
|||
VStack { |
|||
Text("Login") |
|||
.foregroundColor(.white) |
|||
.font(.title) |
|||
.padding() |
|||
|
|||
Text("Enter your account key to login:") |
|||
.foregroundColor(.white) |
|||
.padding() |
|||
|
|||
KeyInput("nsec1...", key: $key) |
|||
|
|||
let parsed = parse_key(key) |
|||
|
|||
if parsed?.is_hex ?? false { |
|||
Text("This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.") |
|||
.font(.subheadline.bold()) |
|||
.foregroundColor(.white) |
|||
PubkeySwitch(isOn: $is_pubkey) |
|||
.padding() |
|||
} |
|||
|
|||
if let error = get_error(parsed_key: parsed) { |
|||
Text(error) |
|||
.foregroundColor(.red) |
|||
.padding() |
|||
} |
|||
|
|||
if parsed?.is_pub ?? false { |
|||
Text("This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.") |
|||
.foregroundColor(.white) |
|||
.padding() |
|||
} |
|||
|
|||
if let p = parsed { |
|||
DamusWhiteButton("Login") { |
|||
if !process_login(p, is_pubkey: self.is_pubkey) { |
|||
self.error = "Invalid key" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.padding() |
|||
} |
|||
.navigationBarBackButtonHidden(true) |
|||
.navigationBarItems(leading: BackNav()) |
|||
} |
|||
} |
|||
|
|||
struct PubkeySwitch: View { |
|||
@Binding var isOn: Bool |
|||
var body: some View { |
|||
HStack { |
|||
Toggle(isOn: $isOn) { |
|||
Text("Public Key?") |
|||
.foregroundColor(.white) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func parse_key(_ thekey: String) -> ParsedKey? { |
|||
var key = thekey |
|||
if key.count > 0 && key.first! == "@" { |
|||
key = String(key.dropFirst()) |
|||
} |
|||
if hex_decode(key) != nil { |
|||
return .hex(key) |
|||
} |
|||
|
|||
if let bech_key = decode_bech32_key(key) { |
|||
switch bech_key { |
|||
case .pub(let pk): |
|||
return .pub(pk) |
|||
case .sec(let sec): |
|||
return .priv(sec) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func process_login(_ key: ParsedKey, is_pubkey: Bool) -> Bool { |
|||
switch key { |
|||
case .priv(let priv): |
|||
save_privkey(privkey: priv) |
|||
guard let pk = privkey_to_pubkey(privkey: priv) else { |
|||
return false |
|||
} |
|||
save_pubkey(pubkey: pk) |
|||
|
|||
case .pub(let pub): |
|||
clear_privkey() |
|||
save_pubkey(pubkey: pub) |
|||
|
|||
case .hex(let hexstr): |
|||
if is_pubkey { |
|||
clear_privkey() |
|||
save_pubkey(pubkey: hexstr) |
|||
} else { |
|||
save_privkey(privkey: hexstr) |
|||
guard let pk = privkey_to_pubkey(privkey: hexstr) else { |
|||
return false |
|||
} |
|||
save_pubkey(pubkey: pk) |
|||
} |
|||
} |
|||
|
|||
notify(.login, ()) |
|||
return true |
|||
} |
|||
|
|||
struct KeyInput: View { |
|||
let title: String |
|||
let key: Binding<String> |
|||
|
|||
init(_ title: String, key: Binding<String>) { |
|||
self.title = title |
|||
self.key = key |
|||
} |
|||
|
|||
var body: some View { |
|||
TextField("", text: key) |
|||
.placeholder(when: key.wrappedValue.isEmpty) { |
|||
Text(title).foregroundColor(.white.opacity(0.6)) |
|||
} |
|||
.padding() |
|||
.background { |
|||
RoundedRectangle(cornerRadius: 4.0).opacity(0.2) |
|||
} |
|||
.autocapitalization(.none) |
|||
.foregroundColor(.white) |
|||
.font(.body.monospaced()) |
|||
} |
|||
} |
|||
|
|||
struct LoginView_Previews: PreviewProvider { |
|||
static var previews: some View { |
|||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" |
|||
let bech32_pubkey = "KeyInput" |
|||
Group { |
|||
LoginView(key: pubkey) |
|||
LoginView(key: bech32_pubkey) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
// |
|||
// Bech32Tests.swift |
|||
// damusTests |
|||
// |
|||
// Created by William Casarin on 2022-05-22. |
|||
// |
|||
|
|||
import XCTest |
|||
@testable import damus |
|||
|
|||
class Bech32Tests: XCTestCase { |
|||
|
|||
override func setUpWithError() throws { |
|||
// Put setup code here. This method is called before the invocation of each test method in the class. |
|||
} |
|||
|
|||
override func tearDownWithError() throws { |
|||
// Put teardown code here. This method is called after the invocation of each test method in the class. |
|||
} |
|||
|
|||
func test_bech32_encode_decode() throws { |
|||
// This is an example of a functional test case. |
|||
// Use XCTAssert and related functions to verify your tests produce the correct results. |
|||
// Any test you write for XCTest can be annotated as throws and async. |
|||
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. |
|||
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. |
|||
|
|||
let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" |
|||
guard let b32_pubkey = bech32_pubkey(pubkey) else { |
|||
XCTAssert(false) |
|||
return |
|||
} |
|||
|
|||
guard let decoded = try? bech32_decode(b32_pubkey) else { |
|||
XCTAssert(false) |
|||
return |
|||
} |
|||
|
|||
let encoded = hex_encode(decoded.data) |
|||
|
|||
XCTAssertEqual(encoded, pubkey) |
|||
} |
|||
|
|||
func testPerformanceExample() throws { |
|||
// This is an example of a performance test case. |
|||
self.measure { |
|||
// Put the code you want to measure the time of here. |
|||
} |
|||
} |
|||
|
|||
} |
Loading…
Reference in new issue