Skip to content

Commit

Permalink
Initial app implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
aitjcize committed Mar 9, 2025
1 parent 88a6997 commit b4d8df7
Show file tree
Hide file tree
Showing 115 changed files with 9,684 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ bin/ghost
bin/ghost.*.linux*
bin/ghost.py*
bin/overlordd
bin/ovl.py.bin
bin/*.pybin*
bin/webroot
build
webroot/apps
webroot/index.html
node_modules
.vite
dist
UserInterfaceState.xcuserstate
20 changes: 20 additions & 0 deletions OverlordApp/OverlordApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftUI

struct ContentView: View {
@EnvironmentObject private var authViewModel: AuthViewModel

var body: some View {
if authViewModel.isAuthenticated {
DashboardView()
} else {
LoginView()
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(AuthViewModel())
}
}
14 changes: 14 additions & 0 deletions OverlordApp/OverlordApp/Models/Camera.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

struct Camera: Identifiable {
let id: String
let clientId: String
var isMinimized: Bool = false
var position: CGPoint = CGPoint(x: 100, y: 100)
var size: CGSize = CGSize(width: 400, height: 300)

init(id: String = UUID().uuidString, clientId: String) {
self.id = id
self.clientId = clientId
}
}
33 changes: 33 additions & 0 deletions OverlordApp/OverlordApp/Models/Client.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

struct Client: Identifiable, Codable {
var id: String { mid }
let mid: String
let name: String?
var properties: [String: String]?
var lastSeen: Date
var hasCamera: Bool

enum CodingKeys: String, CodingKey {
case mid, name, properties
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
mid = try container.decode(String.self, forKey: .mid)
name = try container.decodeIfPresent(String.self, forKey: .name)
properties = try container.decodeIfPresent([String: String].self, forKey: .properties)
lastSeen = Date()

// Determine if client has camera based on properties
hasCamera = properties?["has_camera"] == "true"
}

init(mid: String, name: String? = nil, properties: [String: String]? = nil) {
self.mid = mid
self.name = name
self.properties = properties
self.lastSeen = Date()
self.hasCamera = properties?["has_camera"] == "true"
}
}
17 changes: 17 additions & 0 deletions OverlordApp/OverlordApp/Models/Terminal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

struct Terminal: Identifiable {
let id: String
let clientId: String
var title: String
var output: String = ""
var isMinimized: Bool = false
var position: CGPoint = CGPoint(x: 100, y: 100)
var size: CGSize = CGSize(width: 600, height: 400)

init(id: String = UUID().uuidString, clientId: String, title: String) {
self.id = id
self.clientId = clientId
self.title = title
}
}
25 changes: 25 additions & 0 deletions OverlordApp/OverlordApp/Models/UploadProgress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

struct UploadProgress: Identifiable {
let id: String
let filename: String
let clientId: String
var progress: Double
var status: UploadStatus
var startTime: Date

enum UploadStatus: String {
case uploading
case completed
case failed
}

init(id: String = UUID().uuidString, filename: String, clientId: String) {
self.id = id
self.filename = filename
self.clientId = clientId
self.progress = 0.0
self.status = .uploading
self.startTime = Date()
}
}
13 changes: 13 additions & 0 deletions OverlordApp/OverlordApp/OverlordApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SwiftUI

@main
struct OverlordApp: App {
@StateObject private var authViewModel = AuthViewModel()

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authViewModel)
}
}
}
40 changes: 40 additions & 0 deletions OverlordApp/OverlordApp/Services/APIService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation
import Combine

class APIService {
static let baseURL = "http://your-server-address/api" // Replace with your actual server address

private let session: URLSession
private var cancellables = Set<AnyCancellable>()

init(session: URLSession = .shared) {
self.session = session
}

func getClients(token: String) -> AnyPublisher<[Client], Error> {
let url = URL(string: "\(APIService.baseURL)/agents/list")!
var request = URLRequest(url: url)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

return session.dataTaskPublisher(for: request)
.map { $0.data }
.decode(type: [Client].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}

func getClientProperties(mid: String, token: String) -> AnyPublisher<[String: String], Error> {
let url = URL(string: "\(APIService.baseURL)/agent/properties/\(mid)")!
var request = URLRequest(url: url)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

return session.dataTaskPublisher(for: request)
.map { $0.data }
.decode(type: [String: String].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}

func downloadFile(sid: String, token: String) {
let url = URL(string: "\(APIService.baseURL)/file/download/\(sid)?token=\(token)")!
UIApplication.shared.open(url)
}
}
144 changes: 144 additions & 0 deletions OverlordApp/OverlordApp/Services/WebSocketService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Foundation
import Combine

class WebSocketService: ObservableObject {
private var webSocket: URLSessionWebSocketTask?
private var session: URLSession
private var isStarted = false
private var reconnectTimer: Timer?
private var reconnectAttempts = 0
private let maxReconnectAttempts = 5

@Published var isConnected = false

private var eventHandlers: [String: [(String) -> Void]] = [:]

init(session: URLSession = .shared) {
self.session = session
}

func start(token: String) {
guard !isStarted else { return }

isStarted = true
reconnectAttempts = 0
connect(token: token)
}

func stop() {
isStarted = false

reconnectTimer?.invalidate()
reconnectTimer = nil

webSocket?.cancel(with: .normalClosure, reason: nil)
webSocket = nil

isConnected = false
}

private func connect(token: String) {
guard isStarted, webSocket == nil else { return }

let urlString = "\(APIService.baseURL.replacingOccurrences(of: "http", with: "ws"))/monitor?token=\(token)"
guard let url = URL(string: urlString) else {
print("Invalid WebSocket URL")
return
}

webSocket = session.webSocketTask(with: url)
webSocket?.resume()

receiveMessage()

isConnected = true
}

private func receiveMessage() {
webSocket?.receive { [weak self] result in
guard let self = self else { return }

switch result {
case .success(let message):
switch message {
case .string(let text):
self.handleMessage(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
self.handleMessage(text)
}
@unknown default:
break
}

// Continue receiving messages
self.receiveMessage()

case .failure(let error):
print("WebSocket receive error: \(error)")
self.handleDisconnect()
}
}
}

private func handleMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let message = try? JSONDecoder().decode(WebSocketMessage.self, from: data) else {
return
}

let handlers = eventHandlers[message.event] ?? []
let messageData = message.data.first ?? ""

DispatchQueue.main.async {
handlers.forEach { handler in
handler(messageData)
}
}
}

private func handleDisconnect() {
isConnected = false
webSocket = nil

guard isStarted else { return }

reconnectAttempts += 1

if reconnectAttempts >= maxReconnectAttempts {
// Too many failed attempts, stop trying
stop()

// Notify that authentication might have failed
NotificationCenter.default.post(name: .webSocketAuthenticationFailed, object: nil)
return
}

// Try to reconnect after a delay
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
guard let self = self, let token = UserDefaults.standard.string(forKey: "authToken") else { return }
self.connect(token: token)
}
}

func on(event: String, handler: @escaping (String) -> Void) {
if eventHandlers[event] == nil {
eventHandlers[event] = []
}

eventHandlers[event]?.append(handler)
}

func off(event: String) {
eventHandlers[event] = nil
}
}

struct WebSocketMessage: Codable {
let event: String
let data: [String]
}

extension Notification.Name {
static let webSocketAuthenticationFailed = Notification.Name("webSocketAuthenticationFailed")
}
Loading

0 comments on commit b4d8df7

Please sign in to comment.