aboutsummaryrefslogtreecommitdiff
path: root/Hotline
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 /Hotline
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.
Diffstat (limited to 'Hotline')
-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? {