diff --git a/.gitignore b/.gitignore index 3b29812..8bfa2f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +*/.build /.build /Packages /*.xcodeproj diff --git a/SuperLandoAdmin/AdminLib/Sources/APIRequester/APIRequester.swift b/SuperLandoAdmin/AdminLib/Sources/APIRequester/APIRequester.swift index 6d3eb8f..7fb5d25 100644 --- a/SuperLandoAdmin/AdminLib/Sources/APIRequester/APIRequester.swift +++ b/SuperLandoAdmin/AdminLib/Sources/APIRequester/APIRequester.swift @@ -39,7 +39,8 @@ final class APIRequester: APIRequesting { func makeURL(path: String, queryItems: [URLQueryItem]?) throws -> URL? { #if DEBUG - let host: String = "localhost" + // let host: String = "localhost" + let host: String = "quake.host" #else let host: String = "myones-backend.onrender.com" #endif @@ -51,9 +52,10 @@ final class APIRequester: APIRequesting { // let host: String = try Configuration.value(for: "BASE_URL", in: .main) var components = URLComponents() - components.scheme = isLocalhost ? "http" : "https" + components.scheme = isLocalhost ? "http" : "http" components.host = host - if isLocalhost { components.port = 8080 } +// if isLocalhost { components.port = 8080 } + components.port = 8080 components.path = "/\(path)" components.queryItems = queryItems return components.url diff --git a/SuperLandoAdmin/AdminLib/Sources/AdminLib/UploadRace.swift b/SuperLandoAdmin/AdminLib/Sources/AdminLib/UploadRace.swift index 3f09642..57370c7 100644 --- a/SuperLandoAdmin/AdminLib/Sources/AdminLib/UploadRace.swift +++ b/SuperLandoAdmin/AdminLib/Sources/AdminLib/UploadRace.swift @@ -24,6 +24,39 @@ public struct UploadRaceEvent: Codable, Equatable, Identifiable { var date: Date } +public enum UploadRaceEventBundle: Equatable { + case F1Sprint + case F1Regular + + var tag: String { + switch self { + case .F1Sprint, .F1Regular: + "f1" + } + } + + var events: [UploadRaceEvent] { + switch self { + case .F1Sprint: + [ + .init(title: "Practice 1", date: Date()), + .init(title: "Qualifying", date: Date()), + .init(title: "Practice 2", date: Date()), + .init(title: "Sprint", date: Date()), + .init(title: "Race", date: Date()), + ] + case .F1Regular: + [ + .init(title: "Practice 1", date: Date()), + .init(title: "Practice 2", date: Date()), + .init(title: "Practice 3", date: Date()), + .init(title: "Qualifying", date: Date()), + .init(title: "Race", date: Date()), + ] + } + } +} + public struct UploadRace: Reducer { public init() {} @@ -40,6 +73,7 @@ public struct UploadRace: Reducer { case uploadRace(APIClient.Action) case submitRace case addEvent + case addBundle(UploadRaceEventBundle) case updateEventTitle(UploadRaceEvent, String) case updateEventDate(UploadRaceEvent, Date) case binding(BindingAction) @@ -51,7 +85,16 @@ public struct UploadRace: Reducer { Reduce { state, action in switch action { case .addEvent: - state.events.append(.init(title: "", date: Date())) + if let ev = state.events.first { + state.events.append(.init(title: "", date: ev.date)) + } else { + state.events.append(.init(title: "", date: Date())) + } + return .none + + case .addBundle(let bundle): + state.raceTag = bundle.tag + state.events = bundle.events return .none case .updateEventTitle(let event, let title): @@ -82,7 +125,7 @@ public struct UploadRace: Reducer { try await send(.uploadRace(.request(.post(request)))) } - case .uploadRace(.response(.finished)): + case .uploadRace(.response(.finished(.success))): state.raceTitle = "" state.raceTag = "" state.events = [] @@ -146,6 +189,16 @@ public struct UploadRaceView: View { viewStore.send(.addEvent) } + Menu("add bundle") { + Button("F1 Sprint") { + viewStore.send(.addBundle(.F1Sprint)) + } + + Button("F1 Regular") { + viewStore.send(.addBundle(.F1Regular)) + } + } + } header: { Text("upload race") } footer: { diff --git a/SuperLandoAdmin/SuperLandoAdmin.xcodeproj/project.pbxproj b/SuperLandoAdmin/SuperLandoAdmin.xcodeproj/project.pbxproj index 03d3563..e764d65 100644 --- a/SuperLandoAdmin/SuperLandoAdmin.xcodeproj/project.pbxproj +++ b/SuperLandoAdmin/SuperLandoAdmin.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ /* Begin PBXFileReference section */ B40901912AC1B7B900B6F436 /* Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Upload.swift; sourceTree = ""; }; B40901932AC1C0E200B6F436 /* TestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRequest.swift; sourceTree = ""; }; + B42603E32AC59C43009F9936 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; B48609DB2AC126650098866E /* SuperLandoAdmin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SuperLandoAdmin.app; sourceTree = BUILT_PRODUCTS_DIR; }; B48609DE2AC126650098866E /* SuperLandoAdminApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperLandoAdminApp.swift; sourceTree = ""; }; B48609E02AC126650098866E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -68,6 +69,7 @@ B48609DD2AC126650098866E /* SuperLandoAdmin */ = { isa = PBXGroup; children = ( + B42603E32AC59C43009F9936 /* Info.plist */, B48609EC2AC126860098866E /* SuperLandoAdmin.entitlements */, B48609DE2AC126650098866E /* SuperLandoAdminApp.swift */, B48609E02AC126650098866E /* ContentView.swift */, @@ -301,6 +303,7 @@ DEVELOPMENT_TEAM = 7N9Z7384YN; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SuperLandoAdmin/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -334,6 +337,7 @@ DEVELOPMENT_TEAM = 7N9Z7384YN; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SuperLandoAdmin/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/SuperLandoAdmin/SuperLandoAdmin/Info.plist b/SuperLandoAdmin/SuperLandoAdmin/Info.plist new file mode 100644 index 0000000..9f0981b --- /dev/null +++ b/SuperLandoAdmin/SuperLandoAdmin/Info.plist @@ -0,0 +1,19 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + http://quake.host + + NSIncludesSubdomains + + + + + + diff --git a/backend/Package.resolved b/backend/Package.resolved index 6c49fbc..983f8f3 100644 --- a/backend/Package.resolved +++ b/backend/Package.resolved @@ -54,15 +54,6 @@ "version" : "2.8.0" } }, - { - "identity" : "ginny", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalonunez/ginny.git", - "state" : { - "revision" : "4ab1e7d43d3ef4d161cccdad6145fe90f11c666c", - "version" : "0.1.1" - } - }, { "identity" : "multipart-kit", "kind" : "remoteSourceControl", @@ -117,15 +108,6 @@ "version" : "1.0.0" } }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -225,15 +207,6 @@ "version" : "1.0.2" } }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", - "state" : { - "revision" : "edd2d0cdb988ac45e2515e0dd0624e4a6de54a94", - "version" : "0.50800.0-SNAPSHOT-2022-12-29-a" - } - }, { "identity" : "vapor", "kind" : "remoteSourceControl", diff --git a/backend/Package.swift b/backend/Package.swift index 73f9ce4..e3b605c 100644 --- a/backend/Package.swift +++ b/backend/Package.swift @@ -13,7 +13,6 @@ let package = Package( .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"), // 🐘 Fluent driver for Postgres. .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"), - .package(url: "https://github.com/gonzalonunez/ginny.git", from: "0.1.1"), ], targets: [ .executableTarget( @@ -22,10 +21,6 @@ let package = Package( .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "Vapor", package: "vapor"), - .product(name: "Ginny", package: "ginny"), - ], - plugins: [ - .plugin(name: "GinnyPlugin", package: "ginny") ] ), .testTarget(name: "AppTests", dependencies: [ diff --git a/backend/Sources/App/AsyncRequestHandler.swift b/backend/Sources/App/AsyncRequestHandler.swift new file mode 100644 index 0000000..6e882d7 --- /dev/null +++ b/backend/Sources/App/AsyncRequestHandler.swift @@ -0,0 +1,37 @@ +// +// AsyncRequestHandler.swift +// +// +// Created by Mauricio Cardozo on 28/09/23. +// + +import Foundation +import Vapor + +// This file is part of the de-ginnyfication of the repo - We should investigate the memory issues and bring ginny back + +public protocol AsyncRequestHandler { + init() + associatedtype Response: AsyncResponseEncodable + var method: HTTPMethod { get } + var path: String { get } + func handle(req: Request) async throws -> Response +} + +extension AsyncRequestHandler { + public func register(in app: Application) { + app.logger.info("registering \(path.pathComponents) as \(method.string)") + app + .on(method, path.pathComponents) { [handle] req async throws in + return try await handle(req) + } + } +} + +extension Array where Element == any AsyncRequestHandler { + func register(in app: Application) { + self.forEach { + $0.register(in: app) + } + } +} diff --git a/backend/Sources/App/configure.swift b/backend/Sources/App/configure.swift index 6e15897..e9f7b6b 100644 --- a/backend/Sources/App/configure.swift +++ b/backend/Sources/App/configure.swift @@ -19,5 +19,11 @@ public func configure(_ app: Application) async throws { app.migrations.add(v0_1Migration()) + [ + UploadCategoryHandler(), + NextRaceHandler(), + UploadRaceHandler() + ].register(in: app) + try await app.autoMigrate() } diff --git a/backend/Sources/App/entrypoint.swift b/backend/Sources/App/entrypoint.swift index 3d15081..14cdb1c 100644 --- a/backend/Sources/App/entrypoint.swift +++ b/backend/Sources/App/entrypoint.swift @@ -1,7 +1,6 @@ import Vapor import Dispatch import Logging -import Ginny /// This extension is temporary and can be removed once Vapor gets this support. private extension Vapor.Application { @@ -29,7 +28,6 @@ enum Entrypoint { let app = Application(env) defer { app.shutdown() } - app.registerRoutes() do { try await configure(app) diff --git a/backend/Sources/App/pages/category.swift b/backend/Sources/App/pages/category.swift index 697dc98..db236a0 100644 --- a/backend/Sources/App/pages/category.swift +++ b/backend/Sources/App/pages/category.swift @@ -7,10 +7,10 @@ import Vapor import Foundation -import Ginny struct UploadCategoryHandler: AsyncRequestHandler { var method: HTTPMethod { .POST } + var path: String { "category" } func handle(req: Request) async throws -> some AsyncResponseEncodable { let request = try req.content.decode(UploadCategoryRequest.self) diff --git a/backend/Sources/App/pages/next-race.swift b/backend/Sources/App/pages/next-race.swift index 99f21f7..572a2f6 100644 --- a/backend/Sources/App/pages/next-race.swift +++ b/backend/Sources/App/pages/next-race.swift @@ -7,13 +7,20 @@ import Foundation import Vapor -import Ginny struct NextRaceHandler: AsyncRequestHandler { var method: HTTPMethod { .GET } - + var path: String { "next-race" } + + @Sendable func handle(req: Request) async throws -> some AsyncResponseEncodable { - let args = try req.query.decode(NextRaceRequest.self) + var args: String + do { + args = try req.query.decode(NextRaceRequest.self).argument + } catch { + args = "" + } + let currentDate = Date() let query = Race @@ -24,8 +31,8 @@ struct NextRaceHandler: AsyncRequestHandler { .with(\.$events) .with(\.$category) - if !args.argument.isEmpty { - query.filter(Category.self, \.$tag, .equal, args.argument) + if !args.isEmpty { + query.filter(Category.self, \.$tag, .equal, args) } let nextRace = try await query.first() diff --git a/backend/Sources/App/pages/race.swift b/backend/Sources/App/pages/race.swift index 9d314e2..d93680f 100644 --- a/backend/Sources/App/pages/race.swift +++ b/backend/Sources/App/pages/race.swift @@ -7,10 +7,10 @@ import Vapor import Foundation -import Ginny struct UploadRaceHandler: AsyncRequestHandler { var method: HTTPMethod { .POST } + var path: String { "race" } func handle(req: Request) async throws -> some AsyncResponseEncodable { let request = try req.content.decode(UploadRaceRequest.self) diff --git a/deploy.sh b/deploy.sh index 7cdcf0e..ac6af20 100644 --- a/deploy.sh +++ b/deploy.sh @@ -2,9 +2,12 @@ git fetch git pull origin main -cd telegram -swift build -sudo systemctl restart landinho.service -cd .. -cd backend -docker compose up -d \ No newline at end of file +( + cd telegram + swift build + sudo systemctl restart landinho.service +) +( + cd backend + docker compose up -d --build +) diff --git a/telegram/Dockerfile b/telegram/Dockerfile new file mode 100644 index 0000000..2a302e8 --- /dev/null +++ b/telegram/Dockerfile @@ -0,0 +1,18 @@ +# Use an official Swift runtime as a parent image +FROM swift:5.9-jammy + +# Set the working directory to /app +WORKDIR /app + +# Copy the Package.swift and all source files into the container +COPY ./Package.* ./ +RUN swift package resolve --skip-update \ + "$([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)" + +COPY . . + +# Build the Swift package +RUN swift build + +# Specify the command to run your Swift package +CMD ["./.build/debug/LandinhoBot"] diff --git a/telegram/Sources/LandinhoBot/LandinhoBot.swift b/telegram/Sources/LandinhoBot/LandinhoBot.swift index b73538e..5786dbc 100644 --- a/telegram/Sources/LandinhoBot/LandinhoBot.swift +++ b/telegram/Sources/LandinhoBot/LandinhoBot.swift @@ -1,6 +1,11 @@ @main -public struct LandinhoBot { - public static func main() { - DefaultVroomBot() - } +public class LandinhoBot { + init(bot: DefaultVroomBot) { + self.bot = bot + } + let bot: DefaultVroomBot + + public static func main() { + _ = LandinhoBot(bot: .init()) + } } diff --git a/telegram/Sources/LandinhoBot/SwiftBot/SwiftyBot.swift b/telegram/Sources/LandinhoBot/SwiftBot/SwiftyBot.swift index 0a80ea3..9f74551 100644 --- a/telegram/Sources/LandinhoBot/SwiftBot/SwiftyBot.swift +++ b/telegram/Sources/LandinhoBot/SwiftBot/SwiftyBot.swift @@ -1,4 +1,7 @@ import Foundation +#if os(Linux) +import FoundationNetworking +#endif import TelegramBotSDK open class SwiftyBot { @@ -150,25 +153,33 @@ final class DefaultVroomBot: SwiftyBot { Task { try await self.handleNextRace(update: update, args: args) } - }) + }) ] } func handleNextRace(update: Update, args: [String]) async throws { - var categoryTag = args.dropFirst().first ?? "" + let categoryTag = args.dropFirst().first ?? "" guard let url = self.buildURL(path: "next-race", args: ["argument": categoryTag]) else { + bot.reply(update, text: "Internal error") return } - let data = try await URLSession.shared.data(from: url) + let data = try await URLSession.shared.data(url: url) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 - let response = try decoder.decode(NextRaceResponse.self, from: data.0) - guard let formattedRace = formatRace(response: response) else { return } - bot.reply(update, text: formattedRace) + do { + let response = try decoder.decode(NextRaceResponse.self, from: data) + guard let formattedRace = formatRace(response: response) else { + bot.reply(update, text: "Couldn't find next race") + return + } + bot.reply(update, text: formattedRace) + } catch (let error) { + bot.reply(update, text: "\(error)") + } } func buildURL(path: String, args: [String: String]) -> URL? { @@ -199,6 +210,31 @@ final class DefaultVroomBot: SwiftyBot { } func formatEvent(_ event: RaceEvent) -> String { - "\(event.date.formatted()) \(event.title)" + "\(Self.formatter.string(from: event.date)) - \(event.title)" + } + + static let formatter = { + let f = DateFormatter() + f.dateFormat = "MM/dd/yy HH:mm" + return f + }() +} + +extension URLSession { + func data(url: URL) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + let request = URLRequest(url: url) + let task = dataTask(with: request) { data, _, error in + guard let data else { + if let error { + continuation.resume(throwing: error) + } + return + } + + continuation.resume(returning: data) + } + task.resume() + } } }