aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-11-30 23:46:57 -0800
committerDustin Mierau <dustin@mierau.me>2023-11-30 23:46:57 -0800
commit885119acdd27bd53ed3096285c3eca3e2d99ad70 (patch)
tree7dce98c071859684ff2aaa157f090b7cdc941a04
parent30255476781789df318a571622ab732681696139 (diff)
Bunch of work to and now login, user list, handling replies, etc. works. Or at least the framework is there for them to work.
-rw-r--r--Hotline/Network/HotlineClient.swift179
-rw-r--r--Hotline/Network/HotlineProtocol.swift234
-rw-r--r--Hotline/TrackerView.swift150
-rw-r--r--Hotline/Utility/DataExtensions.swift20
4 files changed, 382 insertions, 201 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift
index 5193fe5..b5092a0 100644
--- a/Hotline/Network/HotlineClient.swift
+++ b/Hotline/Network/HotlineClient.swift
@@ -24,12 +24,14 @@ class HotlineClient : ObservableObject {
@Published var userList: [HotlineUser] = []
@Published var chatMessages: [String] = []
- let userName: String = "bolt"
- let userIconID: UInt32 = 128
-
+ var userName: String = "bolt"
+ var userIconID: UInt16 = 128
+ var serverVersion: UInt16 = 151
var server: HotlineServer?
var connection: NWConnection?
+ private var transactionLog: [UInt32:HotlineTransactionType] = [:]
+
init() {
}
@@ -94,6 +96,10 @@ class HotlineClient : ObservableObject {
return
}
+ print("HotlineClient => \(t.id) \(t.type)")
+
+ self.transactionLog[t.id] = t.type
+
c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in
if disconnectOnError, error != nil {
self?.disconnect()
@@ -110,7 +116,7 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: waiting for transaction...")
+ print("HotlineClient ⏳")
c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in
guard let self = self else {
return
@@ -127,39 +133,40 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: received \(headerData.count) header bytes")
+// print("HotlineClient: received \(headerData.count) header bytes")
if var transaction = self.parseTransaction(data: headerData) {
// Receive additional data if the transaction has data attached to it.
print("DATA SIZE: \(transaction.dataSize)")
if transaction.dataSize > 0 {
- c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (parameterData, context, isComplete, error) in
+ c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (fieldData, context, isComplete, error) in
guard let self = self else {
return
}
- guard let parameterData = parameterData, !parameterData.isEmpty else {
- print("HotlineClient: transaction parameter data is empty!")
+ guard let fieldData = fieldData, !fieldData.isEmpty else {
+ print("HotlineClient: transaction field data is empty!")
self.disconnect()
return
}
- let parameterCount = parameterData.readUInt16(at: 0)!
+ let fieldCount = fieldData.readUInt16(at: 0)!
- if parameterCount > 0 {
+ if fieldCount > 0 {
var dataCursor = 2
- for _ in 0..<parameterCount {
+ for _ in 0..<fieldCount {
if
- let fieldID = parameterData.readUInt16(at: dataCursor),
- let fieldSize = parameterData.readUInt16(at: dataCursor + 2),
- let fieldData = parameterData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
+ let fieldID = fieldData.readUInt16(at: dataCursor),
+ let fieldSize = fieldData.readUInt16(at: dataCursor + 2),
+ let fieldRemainingData = fieldData.readData(at: dataCursor + 4, length: Int(fieldSize)) {
if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) {
- transaction.parameters.append(HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData))
-// transaction.parameters[fieldType] = HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData)
+
+ transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData))
+// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData)
}
else {
- print("HotlineClient: UNKNOWN PARAM TYPE!", fieldID, fieldSize)
+ print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize)
}
dataCursor += 4 + Int(fieldSize)
@@ -201,10 +208,12 @@ class HotlineClient : ObservableObject {
let transactionSize = data.readUInt32(at: 12),
let dataSize = data.readUInt32(at: 16) {
- print("HotlineClient: Parsing transaction type \(type) with data \(dataSize)")
if let transactionType = HotlineTransactionType(rawValue: type) {
return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize)
}
+ else {
+ print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes")
+ }
}
return nil
@@ -249,8 +258,11 @@ class HotlineClient : ObservableObject {
return
}
- print("HotlineClient: completed handshake")
- self.sendLogin()
+ print("HotlineClient 🤝")
+ self.sendLogin() { [weak self] in
+ self?.sendSetClientUserInfo()
+ self?.sendGetUserList()
+ }
self.receiveTransaction()
}
})
@@ -262,15 +274,13 @@ class HotlineClient : ObservableObject {
}
var t = HotlineTransaction(type: .login)
- t.setParameterEncodedString(type: .userLogin, val: "")
- t.setParameterEncodedString(type: .userPassword, val: "")
- t.setParameterUInt32(type: .userIconID, val: self.userIconID)
- t.setParameterString(type: .userName, val: self.userName)
- t.setParameterUInt32(type: .versionNumber, val: 151)
+ t.setFieldEncodedString(type: .userLogin, val: "")
+ t.setFieldEncodedString(type: .userPassword, val: "")
+ t.setFieldUInt16(type: .userIconID, val: self.userIconID)
+ t.setFieldString(type: .userName, val: self.userName)
+ t.setFieldUInt32(type: .versionNumber, val: 123)
- print("HotlineClient: logging in...")
self.sendTransaction(t) { [weak self] in
- print("HotlineClient: logged in!")
DispatchQueue.main.async {
self?.connectionStatus = .loggedIn
}
@@ -279,44 +289,100 @@ class HotlineClient : ObservableObject {
}
}
+ func sendSetClientUserInfo(callback: (() -> Void)? = nil) {
+ var t = HotlineTransaction(type: .setClientUserInfo)
+ t.setFieldString(type: .userName, val: self.userName)
+ t.setFieldUInt16(type: .userIconID, val: self.userIconID)
+
+ self.sendTransaction(t, callback: callback)
+ }
+
func sendAgree(callback: (() -> Void)? = nil) {
var t = HotlineTransaction(type: .agreed)
- t.setParameterString(type: .userName, val: self.userName)
- t.setParameterUInt32(type: .userIconID, val: self.userIconID)
- t.setParameterUInt32(type: .options, val: 0)
-
- print("HotlineClient: agreeing")
+ t.setFieldString(type: .userName, val: self.userName)
+ t.setFieldUInt16(type: .userIconID, val: self.userIconID)
+ t.setFieldUInt32(type: .options, val: 0)
self.sendTransaction(t, callback: callback)
}
func sendChat(message: String, callback: (() -> Void)? = nil) {
var t = HotlineTransaction(type: .sendChat)
- t.setParameterString(type: .data, val: message)
-
- print("HotlineClient: sending chat...")
+ t.setFieldString(type: .data, val: message)
self.sendTransaction(t, callback: callback)
}
func sendGetUserList(callback: (() -> Void)? = nil) {
let t = HotlineTransaction(type: .getUserNameList)
- print("HotlineClient: fetching user list...")
self.sendTransaction(t, callback: callback)
}
// MARK: - Incoming
+ private func processReply(_ transaction: HotlineTransaction) {
+ guard transaction.errorCode == 0 else {
+ if let errorParam = transaction.getField(type: .errorText), let errorText = errorParam.getString() {
+ print("HotlineClient 😵 \(transaction.errorCode): \(errorText)")
+ }
+ else {
+ print("HotlineClient 😵 \(transaction.errorCode)")
+ }
+ return
+ }
+
+ guard let repliedTransactionType = self.transactionLog[transaction.id] else {
+ return
+ }
+
+ print("HotlineClient reply in response to \(repliedTransactionType)")
+
+ switch(repliedTransactionType) {
+ case .login:
+ print("GOT REPLY TO LOGIN!")
+
+ if
+ let serverVersionField = transaction.getField(type: .versionNumber),
+ let serverVersion = serverVersionField.getUInt16() {
+ self.serverVersion = serverVersion
+ print("SERVER VERSION: \(serverVersion)")
+ }
+ case .getUserNameList:
+ print("GOT USER LIST")
+ var newUserList: [HotlineUser] = []
+ for u in transaction.getFieldList(type: .userNameWithInfo) {
+ let info = u.getUserInfo()
+ print("USER: \(info)")
+ let user = HotlineUser(id: info.id, iconID: info.iconID, status: info.flags, name: info.userName)
+ newUserList.append(user)
+ }
+ DispatchQueue.main.async {
+ self.userList = newUserList
+ }
+ default:
+ break
+ }
+ }
+
private func processTransaction(_ transaction: HotlineTransaction) {
+ if transaction.type == .reply {
+ print("HotlineClient <= \(transaction.type) to \(transaction.id):")
+ print(transaction)
+ }
+ else {
+ print("HotlineClient <= \(transaction.type)")
+ }
+
switch(transaction.type) {
case .reply:
- print("HotlineClient: GOT REPLY TRANSACTION? \(transaction)")
+ self.processReply(transaction)
+// print("HotlineClient: Received reply transaction: \(transaction)")
+
case .chatMessage:
- print("HotlineClient: CHAT MESSAGE!")
- if
- let chatTextParam = transaction.getParameter(type: .data),
+ if
+ let chatTextParam = transaction.getField(type: .data),
let chatText = chatTextParam.getString(),
- let userNameParam = transaction.getParameter(type: .userName),
+ let userNameParam = transaction.getField(type: .userName),
let userName = userNameParam.getString(),
- let userIDParam = transaction.getParameter(type: .userID),
+ let userIDParam = transaction.getField(type: .userID),
let userID = userIDParam.getUInt16() {
print("HotlineClient: \(userName):\(userID): \(chatText)")
DispatchQueue.main.async {
@@ -325,37 +391,42 @@ class HotlineClient : ObservableObject {
}
case .getUserNameList:
print("HotlineClient: GOT USER NAME LIST!")
- let userList = transaction.getParameterList(type: .userInfo)
+ let userList = transaction.getFieldList(type: .userNameWithInfo)
for u in userList {
let userInfo = u.getUserInfo()
print("HotlineClient: user \(userInfo.userName)")
}
case .notifyOfUserChange:
- print("HotlineClient: user changed")
- if let p = transaction.getParameter(type: .userName),
+// print("HotlineClient: user changed")
+ if let p = transaction.getField(type: .userName),
let userName = p.getString() {
- print("HotlineClient: user name \(userName)")
+ print("HotlineClient: User changed \(userName)")
}
case .disconnectMessage:
- print("HotlineClient: DISCONNECTED BY SERVER!")
+ print("HotlineClient ❌")
self.disconnect()
case .showAgreement:
- if let agreementParam = transaction.getParameter(type: .data) {
+ if let noAgreementField = transaction.getField(type: .noServerAgreement) {
+ print("NO AGREEMENT?")
+ }
+ if let agreementParam = transaction.getField(type: .data) {
if let agreementText = agreementParam.getString() {
- print("AGREEMENT:", agreementText)
+ print("\n\n--------------------------\n")
+ print(agreementText)
+ print("\n--------------------------\n\n")
DispatchQueue.main.async {
self.agreement = agreementText
}
- self.sendAgree() {
+// self.sendAgree() {
// self.sendGetUserList()
- }
+// }
}
}
case .userAccess:
- print("HotlineClient: user access transaction.")
+ print("")
default:
- print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.parameters.count) parameters")
- print(transaction.parameters)
+ print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters")
+ print(transaction.fields)
}
}
}
diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift
index d5fe734..5372e78 100644
--- a/Hotline/Network/HotlineProtocol.swift
+++ b/Hotline/Network/HotlineProtocol.swift
@@ -19,15 +19,58 @@ struct HotlineServer: Identifiable, Hashable {
struct HotlineUser: Identifiable, Hashable {
let id: UInt16
- let userName: String
+ let iconID: UInt16
+ let status: UInt16
+ let name: String
+
+ var isAdmin: Bool {
+ return ((self.status & 0x0002) != 0)
+ }
+
+ var isIdle: Bool {
+ return ((self.status & 0x0001) != 0)
+ }
static func == (lhs: HotlineUser, rhs: HotlineUser) -> Bool {
return lhs.id == rhs.id
}
+ init(id: UInt16, iconID: UInt16, status: UInt16, name: String) {
+ self.id = id
+ self.iconID = iconID
+ self.status = status
+ self.name = name
+ }
+
+ init(from data: Data) {
+ self.id = data.readUInt16(at: 0)!
+ self.iconID = data.readUInt16(at: 2)!
+ self.status = data.readUInt16(at: 4)!
+
+ let userNameLength = Int(data.readUInt16(at: 6)!)
+ self.name = data.readString(at: 8, length: userNameLength, encoding: .ascii)!
+ }
+
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
+
+ func encoded() -> Data {
+ var data = Data()
+ self.encode(to: &data)
+ return data
+ }
+
+ func encode(to data: inout Data) {
+ data.appendUInt16(self.id)
+ data.appendUInt16(self.iconID)
+ data.appendUInt16(self.status)
+
+ let userNameData = name.data(using: .ascii, allowLossyConversion: true)!
+
+ data.appendUInt16(UInt16(userNameData.count))
+ data.append(userNameData)
+ }
}
struct HotlineUserInfo {
@@ -46,21 +89,82 @@ struct HotlineUserInfo {
}
}
-struct HotlineTransactionParameter {
+struct HotlineTransactionField {
let type: HotlineTransactionFieldType
let dataSize: UInt16
let data: Data
+ init(type: HotlineTransactionFieldType, dataSize: UInt16, data: Data) {
+ self.type = type
+ self.dataSize = dataSize
+ self.data = data
+ }
+
+ init(type: HotlineTransactionFieldType, val: UInt8) {
+ self.init(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))
+ }
+
+ init(type: HotlineTransactionFieldType, val: UInt16) {
+ self.init(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))
+ }
+
+ init(type: HotlineTransactionFieldType, val: UInt32) {
+ self.init(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))
+ }
+
+ init(type: HotlineTransactionFieldType, string: String, encoding: String.Encoding = .ascii, encrypt: Bool = false) {
+ var stringInput = string
+
+ if encrypt {
+ stringInput = String(string.utf8.map { char in
+ Character(UnicodeScalar(0xFF - char))
+ })
+ }
+
+ var stringData: Data?
+ stringData = stringInput.data(using: encoding, allowLossyConversion: true)
+ if stringData == nil {
+ stringData = Data()
+ }
+
+ self.init(type: type, dataSize: UInt16(stringData!.count), data: stringData!)
+ }
+
+ init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) {
+ self.init(type: type, string: string, encoding: .ascii, encrypt: encrypt)
+ }
+
func getUInt8() -> UInt8? {
- return data.readUInt8(at: 0)
+ return self.data.readUInt8(at: 0)
}
func getUInt16() -> UInt16? {
- return data.readUInt16(at: 0)
+ return self.data.readUInt16(at: 0)
}
func getUInt32() -> UInt32? {
- return data.readUInt32(at: 0)
+ return self.data.readUInt32(at: 0)
+ }
+
+ func getInteger() -> Int? {
+ switch(self.data.count) {
+ case 1:
+ if let val = self.getUInt8() {
+ return Int(val)
+ }
+ case 2:
+ if let val = self.getUInt16() {
+ return Int(val)
+ }
+ case 4:
+ if let val = self.getUInt32() {
+ return Int(val)
+ }
+ default:
+ break
+ }
+
+ return nil
}
func getString(encoding: String.Encoding = .ascii) -> String? {
@@ -89,7 +193,7 @@ struct HotlineTransaction {
var totalSize: UInt32 = UInt32(HotlineTransaction.headerSize)
var dataSize: UInt32 = 0
- var parameters: [HotlineTransactionParameter] = []
+ var fields: [HotlineTransactionField] = []
init(type: HotlineTransactionType) {
self.type = type
@@ -105,48 +209,34 @@ struct HotlineTransaction {
self.dataSize = dataSize
}
- mutating func setParameterUInt8(type: HotlineTransactionFieldType, val: UInt8) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))
+ mutating func setFieldUInt8(type: HotlineTransactionFieldType, val: UInt8) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterUInt16(type: HotlineTransactionFieldType, val: UInt16) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))
+ mutating func setFieldUInt16(type: HotlineTransactionFieldType, val: UInt16) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterUInt32(type: HotlineTransactionFieldType, val: UInt32) {
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)))
-// self.parameters[type] = HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))
+ mutating func setFieldUInt32(type: HotlineTransactionFieldType, val: UInt32) {
+ self.fields.append(HotlineTransactionField(type: type, val: val))
}
- mutating func setParameterEncodedString(type: HotlineTransactionFieldType, val: String) {
- let encodedVal = String(val.utf8.map { char in
- Character(UnicodeScalar(0xFF - char))
- })
-
- self.setParameterString(type: type, val: encodedVal)
+ mutating func setFieldEncodedString(type: HotlineTransactionFieldType, val: String) {
+ self.fields.append(HotlineTransactionField(type: type, string: val, encrypt: true))
}
- mutating func setParameterString(type: HotlineTransactionFieldType, val: String) {
- var stringData = Data()
-// stringData.appendUInt16(UInt16(val.count))
- stringData.append(contentsOf: val.utf8)
-
- self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(stringData.count), data: stringData))
-// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(stringData.count), data: stringData)
+ mutating func setFieldString(type: HotlineTransactionFieldType, val: String) {
+ self.fields.append(HotlineTransactionField(type: type, string: val))
}
- func getParameter(type: HotlineTransactionFieldType) -> HotlineTransactionParameter? {
- return self.parameters.first { p in
+ func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? {
+ return self.fields.first { p in
p.type == type
}
-
-// return self.parameters[type]
}
- func getParameterList(type: HotlineTransactionFieldType) -> [HotlineTransactionParameter] {
- return self.parameters.filter { p in
+ func getFieldList(type: HotlineTransactionFieldType) -> [HotlineTransactionField] {
+ return self.fields.filter { p in
p.type == type
}
}
@@ -164,40 +254,76 @@ struct HotlineTransaction {
data.appendUInt32(self.id)
data.appendUInt32(self.errorCode)
- if self.parameters.count > 0 {
- var parameterData = Data()
- parameterData.appendUInt16(UInt16(self.parameters.count))
- for param in self.parameters {
- parameterData.appendUInt16(param.type.rawValue)
- parameterData.appendUInt16(param.dataSize)
- parameterData.append(param.data)
+ if self.fields.count > 0 {
+ var fieldData = Data()
+ fieldData.appendUInt16(UInt16(self.fields.count))
+ for f in self.fields {
+ fieldData.appendUInt16(f.type.rawValue)
+ fieldData.appendUInt16(f.dataSize)
+ fieldData.append(f.data)
}
- data.appendUInt32(UInt32(parameterData.count))
- data.appendUInt32(UInt32(parameterData.count))
- data.append(parameterData)
+ data.appendUInt32(UInt32(fieldData.count))
+ data.appendUInt32(UInt32(fieldData.count))
+ data.append(fieldData)
}
else {
- data.appendUInt32(0)
- data.appendUInt32(0)
+ data.appendUInt32(2)
+ data.appendUInt32(2)
+ data.appendUInt16(0)
}
}
}
enum HotlineTransactionFieldType: UInt16 {
+ case errorText = 100 // String
+ case data = 101 // String
case userName = 102 // String
+ case userID = 103 // Integer
+ case userIconID = 104 // Integer
case userLogin = 105 // Encoded string
case userPassword = 106 // Encoded string
- case userIconID = 104 // Integer
- case userID = 103 // Integer
- case data = 101 // String
- case userAccess = 110 // 64-bit integer??
- case userFlags = 112
+ case referenceNumber = 107 // Integer
+ case transferSize = 108 // Integer
+ case chatOptions = 109 // Integer
+ case userAccess = 110 // 64-bit integer?
+ case userAlias = 111 // ???
+ case userFlags = 112 // Integer
case options = 113 // 32-bit integer?
+ case chatID = 114 // Integer
+ case chatSubject = 115 // String
+ case waitingCount = 116 // Integer
+ case serverAgreement = 150 // ???
+ case serverBanner = 151 // Data?
+ case serverBannerType = 152 // Integer
+ case serverBannerURL = 153 // String
+ case noServerAgreement = 154 // Integer
case versionNumber = 160 // Integer
- case bannerID = 161
- case serverName = 162
- case userInfo = 300
+ case communityBannerID = 161 // Integer
+ case serverName = 162 // String
+ // TODO: Add file field types
+ case quotingMessage = 214 // String?
+ case automaticResponse = 215 // String
+ case folderItemCount = 220 // Integer
+ case userNameWithInfo = 300 // Data { user id: 2, icon id: 2, user flags: 2, user name size: 2, user name: size }
+ case newsCategoryGUID = 319 // Data?
+ case newsCategoryListData = 320 // Data { type: 1 (1 = folder, 10 = category, 255 = other), category name: rest }
+ case newsArticleListData = 321 // Data
+ case newsCategoryName = 322 // String
+ case newsCategoryListData15 = 323 // Data
+ case newsPath = 325 // Data
+ case newsArticleID = 326 // Integer
+ case newsArticleDataFlavor = 327 // String
+ case newsArticleTitle = 328 // String
+ case newsArticlePoster = 329 // String
+ case newsArticleDate = 330 // Data { year: 2, ms: 2, secs: 4 }
+ case newsArticlePrevious = 331 // Integer
+ case newsArticleNext = 332 // Integer
+ case newsArticleData = 333 // Data
+ case newsArticleFlags = 334 // Integer
+ case newsArticleParentArticle = 335 // Integer
+ case newsArticleFirstChildArticle = 336 // Integer
+ case newsArticleRecursiveDelete = 337 // Integer
}
enum HotlineTransactionType: UInt16 {
diff --git a/Hotline/TrackerView.swift b/Hotline/TrackerView.swift
index 0bc9762..1b5cce5 100644
--- a/Hotline/TrackerView.swift
+++ b/Hotline/TrackerView.swift
@@ -1,112 +1,96 @@
import SwiftUI
import SwiftData
+struct AgreementView: View {
+ @Environment(\.dismiss) var dismiss
+
+ let text: String
+
+ var body: some View {
+ Text(text)
+ }
+}
+
+struct TrackerServerView: View {
+ @Environment(\.dismiss) var dismiss
+
+ let server: HotlineServer
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ HStack {
+ Text("🌎")
+ Text(server.name!).bold().dynamicTypeSize(.xxLarge)
+ }
+ .padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
+ Text(server.description!).opacity(0.6).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
+ Text(server.address).opacity(0.3).dynamicTypeSize(.medium)
+ Spacer()
+ HStack(alignment: .center) {
+ Button("Connect") {
+ print("WHAT", "HELLO", server.address)
+ HotlineClient.shared.connect(to: server)
+ dismiss()
+// client = HotlineClient(server: selectedServer)
+ }
+ .bold()
+ .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
+ .frame(maxWidth: .infinity)
+ .foregroundColor(.black)
+ .background(LinearGradient(gradient: Gradient(colors: [Color(white: 0.95), Color(white: 0.91)]), startPoint: .top, endPoint: .bottom))
+ .
+ overlay(
+ RoundedRectangle(cornerRadius: 10.0).stroke(.black, lineWidth: 3).opacity(0.4)
+ )
+ .cornerRadius(10.0)
+ }
+ }
+ .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0))
+ .presentationDetents([.fraction(0.4)])
+ .presentationDragIndicator(.automatic)
+ }
+}
+
struct TrackerView: View {
+
// @Environment(\.modelContext) private var modelContext
// @Query private var items: [Item]
@StateObject var tracker = HotlineTracker(address: "hltracker.com")
- @State private var selectedServer: HotlineServer?
-
- private var client: HotlineClient?
+ @StateObject var client = HotlineClient.shared
+ @State private var selectedServer: HotlineServer?
+ @State private var showingAgreement = false
+ @State private var showingConnectSheet = false
+
var body: some View {
List(selection: $selectedServer) {
ForEach(tracker.servers) { server in
HStack {
+ Text("🌎")
Text(server.name!).bold()
Spacer()
- HStack {
- Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3)
- // Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3)
- }
- .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
- .background(Color(white: 0.94))
- .cornerRadius(20.0)
+ Text("\(server.users)").opacity(0.6)
}
-// .contentShape(Rectangle())
- .listRowSeparator(.hidden)
+ .listRowBackground(Color(white: 0.96))
+ .listRowInsets(EdgeInsets(top: 0, leading: 16.0, bottom: 0, trailing: 16.0))
+ .listRowSeparator(.visible, edges: VerticalEdge.Set.all)
+ .listRowSeparatorTint(Color(white: 1.0))
.tag(server)
- // .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0))
-// .onTapGesture {
-// self.selectedServer = server
-// }
}
}
+ .background(Color(white: 0.96))
.listStyle(.plain)
.frame(maxWidth: .infinity)
.task {
tracker.fetch()
}
.sheet(item: $selectedServer) { item in
- VStack(alignment: .leading) {
- Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
- Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
- Text(item.address).opacity(0.2).dynamicTypeSize(.medium)
- Spacer()
- HStack(alignment: .center) {
- Button("Connect") {
- print("WHAT", "HELLO", selectedServer!.address)
- HotlineClient.shared.connect(to: selectedServer!)
-// client = HotlineClient(server: selectedServer)
- }
- .bold()
- .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
- .frame(maxWidth: .infinity)
- .foregroundColor(.white)
- .background(Color(.black))
- .cornerRadius(8.0)
- }
- }
- .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0))
- .presentationDetents([.fraction(0.4)])
- .presentationDragIndicator(.visible)
+ TrackerServerView(server: item)
+ }
+ .sheet(isPresented: Binding(get: { client.agreement != nil }, set: { _ in })) {
+ AgreementView(text: client.agreement!)
}
-// List(tracker.servers) { server in
-// HStack {
-// Text(server.name!).bold()
-// Spacer()
-// HStack {
-// Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3)
-//// Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3)
-// }
-// .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
-// .background(Color(white: 0.94))
-// .cornerRadius(20.0)
-// }
-// .contentShape(Rectangle())
-// .listRowSeparator(.hidden)
-//// .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0))
-// .onTapGesture {
-// self.selectedServer = server
-// }
-// }
-// .listStyle(.plain)
-// .frame(maxWidth: .infinity)
-// .task {
-// tracker.fetch()
-// }
-// .sheet(item: $selectedServer) { item in
-// VStack(alignment: .leading) {
-// Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0))
-// Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge)
-// Spacer()
-// HStack(alignment: .center) {
-// Button("Connect") {
-// print("WHAT")
-// }
-// .bold()
-// .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
-// .frame(maxWidth: .infinity)
-// .foregroundColor(.white)
-// .background(Color(.black))
-// .cornerRadius(8.0)
-// }
-// }
-// .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0))
-// .presentationDetents([.fraction(0.3)])
-// .presentationDragIndicator(.visible)
-// }
}
}
diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/DataExtensions.swift
index caf26a9..9f4c967 100644
--- a/Hotline/Utility/DataExtensions.swift
+++ b/Hotline/Utility/DataExtensions.swift
@@ -22,33 +22,33 @@ extension Data {
}
func readUInt8(at offset: Int) -> UInt8? {
- guard offset >= 0, offset + MemoryLayout<UInt8>.size <= self.count else {
+ guard offset >= 0, offset + 1 <= self.count else {
return nil
}
return self[offset]
}
- func readUInt16(at offset: Int, endianness: Endianness = .big) -> UInt16? {
- guard offset >= 0, offset + MemoryLayout<UInt16>.size <= self.count else {
+ func readUInt16(at offset: Int) -> UInt16? {
+ guard offset >= 0, offset + 2 <= self.count else {
return nil
}
- let value = self.subdata(in: offset..<(offset + MemoryLayout<UInt16>.size)).withUnsafeBytes { $0.load(as: UInt16.self) }
- return (endianness == .big) ? value.bigEndian : value.littleEndian
+
+ return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1])
}
- func readUInt32(at offset: Int, endianness: Endianness = .big) -> UInt32? {
- guard offset >= 0, offset + MemoryLayout<UInt32>.size <= self.count else {
+ func readUInt32(at offset: Int) -> UInt32? {
+ guard offset >= 0, offset + 4 <= self.count else {
return nil
}
- let value = self.subdata(in: offset..<(offset + MemoryLayout<UInt32>.size)).withUnsafeBytes { $0.load(as: UInt32.self) }
- return (endianness == .big) ? value.bigEndian : value.littleEndian
+
+ return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3])
}
func readData(at offset: Int, length: Int) -> Data? {
guard offset >= 0, offset + length <= self.count else {
return nil
}
- return self[offset..<(offset + length)]
+ return self.subdata(in: offset..<(offset + length))
}
func readString(at offset: Int, length: Int, encoding: String.Encoding) -> String? {