import Foundation
import IrdP2PCrypto
/// The identity block a peer announces in its hello.
public struct IdentityBlock: Sendable, Equatable {
public var pub: String
public var sig: String
public var name: String?
public var hue: Int?
public var glyph: String?
var json: JSONValue {
var o = JSONObject([("pub", .string(pub)), ("sig", .string(sig))])
if let name { o["name"] = .string(name) }
if let hue { o["hue"] = .int(hue) }
if let glyph { o["glyph"] = .string(glyph) }
return .object(o)
}
init?(json: JSONValue?) {
guard let o = json?.objectValue, let pub = o["pub"]?.stringValue, let sig = o["sig"]?.stringValue else { return nil }
self.pub = pub
self.sig = sig
name = o["name"]?.stringValue
hue = o["hue"]?.intValue
glyph = o["glyph"]?.stringValue
}
init(pub: String, sig: String, name: String?, hue: Int?, glyph: String?) {
self.pub = pub; self.sig = sig; self.name = name; self.hue = hue; self.glyph = glyph
}
}
enum ChunkEncoding: String, Sendable { case json, bin }
/// The plaintexts inside sealed envelopes (PROTOCOL.md "Inner messages").
/// Unknown kinds and keys are ignored so the protocol can grow without a bump.
enum InnerMessage: Sendable, Equatable {
case hello(IdentityBlock?)
case offer(sdp: String)
case answer(sdp: String)
case ice(candidate: String?, sdpMid: String?, sdpMLineIndex: Int?)
/// The higher peer asks the offerer to restart ICE.
case restart
case msg(JSONValue)
case bin(Data)
case chunk(id: String, index: Int, count: Int, encoding: ChunkEncoding, data: Data)
case ping
case unknown(kind: String)
var json: JSONValue {
switch self {
case .hello(let id):
var o = JSONObject([("kind", .string("hello"))])
if let id { o["id"] = id.json }
return .object(o)
case .offer(let sdp): return .object(JSONObject([("kind", .string("offer")), ("sdp", .string(sdp))]))
case .answer(let sdp): return .object(JSONObject([("kind", .string("answer")), ("sdp", .string(sdp))]))
case .ice(let candidate, let mid, let index):
return .object(JSONObject([("kind", .string("ice")), ("candidate", candidate.map { .string($0) } ?? .null),
("sdpMid", mid.map { .string($0) } ?? .null), ("sdpMLineIndex", index.map { .int($0) } ?? .null)]))
case .restart: return .object(["kind": .string("restart")])
case .msg(let payload): return .object(JSONObject([("kind", .string("msg")), ("payload", payload)]))
case .bin(let data): return .object(JSONObject([("kind", .string("bin")), ("d", .string(Bytes.base64(data)))]))
case .chunk(let id, let index, let count, let enc, let data):
return .object(JSONObject([("kind", .string("chunk")), ("id", .string(id)), ("i", .int(index)), ("n", .int(count)),
("enc", .string(enc.rawValue)), ("d", .string(Bytes.base64(data)))]))
case .ping: return .object(["kind": .string("ping")])
case .unknown(let kind): return .object(["kind": .string(kind)])
}
}
/// `nil` when the value is not an inner message at all (not an object, no kind).
init?(json: JSONValue) {
guard let o = json.objectValue, let kind = o["kind"]?.stringValue else { return nil }
switch kind {
case "hello": self = .hello(IdentityBlock(json: o["id"]))
case "offer": guard let sdp = o["sdp"]?.stringValue else { return nil }; self = .offer(sdp: sdp)
case "answer": guard let sdp = o["sdp"]?.stringValue else { return nil }; self = .answer(sdp: sdp)
case "ice": self = .ice(candidate: o["candidate"]?.stringValue, sdpMid: o["sdpMid"]?.stringValue, sdpMLineIndex: o["sdpMLineIndex"]?.intValue)
case "restart": self = .restart
case "msg": guard let p = o["payload"] else { return nil }; self = .msg(p)
case "bin": guard let d = o["d"]?.stringValue, let data = Bytes.fromBase64(d) else { return nil }; self = .bin(data)
case "chunk":
guard let id = o["id"]?.stringValue, let i = o["i"]?.intValue, let n = o["n"]?.intValue,
let enc = o["enc"]?.stringValue.flatMap(ChunkEncoding.init(rawValue:)),
let d = o["d"]?.stringValue, let data = Bytes.fromBase64(d), i >= 0, n > 0, i < n
else { return nil }
self = .chunk(id: id, index: i, count: n, encoding: enc, data: data)
case "ping": self = .ping
default: self = .unknown(kind: kind)
}
}
}