commit 77672b5c2a6082b73da5a7c86062375c6abef36b Author: kogeletey Date: Sat Oct 19 17:39:04 2024 +0300 init: start bot write of swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8695f3a --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +ISC License + +Copyright 2024 Oleh Nersh and Konrad Geletey + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..69bae57 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,231 @@ +{ + "originHash" : "5b140fcd6a36165fb4c1302609ef5353bacb59ce0f82996aa0cf66ee021cbb17", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "0a9b72369b9d87ab155ef585ef50700a34abf070", + "version" : "1.23.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", + "version" : "1.20.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "78c0dd739df8cb9ee14a8bbbf770facc4fc3402a", + "version" : "4.15.0" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "a31236f24bfd2ea2f520a74575881f6731d7ae68", + "version" : "4.7.0" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "8c9a227476555c55837e569be71944e02a056b72", + "version" : "4.9.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "21f7878f2b39d46fd8ba2b06459ccb431cdf876c", + "version" : "3.8.1" + } + }, + { + "identity" : "swift-custom-logger", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nerzh/swift-custom-logger", + "state" : { + "revision" : "b76cd97634c3d23348f041ca161187dac8bf6fe3", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "e0165b53d49b413dd987526b641e05e246782685", + "version" : "2.5.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "f7dc3f527576c398709b017584392fb58592e7f5", + "version" : "2.75.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca", + "version" : "1.34.1" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "d7ceaf0e4d8001cd35cdc12e42cdd281e9e564e8", + "version" : "2.28.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "dbace16f126fdcd80d58dc54526c561ca17327d7", + "version" : "1.22.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-regular-expression", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nerzh/swift-regular-expression", + "state" : { + "revision" : "19c3e569a8e81da6f7f4ee3b73028c25737d3706", + "version" : "0.2.4" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-telegram-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nerzh/swift-telegram-sdk", + "state" : { + "revision" : "1be977abc50e9e6aed5a60f1d1d27e25e635da9b", + "version" : "3.6.0" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "1466c50e4ad39072143e2fcdf13b4ba11be375a0", + "version" : "4.106.0" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..ef9e02b --- /dev/null +++ b/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + + +var packageDependencies: [Package.Dependency] = [ + .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.57.0")), +] + +var targetDependencies: [PackageDescription.Target.Dependency] = [ + .product(name: "Vapor", package: "vapor"), +] + +packageDependencies.append(.package(url: "https://github.com/nerzh/swift-telegram-sdk", .upToNextMajor(from: "3.0.3"))) +targetDependencies.append(.product(name: "SwiftTelegramSdk", package: "swift-telegram-sdk")) + +let package = Package( + name: "TelegramModeratorBot", + dependencies: packageDependencies, + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "TelegramModeratorBot", + dependencies: targetDependencies + ), + ] +) diff --git a/Sources/TelegramModeratorBot/DefaultBotHandlers.swift b/Sources/TelegramModeratorBot/DefaultBotHandlers.swift new file mode 100644 index 0000000..ef4039f --- /dev/null +++ b/Sources/TelegramModeratorBot/DefaultBotHandlers.swift @@ -0,0 +1,18 @@ +import Vapor +import SwiftTelegramSdk + +final class DefaultBotHandlers { + static func addHandlers() async { + await defaultBaseHandler() + } + + private static func defaultBaseHandler() async { + await botActor.bot.dispatcher.add(TGBaseHandler({ update in + guard let message = update.message else { return } + let params: TGSendMessageParams = .init(chatId: .chat(message.chat.id), text: """ + Help message + """) + try await botActor.bot.sendMessage(params: params) + })) + } +} diff --git a/Sources/TelegramModeratorBot/TGBotActor.swift b/Sources/TelegramModeratorBot/TGBotActor.swift new file mode 100644 index 0000000..2ea8509 --- /dev/null +++ b/Sources/TelegramModeratorBot/TGBotActor.swift @@ -0,0 +1,13 @@ +import SwiftTelegramSdk + +actor TGBotActor { + private var _bot: TGBot! + + var bot: TGBot { + self._bot + } + + func setBot(_ bot: TGBot) { + self._bot = bot + } +} diff --git a/Sources/TelegramModeratorBot/VaporTGClient.swift b/Sources/TelegramModeratorBot/VaporTGClient.swift new file mode 100644 index 0000000..c828a27 --- /dev/null +++ b/Sources/TelegramModeratorBot/VaporTGClient.swift @@ -0,0 +1,110 @@ +// +// VaporTGClient.swift +// Vapor-telegram-bot-example +// +// Created by Oleh Hudeichuk on 01.07.2024. +// + +import Foundation +import Vapor +import SwiftTelegramSdk + +public enum TGHTTPMediaType: String, Equatable { + case formData + case json +} + +private struct TGEmptyParams: Encodable {} + +public final class VaporTGClient: TGClientPrtcl { + + public typealias HTTPMediaType = SwiftTelegramSdk.HTTPMediaType + public var log: Logging.Logger = .init(label: "VaporTGClient") + private let client: Vapor.Client + + public init(client: Vapor.Client) { + self.client = client + } + + @discardableResult + public func post + ( + _ url: URL, + params: Params? = nil, + as mediaType: HTTPMediaType? = nil + ) async throws -> Response { + let clientResponse: ClientResponse = try await client.post(URI(string: url.absoluteString), headers: HTTPHeaders()) { clientRequest in + if mediaType == .formData || mediaType == nil { + #warning("THIS CODE FOR FAST FIX, BECAUSE https://github.com/vapor/multipart-kit/issues/63 not accepted yet") + var rawMultipart: (body: NSMutableData, boundary: String)! + do { + /// Content-Disposition: form-data; name="nested_object" + /// + /// { json string } + if let currentParams: Params = params { + rawMultipart = try currentParams.toMultiPartFormData(log: log) + } else { + rawMultipart = try TGEmptyParams().toMultiPartFormData(log: log) + } + } catch { + log.critical("Post request error: \(error.logMessage)") + } + clientRequest.headers.add(name: "Content-Type", value: "multipart/form-data; boundary=\(rawMultipart.boundary)") + let buffer = ByteBuffer.init(data: rawMultipart.body as Data) + clientRequest.body = buffer + /// Debug + // TGBot.log.critical("url: \(url)\n\(String(decoding: rawMultipart.body, as: UTF8.self))") + } else { + let mediaType: Vapor.HTTPMediaType = if let mediaType { + .init(type: mediaType.type, subType: mediaType.subType, parameters: mediaType.parameters) + } else { + .json + } + try clientRequest.content.encode(params ?? (TGEmptyParams() as! Params), as: mediaType) + } + } + let telegramContainer: TGTelegramContainer = try clientResponse.content.decode(TGTelegramContainer.self) + return try processContainer(telegramContainer) + } + + @discardableResult + public func post(_ url: URL) async throws -> Response { + try await post(url, params: TGEmptyParams(), as: nil) + } + + private func processContainer(_ container: TGTelegramContainer) throws -> T { + guard container.ok else { + let desc = """ + Response marked as `not Ok`, it seems something wrong with request + Code: \(container.errorCode ?? -1) + \(container.description ?? "Empty") + """ + let error = BotError( + type: .server, + description: desc + ) + log.error(error.logMessage) + throw error + } + + guard let result = container.result else { + let error = BotError( + type: .server, + reason: "Response marked as `Ok`, but doesn't contain `result` field." + ) + log.error(error.logMessage) + throw error + } + + let logString = """ + + Response: + Code: \(container.errorCode ?? 0) + Status OK: \(container.ok) + Description: \(container.description ?? "Empty") + + """ + log.trace(logString.logMessage) + return result + } +} diff --git a/Sources/TelegramModeratorBot/configure.swift b/Sources/TelegramModeratorBot/configure.swift new file mode 100644 index 0000000..9d8f585 --- /dev/null +++ b/Sources/TelegramModeratorBot/configure.swift @@ -0,0 +1,20 @@ +import Foundation +import Vapor +import SwiftTelegramSdk + +public func configure(_ app: Application) async throws { + guard let tgApi: String = Environment.get("TG_BOT_API") else { throw Errors.notVariable("Telegram key is not defined")} + app.logger.logLevel = .debug + let bot: TGBot = try await .init(connectionType: .longpolling(limit: nil, + timeout: nil, + allowedUpdates: nil), + dispatcher: nil, + tgClient: VaporTGClient(client: app.client), + tgURI: TGBot.standardTGURL, + botId: tgApi, + log: app.logger) + await botActor.setBot(bot) + await DefaultBotHandlers.addHandlers() + try await botActor.bot.start() + // try routes(app) +} diff --git a/Sources/TelegramModeratorBot/errors.swift b/Sources/TelegramModeratorBot/errors.swift new file mode 100644 index 0000000..74146bd --- /dev/null +++ b/Sources/TelegramModeratorBot/errors.swift @@ -0,0 +1,3 @@ +enum Errors: Error { + case notVariable(String) +} diff --git a/Sources/TelegramModeratorBot/main.swift b/Sources/TelegramModeratorBot/main.swift new file mode 100644 index 0000000..230fce6 --- /dev/null +++ b/Sources/TelegramModeratorBot/main.swift @@ -0,0 +1,18 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import Vapor +import SwiftTelegramSdk + +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) + +let eventLoop: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount * 4) +let app: Application = try await Application.make(env, Application.EventLoopGroupProvider.shared(eventLoop)) + +let botActor: TGBotActor = .init() + +defer { app.shutdown() } + +try await configure(app) +try await app.execute() diff --git a/Sources/TelegramModeratorBot/routes.swift b/Sources/TelegramModeratorBot/routes.swift new file mode 100644 index 0000000..236285c --- /dev/null +++ b/Sources/TelegramModeratorBot/routes.swift @@ -0,0 +1,8 @@ + +/* +import Vapor + +func routes(_ app: Application) throws { + try app.register(collection: TelegramController()) +} +*/