├── logo.png ├── .gitignore ├── .sourcery.yml ├── Tests ├── LinuxMain.swift └── BotterTests │ ├── XCTestManifests.swift │ └── BotterTests.swift ├── Sources ├── Botter │ ├── Protocols │ │ ├── Sourcery.swift │ │ ├── PlatformObject.swift │ │ ├── Attachable.swift │ │ ├── UserFetchable.swift │ │ └── Replyable.swift │ ├── Filters │ │ ├── AllFilter.swift │ │ ├── CommandFilter.swift │ │ ├── TextFilter.swift │ │ ├── PhotoFilter.swift │ │ ├── DocumentFilter.swift │ │ ├── AudioFilter.swift │ │ ├── GameFilter.swift │ │ ├── VenueFilter.swift │ │ ├── VideoFilter.swift │ │ ├── VoiceFilter.swift │ │ ├── GroupFilter.swift │ │ ├── ContactFilter.swift │ │ ├── InvoiceFilter.swift │ │ ├── StickerFilter.swift │ │ ├── LocationFilter.swift │ │ ├── PrivateFilter.swift │ │ ├── ReplyFilter.swift │ │ ├── VideoNoteFilter.swift │ │ ├── SuccesfulPaymentFilter.swift │ │ ├── RegexpFilter.swift │ │ ├── ForwarderFilter.swift │ │ ├── EntityFilter.swift │ │ ├── LanguageFilter.swift │ │ ├── CaptionEntityFilter.swift │ │ ├── ChatFilter.swift │ │ ├── Filter.swift │ │ ├── UserFilter.swift │ │ └── StatusUpdateFilters.swift │ ├── Network │ │ ├── Network.swift │ │ ├── BaseWebhooks.swift │ │ ├── Webhooks.swift │ │ └── UpdatesServer.swift │ ├── Dispatcher │ │ ├── HandlerGroup.swift │ │ ├── BaseDispatcher.swift │ │ └── Dispatcher.swift │ ├── Types │ │ ├── InputFile.swift │ │ ├── DestinationId.swift │ │ ├── Encodables.swift │ │ ├── Keyboard.swift │ │ ├── FileInfo.swift │ │ ├── Attachment.swift │ │ ├── Button.swift │ │ └── Platform.swift │ ├── Helpers │ │ ├── JSONCoder+snakeCased.swift │ │ ├── WebhooksConfig+baseUrlInit.swift │ │ ├── Error+Helper.swift │ │ ├── Future+throwingFlatMap.swift │ │ ├── String+Helper.swift │ │ └── String+mimeType.swift │ ├── Handlers │ │ ├── TgSimpleCallbackQueryHandler.swift │ │ ├── MessageEventHandler.swift │ │ ├── Handler.swift │ │ ├── CommandHandler.swift │ │ └── MessageHandler.swift │ ├── Bot │ │ ├── Methods │ │ │ ├── Bot+getUser.swift │ │ │ ├── Bot+forwardMessage.swift │ │ │ ├── Bot+sendMessageEventAnswer.swift │ │ │ ├── Bot+editMessage.swift │ │ │ └── Bot+sendMessage.swift │ │ └── Models │ │ │ ├── MessageEvent.swift │ │ │ ├── User.swift │ │ │ ├── Document.swift │ │ │ ├── Update.swift │ │ │ ├── Photo.swift │ │ │ └── Message.swift │ ├── Errors │ │ └── BotError.swift │ ├── Botter.swift │ ├── Updater │ │ └── Updater.swift │ └── Generated │ │ └── AutoCodable.generated.swift └── BotterDemoEchoBot │ ├── openUrl.swift │ └── main.swift ├── .github └── workflows │ ├── macos.yml │ └── ubuntu.yml ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── Package.resolved └── Templates └── AutoCodable.swifttemplate /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoolONEOfficial/Botter/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm/ 7 | -------------------------------------------------------------------------------- /.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | - Sources/Botter/ 3 | - Tests/ 4 | templates: 5 | - Templates/ 6 | output: 7 | Sources/Botter/Generated/ 8 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import BotterTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += BotterTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/BotterTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(BotterTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/Botter/Protocols/Sourcery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 08.01.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AutoDecodable: Decodable {} 11 | protocol AutoEncodable: Encodable {} 12 | protocol AutoCodable: AutoEncodable, AutoDecodable {} 13 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | -------------------------------------------------------------------------------- /Sources/Botter/Protocols/PlatformObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 21.12.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol PlatformObject: Codable { 11 | associatedtype Tg: Codable 12 | associatedtype Vk: Codable 13 | 14 | var platform: Platform { get } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/AllFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | /// Filter for any update, said "no filter" 13 | public extension Filters { 14 | static var all = Filters(vk: Vkontakter.Filters.all, tg: Telegrammer.Filters.all) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/CommandFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | /// Messages which contains command 13 | public extension Filters { 14 | static var command = Filters(vk: Vkontakter.Filters.command, tg: Telegrammer.Filters.command) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/TextFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | /// Filters messages to allow only those which contains text 13 | public extension Filters { 14 | static var text = Filters(vk: Vkontakter.Filters.text, tg: Telegrammer.Filters.text) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/PhotoFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | /// Filters messages to allow only those which contains photo 13 | public extension Filters { 14 | static var photo = Filters(vk: Vkontakter.Filters.photo, tg: Telegrammer.Filters.photo) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/DocumentFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | /// Filters messages to allow only those which contains document 13 | public extension Filters { 14 | static var document = Filters(vk: Vkontakter.Filters.document, tg: Telegrammer.Filters.document) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Sources/Botter/Network/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 09.04.2018. 6 | // 7 | 8 | import NIO 9 | import AsyncHTTPClient 10 | 11 | /// Convenience shorthand for `EventLoopFuture`. 12 | public typealias Future = EventLoopFuture 13 | 14 | /// Convenience shorthand for `EventLoopPromise`. 15 | public typealias Promise = EventLoopPromise 16 | 17 | public protocol Connection { 18 | var worker: Worker { get } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/BotterTests/BotterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Botter 3 | 4 | final class BotterTests: XCTestCase { 5 | // func testExample() { 6 | // // This is an example of a functional test case. 7 | // // Use XCTAssert and related functions to verify your tests produce the correct 8 | // // results. 9 | // XCTAssertEqual(Bot().text, "Hello, World!") 10 | // } 11 | // 12 | // static var allTests = [ 13 | // ("testExample", testExample), 14 | // ] 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/AudioFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Audio` 11 | //public struct AudioFilter: Filter { 12 | // public var name: String = "audio" 13 | // 14 | // public func filter(message: Message) -> Bool { 15 | // return message.audio != nil 16 | // } 17 | //} 18 | // 19 | //public extension Filters { 20 | // static var audio = Filters(filter: AudioFilter()) 21 | //} 22 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/GameFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Game` 11 | //public struct GameFilter: Filter { 12 | // 13 | // public var name: String = "forwarded" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.game != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var game = Filters(filter: GameFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/VenueFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VenueFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Vanue` 11 | //public struct VenueFilter: Filter { 12 | // 13 | // public var name: String = "venue" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.venue != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var venue = Filters(filter: VenueFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/VideoFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Video` 11 | //public struct VideoFilter: Filter { 12 | // 13 | // public var name: String = "video" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.video != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var video = Filters(filter: VideoFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/VoiceFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VoiceFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Voice` 11 | //public struct VoiceFilter: Filter { 12 | // 13 | // public var name: String = "voice" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.voice != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var voice = Filters(filter: VoiceFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/GroupFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages sent in a group chat 11 | //public struct GroupFilter: Filter { 12 | // 13 | // public var name: String = "group" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.chat.type != .private 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var group = Filters(filter: GroupFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/ContactFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Contact` 11 | //public struct ContactFilter: Filter { 12 | // 13 | // public var name: String = "contact" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.contact != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var contact = Filters(filter: ContactFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/InvoiceFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvoiceFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Invoice` 11 | //public struct InvoiceFilter: Filter { 12 | // 13 | // public var name: String = "invoice" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.invoice != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var invoice = Filters(filter: InvoiceFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/StickerFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StickerFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Sticker` 11 | //public struct StickerFilter: Filter { 12 | // 13 | // public var name: String = "sticker" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.sticker != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var sticker = Filters(filter: StickerFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | Please feel free to contribute. Check out our issues list. 4 | 5 | ## Pull requests 6 | The best way to contribute is by submitting a pull request. 7 | I'll respond as soon as possible. 8 | 9 | ## Issues 10 | If you find a bug or you have a suggestion create an issue. 11 | 12 | ## Coding style 13 | 14 | Comment every public methods, properties, classes. 15 | Make commits as atomic as possible with undestandable comment. 16 | If you are developing feature of fixing bug, please mention issue number (e.g. #222) in commit text. 17 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/LocationFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `Location` 11 | //public struct LocationFilter: Filter { 12 | // 13 | // public var name: String = "location" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.location != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var location = Filters(filter: LocationFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/PrivateFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrivateFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages sent in a private chat 11 | //public struct PrivateFilter: Filter { 12 | // 13 | // public var name: String = "private" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.chat.type == .private 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var `private` = Filters(filter: PrivateFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/ReplyFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that are a reply to another message 11 | //public struct ReplyFilter: Filter { 12 | // 13 | // public var name: String = "reply" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.replyToMessage != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var reply = Filters(filter: ReplyFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/VideoNoteFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoNoteFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that contain `VideoNote` 11 | //public struct VideoNoteFilter: Filter { 12 | // 13 | // public var name: String = "video_note" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.videoNote != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var videoNote = Filters(filter: VideoNoteFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Dispatcher/HandlerGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 06.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | public struct HandlerGroup: Hashable { 13 | public init(id: UInt, name: String) { 14 | self.init(vk: .init(id: id, name: name), tg: .init(id: id, name: name)) 15 | } 16 | 17 | public init(vk: Vkontakter.HandlerGroup, tg: Telegrammer.HandlerGroup) { 18 | self.vk = vk 19 | self.tg = tg 20 | } 21 | 22 | let vk: Vkontakter.HandlerGroup 23 | let tg: Telegrammer.HandlerGroup 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/SuccesfulPaymentFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuccesfulPaymentFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters messages that contains a `SuccessfulPayment`. 11 | //public struct SuccesfulPaymentFilter: Filter { 12 | // 13 | // public var name: String = "successful_payment" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.successfulPayment != nil 17 | // } 18 | //} 19 | // 20 | //public extension Filters { 21 | // static var successfulPayment = Filters(filter: SuccesfulPaymentFilter()) 22 | //} 23 | -------------------------------------------------------------------------------- /Sources/Botter/Types/InputFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 11.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | public struct InputFile: Codable { 13 | 14 | let data: Data 15 | let filename: String 16 | 17 | public init(data: Data, filename: String) { 18 | self.data = data 19 | self.filename = filename 20 | } 21 | 22 | var vk: Vkontakter.InputFile { 23 | .init(data: data, filename: filename) 24 | } 25 | 26 | var tg: Telegrammer.InputFile { 27 | .init(data: data, filename: filename) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/RegexpFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegexpFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | /// Filters updates by searching for an occurence of pattern in the message text. The `NSRegularExpression` is used to determine whether an update should be filtered. Refer to the documentation of the `NSRegularExpression` for more information. 13 | public extension Filters { 14 | static func regexp(pattern: String, options: NSRegularExpression.Options = []) -> Filters { 15 | return Filters(vk: .regexp(pattern: pattern, options: options), tg: .regexp(pattern: pattern, options: options)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/JSONCoder+snakeCased.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 02.12.2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension JSONDecoder { 11 | static var snakeCased: JSONDecoder = { 12 | let decoder = JSONDecoder() 13 | decoder.keyDecodingStrategy = .convertFromSnakeCase 14 | decoder.dateDecodingStrategy = .iso8601 15 | return decoder 16 | }() 17 | } 18 | 19 | public extension JSONEncoder { 20 | static var snakeCased: JSONEncoder = { 21 | let encoder = JSONEncoder() 22 | encoder.keyEncodingStrategy = .convertToSnakeCase 23 | encoder.dateEncodingStrategy = .iso8601 24 | return encoder 25 | }() 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Botter/Network/BaseWebhooks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseWebhooks.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 19.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Vapor 12 | 13 | protocol BaseWebhooks { 14 | func start(vkServerName: String?) throws -> EventLoopFuture 15 | func stop() throws -> EventLoopFuture 16 | } 17 | 18 | extension Vkontakter.Webhooks: BaseWebhooks { 19 | func start(vkServerName: String?) throws -> EventLoopFuture { 20 | try start(serverName: vkServerName) 21 | } 22 | } 23 | extension Telegrammer.Webhooks: BaseWebhooks { 24 | func start(vkServerName: String?) throws -> EventLoopFuture { 25 | try start() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/ForwarderFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForwarderFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Messages that are forwarded. 11 | //public struct ForwarderFilter: Filter { 12 | // 13 | // public var name: String = "forwarded" 14 | // 15 | // public func filter(message: Message) -> Bool { 16 | // return message.forwardDate != nil || 17 | // message.forwardFrom != nil || 18 | // message.forwardFromChat != nil || 19 | // message.forwardSignature != nil || 20 | // message.forwardFromMessageId != nil 21 | // } 22 | //} 23 | // 24 | //public extension Filters { 25 | // static var forwarded = Filters(filter: ForwarderFilter()) 26 | //} 27 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/WebhooksConfig+baseUrlInit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 19.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | // MARK: - Webhooks config inits 13 | 14 | extension Vkontakter.Webhooks.Config { 15 | public init(ip: String, baseUrl: String, port: Int, groupId: UInt64? = nil) { 16 | self.init(ip: ip, url: baseUrl + "/vk", port: port, groupId: groupId) 17 | } 18 | } 19 | 20 | extension Telegrammer.Webhooks.Config { 21 | public init(ip: String, baseUrl: String, port: Int, publicCert: Telegrammer.Webhooks.Config.Certificate? = nil) { 22 | self.init(ip: ip, url: baseUrl + "/tg", port: port, publicCert: publicCert) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Botter/Types/DestinationId.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 14.01.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | //public enum DestinationId: AutoCodable { 11 | // /// Идентификатор чата, которому отправляется сообщение. 12 | // case chatId(Int64) 13 | // 14 | // var chatId: Int64? { 15 | // if case let .chatId(chatId) = self { 16 | // return chatId 17 | // } 18 | // return nil 19 | // } 20 | // 21 | // /// Идентификатор пользователя, которому отправляется сообщение 22 | // case userId(Int64) 23 | // 24 | // var userId: Int64? { 25 | // if case let .userId(userId) = self { 26 | // return userId 27 | // } 28 | // return nil 29 | // } 30 | //} 31 | -------------------------------------------------------------------------------- /Sources/Botter/Handlers/TgSimpleCallbackQueryHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TgSimpleCallbackQueryHandler.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 23.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | 11 | /// Handler for CallbackQuery updates 12 | struct TgSimpleCallbackQueryHandler: Telegrammer.Handler { 13 | 14 | var name: String 15 | 16 | let callback: Telegrammer.HandlerCallback 17 | 18 | public func check(update: Telegrammer.Update) -> Bool { 19 | update.callbackQuery?.data != nil 20 | } 21 | 22 | public func handle(update: Telegrammer.Update, dispatcher: Telegrammer.Dispatcher) async { 23 | do { 24 | try await callback(update, nil) 25 | } catch { 26 | log.error(error.logMessage) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Botter/Types/Encodables.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodables.swift 3 | // App 4 | // 5 | // Created by Givi Pataridze on 07.04.2018. 6 | // 7 | 8 | import Foundation 9 | import struct NIO.ByteBufferAllocator 10 | 11 | /// Represent Telegram type, which will be encoded as Json on sending to server 12 | protocol JSONEncodable: Encodable {} 13 | 14 | extension JSONEncodable { 15 | var dictionary: [String: String]? { 16 | guard let data = try? JSONEncoder().encode(self) else { return nil } 17 | let test = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { dict in 18 | return dict as? [String: Any] 19 | } 20 | let mapped = test?.compactMapValues { val in 21 | val is Dictionary 22 | ? String(data: try! JSONSerialization.data(withJSONObject: val, options: .fragmentsAllowed), encoding: .utf8) 23 | : String(describing: val) 24 | } 25 | return mapped 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/EntityFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters messages to only allow those which have a `MessageEntity` where their type matches `type`. 11 | //public struct EntityFilter: Filter { 12 | // 13 | // let entityTypes: Set 14 | // 15 | // public init(types: [MessageEntityType]) { 16 | // self.entityTypes = Set(types) 17 | // } 18 | // 19 | // public var name: String = "entity" 20 | // 21 | // public func filter(message: Message) -> Bool { 22 | // guard let entities = message.entities else { return false } 23 | // let incomingTypes = entities.map { $0.type } 24 | // return !entityTypes.isDisjoint(with: incomingTypes) 25 | // } 26 | //} 27 | // 28 | //public extension Filters { 29 | // static func entity(types: [MessageEntityType]) -> Filters { 30 | // return Filters(filter: EntityFilter(types: types)) 31 | // } 32 | //} 33 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/LanguageFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LanguageFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters messages to only allow those which are from users with a certain language code. 11 | /// 12 | /// Note: According to telegrams documentation, every single user does not have the language_code attribute. 13 | //public struct LanguageFilter: Filter { 14 | // 15 | // var lang: String 16 | // 17 | // public init(lang: String) { 18 | // self.lang = lang 19 | // } 20 | // 21 | // public var name: String = "language" 22 | // 23 | // public func filter(message: Message) -> Bool { 24 | // guard let languageCode = message.from?.languageCode else { return true } 25 | // return languageCode.starts(with: lang) 26 | // } 27 | //} 28 | // 29 | //public extension Filters { 30 | // static func language(_ lang: String) -> Filters { 31 | // return Filters(filter: LanguageFilter(lang: lang)) 32 | // } 33 | //} 34 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/CaptionEntityFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptionEntity.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 24/06/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters media messages to only allow those which have a `MessageEntity` where their type matches `type`. 11 | //public struct CaptionEntityFilter: Filter { 12 | // 13 | // var entityType: MessageEntityType 14 | // 15 | // public init(type: MessageEntityType) { 16 | // self.entityType = type 17 | // } 18 | // 19 | // public var name: String = "caption_entity" 20 | // 21 | // public func filter(message: Message) -> Bool { 22 | // guard let entities = message.entities else { return false } 23 | // return entities.contains(where: { (entity) -> Bool in 24 | // return entity.type == entityType 25 | // }) 26 | // } 27 | //} 28 | // 29 | //public extension Filters { 30 | // static func captionEntity(type: MessageEntityType) -> Filters { 31 | // return Filters(filter: CaptionEntityFilter(type: type)) 32 | // } 33 | //} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nikolai Trukhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/ChatFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters messages to allow only those which are from specified chat ID. 11 | //public struct ChatFilter: Filter { 12 | // 13 | // var chatId: Int64 14 | // var username: String? 15 | // 16 | // public init(chatId: Int64, username: String? = nil) { 17 | // self.chatId = chatId 18 | // self.username = username 19 | // } 20 | // 21 | // public var name: String = "chat" 22 | // 23 | // public func filter(message: Message) -> Bool { 24 | // guard message.chat.id == chatId else { return false } 25 | // guard let desiredUsername = username else { return true } 26 | // guard let incomingUsername = message.chat.username else { return true } 27 | // return desiredUsername == incomingUsername 28 | // } 29 | //} 30 | // 31 | //public extension Filters { 32 | // static func chat(chatId: Int64, username: String? = nil) -> Filters { 33 | // return Filters(filter: ChatFilter(chatId: chatId, username: username)) 34 | // } 35 | //} 36 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Methods/Bot+getUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.02.2021. 6 | // 7 | 8 | import Telegrammer 9 | import Vkontakter 10 | import Foundation 11 | import NIO 12 | import Vapor 13 | 14 | public extension Bot { 15 | 16 | enum GetUserError: Error { 17 | case emptyArray 18 | case fromEntryNotFound 19 | } 20 | 21 | @discardableResult 22 | func getUser(from userFetchable: UserFetchable, app: Application) throws -> Future? { 23 | switch userFetchable.userInfo { 24 | case let .id(userId): 25 | guard let userId = userId else { return nil } 26 | return try vk?.getUser(params: .init(userIds: [.id(userId)], fields: nil, nameCase: nil)).map { User(from: $0.first) }.unwrap(orError: Bot.GetUserError.emptyArray) 27 | 28 | case let .user(user): 29 | return app.eventLoopGroup.future(user).unwrap(or: GetUserError.fromEntryNotFound) 30 | } 31 | } 32 | 33 | } 34 | 35 | extension Message { 36 | var vkUserParams: Vkontakter.Bot.GetUserParams? { 37 | guard let userId = self.userId else { return nil } 38 | return .init(userIds: [.id(userId)], fields: nil, nameCase: nil) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Botter/Handlers/MessageEventHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallbackQueryHandler.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 23.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Vapor 12 | 13 | /// Handler for MessageEvent updates 14 | public class MessageEventHandler: Handler { 15 | 16 | public var name: String 17 | public let callback: HandlerCallback 18 | public var app: Application! 19 | public var bot: Botter.Bot! 20 | 21 | public lazy var vk: Vkontakter.Handler = Vkontakter.MessageEventHandler(name: name) { update, _ throws in 22 | guard let update = Update(from: update) else { return } 23 | try self.callback(update, self.context(update.platform.any)) 24 | } 25 | 26 | public lazy var tg: Telegrammer.Handler = TgSimpleCallbackQueryHandler(name: name) { update, _ throws in 27 | guard let update = Update(from: update) else { return } 28 | try self.callback(update, self.context(update.platform.any)) 29 | } 30 | 31 | public init( 32 | name: String = String(describing: MessageEventHandler.self), 33 | callback: @escaping HandlerCallback 34 | ) { 35 | self.callback = callback 36 | self.name = name 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/Filter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Filter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | ///Base protocol for atomic filter 13 | public protocol Filter { 14 | var vk: Vkontakter.Filters { get } 15 | var tg: Telegrammer.Filters { get } 16 | 17 | var name: String { get } 18 | func filter(message: Message) -> Bool 19 | } 20 | 21 | extension Filter { 22 | public func filter(message: Message) -> Bool { 23 | switch message.platform { 24 | case let .tg(tg): 25 | return self.tg.check(tg) 26 | case let .vk(vk): 27 | return self.vk.check(vk) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | Class cluster for all filters. 34 | */ 35 | public class Filters { 36 | 37 | let vk: Vkontakter.Filters 38 | let tg: Telegrammer.Filters 39 | 40 | public init(vk: Vkontakter.Filters, tg: Telegrammer.Filters) { 41 | self.vk = vk 42 | self.tg = tg 43 | } 44 | 45 | public func check(_ message: Message) -> Bool { 46 | switch message.platform { 47 | case let .tg(tg): 48 | return self.tg.check(tg) 49 | case let .vk(vk): 50 | return self.vk.check(vk) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/Error+Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error+Helper.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi on 19/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | public extension Error { 12 | var logMessage: Logger.Message { 13 | var errorDescription: String 14 | if let coreError = self as? CoreError { 15 | errorDescription = coreError.localizedDescription 16 | } else if let decodingError = self as? DecodingError { 17 | switch decodingError { 18 | case .dataCorrupted(let context): 19 | errorDescription = context.debugDescription 20 | case .keyNotFound(_, let context): 21 | errorDescription = context.debugDescription 22 | case .typeMismatch(_, let context): 23 | errorDescription = context.debugDescription 24 | case .valueNotFound(_, let context): 25 | errorDescription = context.debugDescription 26 | @unknown default: 27 | errorDescription = "Uknown DecodingError" 28 | } 29 | } else { 30 | errorDescription = "Cannot detect error type, providing default description:\n\(self.localizedDescription)" 31 | } 32 | return Logger.Message(stringLiteral: errorDescription) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Botter/Dispatcher/BaseDispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 19.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Vapor 12 | 13 | protocol BaseDispatcher { 14 | func enqueue(_ bytebuffer: ByteBuffer) 15 | func add(_ handler: Handler, to group: HandlerGroup) 16 | func remove(_ handler: Handler, from group: HandlerGroup) 17 | } 18 | 19 | extension Vkontakter.Dispatcher: BaseDispatcher { 20 | func enqueue(_ bytebuffer: ByteBuffer) { 21 | enqueue(bytebuffer: bytebuffer) 22 | } 23 | 24 | func add(_ handler: Handler, to group: HandlerGroup) { 25 | add(handler: handler.vk, to: group.vk) 26 | } 27 | 28 | func remove(_ handler: Handler, from group: HandlerGroup) { 29 | remove(handler: handler.vk, from: group.vk) 30 | } 31 | } 32 | 33 | extension Telegrammer.Dispatcher: BaseDispatcher { 34 | 35 | func enqueue(_ bytebuffer: ByteBuffer) { 36 | enqueue(bytebuffer: bytebuffer) 37 | } 38 | 39 | func add(_ handler: Handler, to group: HandlerGroup) { 40 | add(handler: handler.tg, to: group.tg) 41 | } 42 | 43 | func remove(_ handler: Handler, from group: HandlerGroup) { 44 | remove(handler: handler.tg, from: group.tg) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/BotterDemoEchoBot/openUrl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 31.03.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | func openUrl(_ port: Int = 80) -> String { 11 | 12 | let command = "ssh -R 80:localhost:\(port) localhost.run" 13 | 14 | let task = Process() 15 | let pipe = Pipe() 16 | task.standardOutput = pipe 17 | task.arguments = ["-c", command] 18 | task.launchPath = "/bin/zsh" 19 | task.launch() 20 | sleep(5) 21 | task.interrupt() 22 | 23 | let bgTask = Process() 24 | bgTask.arguments = ["-c", command] 25 | bgTask.launchPath = "/bin/zsh" 26 | bgTask.launch() 27 | 28 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 29 | let output = String(data: data, encoding: .utf8)! 30 | 31 | func matches(for regex: String, in text: String) -> [String] { 32 | 33 | do { 34 | let regex = try NSRegularExpression(pattern: regex) 35 | let results = regex.matches(in: text, 36 | range: NSRange(text.startIndex..., in: text)) 37 | return results.map { 38 | String(text[Range($0.range, in: text)!]) 39 | } 40 | } catch let error { 41 | print("invalid regex: \(error.localizedDescription)") 42 | return [] 43 | } 44 | } 45 | 46 | let res = matches(for: "\\S+(localhost.run)", in: output).last! 47 | 48 | return res 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Botter/Network/Webhooks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Webhooks.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.12.2020. 6 | // 7 | 8 | import Foundation 9 | import AsyncHTTPClient 10 | import NIO 11 | import Vkontakter 12 | import Telegrammer 13 | 14 | /// Will take care of you webhooks updates 15 | public class Webhooks: Connection { 16 | 17 | public let vk: Vkontakter.Webhooks? 18 | public let tg: Telegrammer.Webhooks? 19 | 20 | private let webhooks: [BaseWebhooks] 21 | 22 | public var worker: Worker 23 | 24 | public init(bot: Bot, dispatcher: Dispatcher, worker: Worker = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)) { 25 | self.worker = worker 26 | if let vkBot = bot.vk, let vkDispatcher = dispatcher.vk { 27 | vk = .init(bot: vkBot, dispatcher: vkDispatcher, worker: worker) 28 | } else { 29 | vk = nil 30 | } 31 | if let tgBot = bot.tg, let tgDispatcher = dispatcher.tg { 32 | tg = .init(bot: tgBot, dispatcher: tgDispatcher, worker: worker) 33 | } else { 34 | tg = nil 35 | } 36 | webhooks = ([ tg, vk ] as [BaseWebhooks?]).compactMap { $0 } 37 | } 38 | 39 | public func start(vkServerName: String?) throws -> EventLoopFuture { 40 | try webhooks.map { try $0.start(vkServerName: vkServerName) }.flatten(on: worker.next()) 41 | } 42 | 43 | public func stop() throws -> EventLoopFuture { 44 | try webhooks.map { try $0.stop() }.flatten(on: worker.next()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Botter/Handlers/Handler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 05.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Vapor 12 | 13 | public typealias HandlerCallback = (_ update: Update, _ context: BotContextProtocol) throws -> Void 14 | 15 | /** 16 | Protocol for any update handler 17 | 18 | Every handler must implement `check` and `handle` methods 19 | */ 20 | public protocol Handler { 21 | var name: String { get } 22 | var vk: Vkontakter.Handler { get } 23 | var tg: Telegrammer.Handler { get } 24 | var callback: HandlerCallback { get } 25 | var app: Application! { get set } 26 | var bot: Bot! { get set } 27 | 28 | func check(update: Update) -> Bool 29 | func handle(update: Update, dispatcher: Dispatcher) 30 | } 31 | 32 | extension Handler { 33 | internal func context(_ platform: AnyPlatform) -> BotContext { 34 | .init(app: app, bot: bot, platform: platform) 35 | } 36 | 37 | public var name: String { 38 | return String(describing: Self.self) 39 | } 40 | 41 | public func check(update: Update) -> Bool { 42 | switch update.platform { 43 | case let .tg(tg): 44 | return self.tg.check(update: tg) 45 | case let .vk(vk): 46 | return self.vk.check(update: vk) 47 | } 48 | } 49 | 50 | public func handle(update: Update, dispatcher: Dispatcher) { 51 | do { 52 | let context = BotContext(app: app, bot: bot, platform: update.platform.any) 53 | try callback(update, context) 54 | } catch { 55 | log.error(error.logMessage) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/Future+throwingFlatMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 01.04.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Future { 11 | public func throwingFlatMap(_ transform: @escaping (Value) throws -> Future) -> Future { 12 | flatMap { value in 13 | do { 14 | return try transform(value) 15 | } catch { 16 | return self.eventLoop.makeFailedFuture(error) 17 | } 18 | } 19 | } 20 | 21 | public func optionalThrowingFlatMap( 22 | _ closure: @escaping (_ unwrapped: Wrapped) throws -> Future 23 | ) -> Future where Value == Optional { 24 | return self.flatMap { optional in 25 | do { 26 | guard let future = try optional.map(closure) else { 27 | return self.eventLoop.makeSucceededFuture(nil) 28 | } 29 | 30 | return future.map(Optional.init) 31 | } catch { 32 | return self.eventLoop.makeFailedFuture(error) 33 | } 34 | } 35 | } 36 | 37 | public func optionalThrowingFlatMap( 38 | _ closure: @escaping (_ unwrapped: Wrapped) throws -> Future 39 | ) -> Future where Value == Optional { 40 | return self.flatMap { optional in 41 | do { 42 | return try optional.flatMap(closure)?.map { $0 } ?? self.eventLoop.makeSucceededFuture(nil) 43 | } catch { 44 | return self.eventLoop.makeFailedFuture(error) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/MessageEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 21.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import AnyCodable 12 | 13 | public struct MessageEvent { 14 | 15 | public let id: String 16 | 17 | public let data: AnyCodable 18 | 19 | public var peerId: Int64? 20 | 21 | public var fromId: Int64? 22 | 23 | public let platform: Platform 24 | 25 | public func decodeData(decoder: JSONDecoder = .snakeCased) throws -> T { 26 | try decoder.decode(T.self, from: JSONSerialization.data(withJSONObject: data.value)) 27 | } 28 | 29 | } 30 | 31 | extension MessageEvent: PlatformObject { 32 | 33 | public typealias Tg = Telegrammer.CallbackQuery 34 | public typealias Vk = Vkontakter.MessageEvent 35 | 36 | init?(from tg: Tg) { 37 | platform = .tg(tg) 38 | 39 | id = tg.id 40 | peerId = tg.from.id 41 | fromId = tg.from.id 42 | guard let data = tg.data?.data(using: .utf8) else { return nil } 43 | if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) { 44 | self.data = .init(jsonObject) 45 | } else { 46 | self.data = .init(data) 47 | } 48 | } 49 | 50 | init?(from vk: Vk) { 51 | platform = .vk(vk) 52 | 53 | id = vk.eventId 54 | peerId = vk.peerId 55 | fromId = vk.userId 56 | guard let data = vk.payload else { return nil } 57 | if let str = data.value as? String { 58 | self.data = .init(str.data(using: .utf8)) 59 | } else { 60 | self.data = data 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.02.2021. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Vapor 12 | 13 | public struct User: Codable { 14 | 15 | public let id: Int64 16 | 17 | public let firstName: String? 18 | 19 | public let lastName: String? 20 | 21 | // MARK: - Platform object 22 | 23 | public typealias Tg = Telegrammer.User 24 | public typealias Vk = Vkontakter.User 25 | 26 | public let platform: Platform 27 | 28 | } 29 | 30 | extension User: PlatformObject { 31 | 32 | public init?(from vk: Vk?) { 33 | guard let vk = vk else { return nil } 34 | 35 | platform = .vk(vk) 36 | firstName = vk.firstName 37 | lastName = vk.lastName 38 | id = vk.id 39 | } 40 | 41 | public init?(from tg: Tg?) { 42 | guard let tg = tg else { return nil } 43 | 44 | platform = .tg(tg) 45 | firstName = tg.firstName 46 | lastName = tg.lastName 47 | id = tg.id 48 | } 49 | 50 | } 51 | 52 | extension User { 53 | 54 | public func getUsername(bot: Bot, app: Application) throws -> Future { 55 | switch platform { 56 | case let .tg(tg): 57 | return app.eventLoopGroup.future(tg.username) 58 | case let .vk(vk): 59 | if let username = vk.screenName { 60 | return app.eventLoopGroup.future(username) 61 | } else { 62 | return try bot.requireVkBot().getUser(params: .init(userIds: [.id(id)], fields: [.screen_name])).map(\.first?.screenName) 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Botter/Protocols/Attachable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 06.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | 11 | public protocol Attachable: Codable { 12 | func attachmentId(for platform: AnyPlatform) -> String? 13 | } 14 | 15 | extension Attachable { 16 | func object(for platform: AnyPlatform) -> BotterAttachable? { 17 | guard let attachmentId = attachmentId(for: platform) else { return nil } 18 | return .init(platform.convert(to: attachmentId)) 19 | } 20 | } 21 | 22 | public struct BotterAttachable: Attachable { 23 | public typealias PlatformId = Platform 24 | 25 | public let platformAttachmentIds: [PlatformId] 26 | 27 | public init(_ platformAttachmentIds: PlatformId...) { 28 | self.init(platformAttachmentIds) 29 | } 30 | 31 | public init(_ platformAttachmentIds: [PlatformId]) { 32 | self.platformAttachmentIds = platformAttachmentIds 33 | } 34 | 35 | public func attachmentId(for platform: AnyPlatform) -> String? { 36 | guard let platformAttachmentId = self.platformAttachmentIds.first(where: { $0.same(platform) }) else { return nil } 37 | return platformAttachmentId.value 38 | } 39 | } 40 | 41 | extension Vkontakter.Attachable { 42 | var botterAttachable: BotterAttachable { 43 | .init(.vk(attachmentId)) 44 | } 45 | } 46 | 47 | extension Photo: Attachable { 48 | public func attachmentId(for platform: AnyPlatform) -> String? { 49 | attachmentId 50 | } 51 | } 52 | 53 | extension Document: Attachable { 54 | public func attachmentId(for platform: AnyPlatform) -> String? { 55 | attachmentId 56 | } 57 | } 58 | 59 | // TODO: other kinds 60 | -------------------------------------------------------------------------------- /Sources/Botter/Errors/BotError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BotError 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | public class BotError: Error {} 12 | 13 | public class CoreError: Error { 14 | public enum `Type` { 15 | case `internal` 16 | case network 17 | case server 18 | } 19 | 20 | public let type: Type 21 | public let description: String 22 | public let reason: String 23 | 24 | public init(type: Type, description: String = "", reason: String = "") { 25 | self.type = type 26 | self.description = description 27 | self.reason = reason 28 | } 29 | 30 | public var localizedDescription: String { 31 | return """ 32 | 33 | >>>Type: \(type) 34 | >>>Description: \(description) 35 | >>>Reason: \(reason) 36 | 37 | """ 38 | } 39 | } 40 | //exception telegram.error.BadRequest(message) 41 | //Bases: telegram.error.NetworkError 42 | // 43 | //exception telegram.error.ChatMigrated(new_chat_id) 44 | //Bases: telegram.error.TelegramError 45 | // 46 | //Parameters: new_chat_id (int) – 47 | //exception telegram.error.InvalidToken 48 | //Bases: telegram.error.TelegramError 49 | // 50 | //exception telegram.error.NetworkError(message) 51 | //Bases: telegram.error.TelegramError 52 | // 53 | //exception telegram.error.RetryAfter(retry_after) 54 | //Bases: telegram.error.TelegramError 55 | // 56 | //Parameters: retry_after (int) – 57 | //exception telegram.error.TelegramError(message) 58 | //Bases: Exception 59 | // 60 | //exception telegram.error.TimedOut 61 | //Bases: telegram.error.NetworkError 62 | // 63 | //exception telegram.error.Unauthorized(message) 64 | //Bases: telegram.error.TelegramError 65 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Methods/Bot+forwardMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 20.03.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | import Vapor 12 | 13 | public extension Bot { 14 | 15 | /// Parameters container struct for `forwardMessage` method 16 | class ForwardMessageParams: Codable { 17 | 18 | /// Destination of forwarding message 19 | public var destination: SendDestination 20 | 21 | /// Forwarding message 22 | public var message: Message 23 | 24 | func tg(destination: SendDestination, message: Message) throws -> Telegrammer.Bot.ForwardMessageParams? { 25 | guard let fromChatId = message.chatId else { return nil } 26 | return Telegrammer.Bot.ForwardMessageParams(chatId: try destination.tgChatId(), fromChatId: .chat(fromChatId), messageId: Int(message.id)) 27 | } 28 | 29 | } 30 | 31 | @discardableResult 32 | func editMessage(params: ForwardMessageParams, app: Application) throws -> Future? { 33 | switch params.message.platform { 34 | case .vk: 35 | fatalError() 36 | // guard let vk = vk else { return nil } 37 | // 38 | // guard let params = params.vk(message) else { return nil } 39 | // return try vk.editMessage(params: params).map { Message(params: params, resp: $0) } 40 | 41 | case .tg: 42 | guard let tg = tg else { return nil } 43 | 44 | guard let params = try params.tg(destination: params.destination, message: params.message) else { return nil } 45 | return try tg.forwardMessage(params: params).map { Message(from: $0) } 46 | 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/Document.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 04.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | public struct Document: Codable { 13 | 14 | public let platform: Platform 15 | 16 | /// Identifier for this file, which can be used to download or reuse the file 17 | public var attachmentId: String 18 | 19 | /// Optional. Document thumbnail as defined by sender 20 | public var thumb: [Photo.Size]? 21 | 22 | /// Optional. Original filename as defined by sender 23 | public var fileName: String? 24 | 25 | /// Optional. MIME type of the file as defined by sender 26 | public var mimeType: String? 27 | 28 | /// Optional. File size 29 | public var size: Int64? 30 | 31 | } 32 | 33 | extension Document: PlatformObject { 34 | 35 | public typealias Tg = Telegrammer.Document 36 | public typealias Vk = Vkontakter.Doc 37 | 38 | init?(from tg: Tg) { 39 | platform = .tg(tg) 40 | 41 | attachmentId = tg.fileId 42 | thumb = [ tg.thumb != nil ? Photo.Size(from: tg.thumb!) : nil ].compactMap { $0 } 43 | fileName = tg.fileName 44 | mimeType = tg.mimeType 45 | size = tg.fileSize != nil ? .init(tg.fileSize!) : nil 46 | } 47 | 48 | init?(from vk: Vk) { 49 | platform = .vk(vk) 50 | 51 | attachmentId = vk.attachmentId 52 | thumb = vk.preview?.photo?.sizes?.compactMap { Photo.Size(from: $0) } 53 | if let title = vk.title, let ext = vk.ext { 54 | fileName = title + ext 55 | } else { 56 | fileName = nil 57 | } 58 | mimeType = vk.ext?.mimeType() 59 | size = vk.size 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/Update.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 06.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | public struct Update { 13 | 14 | public enum Content: AutoCodable { 15 | case message(Message) 16 | case event(MessageEvent) 17 | } 18 | 19 | public let content: Content 20 | 21 | public let platform: Platform 22 | 23 | public let secret: String? 24 | 25 | } 26 | 27 | extension Update: PlatformObject { 28 | 29 | public typealias Tg = Telegrammer.Update 30 | public typealias Vk = Vkontakter.Update 31 | 32 | public init?(from vk: Vk?) { 33 | guard let vk = vk, let object = vk.object else { return nil } 34 | 35 | platform = .vk(vk) 36 | switch object { 37 | case let .messageWrapper(wrapper): 38 | content = .message(Message(from: wrapper.message)) 39 | case let .event(event): 40 | guard let event = MessageEvent(from: event) else { return nil } 41 | content = .event(event) 42 | case let .message(message): 43 | content = .message(Message(from: message)) 44 | } 45 | secret = vk.secret 46 | } 47 | 48 | public init?(from tg: Tg?) { 49 | guard let tg = tg else { return nil } 50 | 51 | platform = .tg(tg) 52 | secret = nil 53 | 54 | if let message = tg.message { 55 | content = .message(Message(from: message)) 56 | } else if let callbackQuery = tg.callbackQuery { 57 | guard let event = MessageEvent(from: callbackQuery) else { return nil } 58 | content = .event(event) 59 | } else { 60 | return nil 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Botter/Protocols/UserFetchable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.02.2021. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | // MARK: - User Fetchanble 13 | 14 | public protocol UserFetchable { 15 | var userInfo: UserInfo { get } 16 | } 17 | 18 | public enum UserInfo { 19 | case id(Int64?) 20 | case user(User?) 21 | } 22 | 23 | // MARK: - Implementation 24 | 25 | // MARK: Vkontakter 26 | 27 | extension Vkontakter.Message: UserFetchable { 28 | public var userInfo: UserInfo { .id(fromId) } 29 | } 30 | 31 | extension Vkontakter.MessageEvent: UserFetchable { 32 | public var userInfo: UserInfo { .id(fromId) } 33 | } 34 | 35 | // MARK: Telegrammer 36 | 37 | extension Telegrammer.Message: UserFetchable { 38 | public var userInfo: UserInfo { .user(User(from: self.from)) } 39 | } 40 | 41 | extension Telegrammer.CallbackQuery: UserFetchable { 42 | public var userInfo: UserInfo { .user(User(from: self.from)) } 43 | } 44 | 45 | // MARK: Botter 46 | 47 | extension Update: UserFetchable { 48 | public var userInfo: UserInfo { 49 | switch content { 50 | case let .event(event): 51 | return event.userInfo 52 | case let .message(message): 53 | return message.userInfo 54 | } 55 | } 56 | } 57 | 58 | extension MessageEvent: UserFetchable { 59 | public var userInfo: UserInfo { platform.fetchable.userInfo } 60 | } 61 | 62 | extension Message: UserFetchable { 63 | public var userInfo: UserInfo { platform.fetchable.userInfo } 64 | } 65 | 66 | extension Platform where Vk: UserFetchable, Tg: UserFetchable { 67 | var fetchable: UserFetchable { 68 | switch self { 69 | case let .tg(tg): 70 | return tg 71 | case let .vk(vk): 72 | return vk 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 04.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | public struct Photo { 13 | 14 | public let platform: Platform 15 | 16 | /// Идентификатор фотографии. 17 | public let attachmentId: String 18 | 19 | public let sizes: [Size] 20 | 21 | } 22 | 23 | extension Photo: PlatformObject { 24 | 25 | public typealias Tg = [Telegrammer.PhotoSize] 26 | public typealias Vk = Vkontakter.Photo 27 | 28 | init?(from tg: Tg) { 29 | platform = .tg(tg) 30 | 31 | attachmentId = tg.largerElement!.fileId 32 | sizes = tg.map { Size(from: $0) }.compactMap { $0 } 33 | } 34 | 35 | init?(from vk: Vk) { 36 | platform = .vk(vk) 37 | 38 | attachmentId = vk.attachmentId 39 | sizes = vk.sizes?.compactMap { Size(from: $0) } ?? [] 40 | } 41 | 42 | } 43 | 44 | extension Array where Element == Telegrammer.PhotoSize { 45 | var largerElement: Element? { 46 | sorted { $0.fileSize ?? 0 > $1.fileSize ?? 0 }.first 47 | } 48 | } 49 | 50 | public extension Photo { 51 | struct Size: PlatformObject { 52 | public typealias Tg = Telegrammer.PhotoSize 53 | public typealias Vk = Vkontakter.PhotoSize 54 | 55 | public let platform: Platform 56 | 57 | /// 58 | public let url: String? 59 | 60 | /// 61 | public let width: Int? 62 | 63 | /// 64 | public let height: Int? 65 | 66 | init?(from tg: Tg) { 67 | platform = .tg(tg) 68 | 69 | url = nil 70 | width = tg.width 71 | height = tg.height 72 | } 73 | 74 | init?(from vk: Vk) { 75 | platform = .vk(vk) 76 | 77 | url = vk.url 78 | width = vk.width 79 | height = vk.height 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Botter/Types/Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 20.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | import AnyCodable 12 | 13 | public struct Keyboard: Codable { 14 | 15 | /// 16 | public let oneTime: Bool 17 | 18 | /// 19 | public var buttons: [[Button]] 20 | 21 | /// 22 | public let inline: Bool 23 | 24 | public init(oneTime: Bool = false, inline: Bool = true, buttons: [[Button]]) { 25 | self.oneTime = oneTime 26 | self.buttons = buttons 27 | self.inline = inline 28 | } 29 | 30 | var tgInline: Telegrammer.InlineKeyboardMarkup { 31 | .init( 32 | inlineKeyboard: buttonsFor(\.inlineTg) 33 | ) 34 | } 35 | 36 | var tg: Telegrammer.ReplyMarkup { 37 | inline 38 | ? .inlineKeyboardMarkup(tgInline) 39 | : .replyKeyboardMarkup(.init( 40 | keyboard: buttonsFor(\.tg), 41 | resizeKeyboard: true, 42 | oneTimeKeyboard: oneTime, 43 | selective: nil 44 | )) 45 | } 46 | 47 | var vk: Vkontakter.Keyboard { 48 | .init(oneTime: oneTime, buttons: buttonsFor(\.vk), inline: inline) 49 | } 50 | } 51 | 52 | private extension Keyboard { 53 | func buttonsFor(_ transform: (Button) -> T?) -> [[T]] { 54 | buttons.map { $0.compactMap(transform) } 55 | } 56 | } 57 | 58 | extension Keyboard: ExpressibleByArrayLiteral { 59 | public typealias ArrayLiteralElement = [Button] 60 | 61 | public init(arrayLiteral elements: ArrayLiteralElement...) { 62 | self.init(buttons: elements) 63 | } 64 | } 65 | 66 | public extension Array where Element == [Button] { 67 | mutating func safeAppend(_ buttons: [Button]) { 68 | guard let lastRow = last else { 69 | append(buttons) 70 | return 71 | } 72 | 73 | if lastRow.count < 2 && lastRow.map(\.text.count).reduce(0, +) < 18 { 74 | indices.last.map { self[$0] += buttons } 75 | } else { 76 | append(buttons) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Botter", 8 | platforms: [ 9 | .macOS(.v10_15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "Botter", 15 | targets: ["Botter"] 16 | ), 17 | .executable(name: "Botter EchoBot", targets: [ "BotterDemoEchoBot" ]) 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/vapor/vapor.git", from: "4.77.1"), 21 | .package(url: "https://github.com/givip/Telegrammer.git", from: "1.0.0-alpha.3"), 22 | .package(url: "https://github.com/CoolONEOfficial/telegrammer-vapor-middleware.git", branch: "main"), 23 | .package(url: "https://github.com/CoolONEOfficial/vkontakter-vapor-middleware.git", branch: "master"), 24 | .package(url: "https://github.com/CoolONEOfficial/Vkontakter.git", from: "0.1.4"), 25 | .package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.4.0"), 26 | ], 27 | targets: [ 28 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 29 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 30 | .target( 31 | name: "Botter", 32 | dependencies: [ 33 | .product(name: "Vapor", package: "vapor"), 34 | .product(name: "Telegrammer", package: "Telegrammer"), 35 | .product(name: "Vkontakter", package: "Vkontakter"), 36 | .product(name: "TelegrammerMiddleware", package: "telegrammer-vapor-middleware"), 37 | .product(name: "VkontakterMiddleware", package: "vkontakter-vapor-middleware"), 38 | .product(name: "AnyCodable", package: "AnyCodable"), 39 | ] 40 | ), 41 | .testTarget( 42 | name: "BotterTests", 43 | dependencies: ["Botter"] 44 | ), 45 | .target(name: "BotterDemoEchoBot", dependencies: ["Botter"]), 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /Sources/Botter/Handlers/CommandHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageHandler.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.12.2020. 6 | // 7 | 8 | import AsyncHTTPClient 9 | import Telegrammer 10 | import Vkontakter 11 | import Vapor 12 | 13 | /// Handler for bot messages, can handle normal messages, channel posts, edited messages 14 | public class CommandHandler: Handler { 15 | 16 | 17 | /// Name of particular MessageHandler, needed for determine handlers instances of one class in groups 18 | public var name: String 19 | 20 | /// Option Set for `MessageHandler` 21 | public struct Options: OptionSet { 22 | public let rawValue: Int 23 | 24 | public init(rawValue: Int) { 25 | self.rawValue = rawValue 26 | } 27 | 28 | ///Should “normal” message updates be handled? 29 | public static let messageUpdates = Options(rawValue: 1) 30 | } 31 | 32 | let commands: [String] 33 | let filters: Filters 34 | public let callback: HandlerCallback 35 | let options: Options 36 | public var app: Application! 37 | public var bot: Bot! 38 | 39 | public lazy var vk: Vkontakter.Handler = Vkontakter.CommandHandler( 40 | name: name, commands: commands, filters: filters.vk, 41 | options: .init(rawValue: options.rawValue) 42 | ) { update, _ throws in 43 | guard let update = Update(from: update) else { return } 44 | try! self.callback(update, self.context(update.platform.any)) 45 | } 46 | 47 | public lazy var tg: Telegrammer.Handler = Telegrammer.CommandHandler ( 48 | name: name, commands: commands.map { "/" + $0 }, filters: filters.tg, 49 | options: .init(rawValue: options.rawValue) 50 | ) { update, _ throws in 51 | guard let update = Update(from: update) else { return } 52 | try! self.callback(update, self.context(update.platform.any)) 53 | } 54 | 55 | public init( 56 | name: String = String(describing: CommandHandler.self), 57 | commands: [String], 58 | filters: Filters = .all, 59 | options: Options = [], 60 | callback: @escaping HandlerCallback 61 | ) { 62 | self.filters = filters 63 | self.commands = commands 64 | self.callback = callback 65 | self.options = options 66 | self.name = name 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Botter/Dispatcher/Dispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 06.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | import Dispatch 12 | import NIO 13 | import Logging 14 | import AsyncHTTPClient 15 | import Vapor 16 | 17 | public class Dispatcher { 18 | public let vk: Vkontakter.Dispatcher? 19 | public let tg: Telegrammer.Dispatcher? 20 | private let dispatchers: [BaseDispatcher] 21 | private let bot: Bot 22 | private let app: Application? 23 | 24 | public init(bot: Bot, app: Application?, worker: Worker = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)) { 25 | if let vkBot = bot.vk { 26 | vk = .init(bot: vkBot, worker: worker) 27 | } else { 28 | vk = nil 29 | } 30 | if let tgBot = bot.tg { 31 | tg = .init(bot: tgBot, worker: worker) 32 | } else { 33 | tg = nil 34 | } 35 | self.bot = bot 36 | self.app = app 37 | dispatchers = ([tg, vk] as [BaseDispatcher?]).compactMap { $0 } 38 | } 39 | 40 | public func enqueue(bytebuffer: ByteBuffer) { 41 | for dispatcher in dispatchers { 42 | dispatcher.enqueue(bytebuffer) 43 | } 44 | } 45 | } 46 | 47 | public extension Dispatcher { 48 | /** 49 | Add new handler to group 50 | 51 | - Parameters: 52 | - handler: Handler to add in `Dispatcher`'s handlers queue 53 | - group: Group of `Dispatcher`'s handler queue, `zero` group by default 54 | */ 55 | func add(handler: Handler, to group: HandlerGroup = .init(vk: .zero, tg: .zero)) { 56 | var handler = handler 57 | handler.app = app 58 | handler.bot = bot 59 | vk?.add(handler: handler.vk, to: group.vk) 60 | tg?.add(handler: handler.tg, to: group.tg) 61 | } 62 | 63 | /** 64 | Remove handler from specific group of `Dispatchers`'s queue 65 | 66 | Note: If in one group present more then one handlers with the same name, they both will be deleted 67 | 68 | - Parameters: 69 | - handler: Handler to be removed 70 | - group: Group from which handlers will be removed 71 | */ 72 | func remove(handler: Handler, from group: HandlerGroup) { 73 | vk?.remove(handler: handler.vk, from: group.vk) 74 | tg?.remove(handler: handler.tg, from: group.tg) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Botter/Botter.swift: -------------------------------------------------------------------------------- 1 | import Vkontakter 2 | import Telegrammer 3 | import Foundation 4 | import Logging 5 | import NIO 6 | import NIOHTTP1 7 | import AsyncHTTPClient 8 | import Vapor 9 | 10 | let log = Logger(label: "com.gp-apps.botter") 11 | 12 | public typealias Worker = EventLoopGroup 13 | 14 | public protocol BotContextProtocol { 15 | var app: Application { get } 16 | var bot: Botter.Bot { get } 17 | var platform: AnyPlatform { get } 18 | } 19 | 20 | public struct BotContext: BotContextProtocol { 21 | public init(app: Application, bot: Bot, platform: AnyPlatform) { 22 | self.app = app 23 | self.bot = bot 24 | self.platform = platform 25 | } 26 | 27 | public let app: Application 28 | public let bot: Botter.Bot 29 | public let platform: AnyPlatform 30 | } 31 | 32 | public final class Bot { 33 | public struct Settings { 34 | public let vk: Vkontakter.Bot.Settings? 35 | public let tg: Telegrammer.Bot.Settings? 36 | 37 | public init(vk: Vkontakter.Bot.Settings? = nil, tg: Telegrammer.Bot.Settings? = nil) { 38 | self.vk = vk 39 | self.tg = tg 40 | } 41 | } 42 | 43 | public let tg: Telegrammer.Bot? 44 | public let vk: Vkontakter.Bot? 45 | 46 | public init(settings: Settings, numThreads: Int = System.coreCount) throws { 47 | if let tgSettings = settings.tg { 48 | tg = try .init(settings: tgSettings, numThreads: numThreads) 49 | } else { 50 | tg = nil 51 | } 52 | if let vkSettings = settings.vk { 53 | vk = try .init(settings: vkSettings, numThreads: numThreads) 54 | } else { 55 | vk = nil 56 | } 57 | } 58 | 59 | public func checkSecret(with update: Update?) -> Bool { 60 | guard let update = update else { return false } 61 | switch update.platform { 62 | case .tg: 63 | return true 64 | case .vk: 65 | return vk?.checkSecretKey(with: update.secret) ?? false 66 | } 67 | 68 | } 69 | } 70 | 71 | extension Bot { 72 | enum BotError: Error { 73 | case botNotFound 74 | } 75 | 76 | func requireVkBot() throws -> Vkontakter.Bot { 77 | guard let vk = vk else { throw BotError.botNotFound } 78 | return vk 79 | } 80 | 81 | func requireTgBot() throws -> Telegrammer.Bot { 82 | guard let tg = tg else { throw BotError.botNotFound } 83 | return tg 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Botter/Handlers/MessageHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageHandler.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.12.2020. 6 | // 7 | 8 | import AsyncHTTPClient 9 | import Telegrammer 10 | import Vkontakter 11 | import Vapor 12 | 13 | /// Handler for bot messages, can handle normal messages, channel posts, edited messages 14 | public class MessageHandler: Handler { 15 | 16 | /// Name of particular MessageHandler, needed for determine handlers instances of one class in groups 17 | public var name: String 18 | 19 | /// Option Set for `MessageHandler` 20 | public struct Options: OptionSet { 21 | public let rawValue: Int 22 | 23 | public init(rawValue: Int) { 24 | self.rawValue = rawValue 25 | } 26 | 27 | ///Should “normal” message updates be handled? 28 | public static let messageUpdates = Options(rawValue: 1) 29 | // ///Should channel posts updates be handled? 30 | // public static let channelPostUpdates = Options(rawValue: 2) 31 | // ///Should “edited” message updates be handled? 32 | // public static let editedUpdates = Options(rawValue: 4) 33 | } 34 | 35 | let filters: Filters 36 | public let callback: HandlerCallback 37 | let options: Options 38 | public var app: Application! 39 | public var bot: Bot! 40 | 41 | public lazy var vk: Vkontakter.Handler = Vkontakter.MessageHandler( 42 | name: name, filters: filters.vk, 43 | options: .init(rawValue: options.rawValue) 44 | ) { update, _ throws in 45 | guard let update = Update(from: update) else { return } 46 | let context = BotContext(app: self.app, bot: self.bot, platform: update.platform.any) 47 | try! self.callback(update, context) 48 | } 49 | 50 | public lazy var tg: Telegrammer.Handler = Telegrammer.MessageHandler ( 51 | name: name, filters: filters.tg, 52 | options: .init(rawValue: options.rawValue) 53 | ) { update, _ throws in 54 | guard let update = Update(from: update) else { return } 55 | let context = BotContext(app: self.app, bot: self.bot, platform: update.platform.any) 56 | try! self.callback(update, context) 57 | } 58 | 59 | public init( 60 | name: String = String(describing: MessageHandler.self), 61 | filters: Filters = .all, 62 | options: Options = [.messageUpdates],// .channelPostUpdates], 63 | callback: @escaping HandlerCallback 64 | ) { 65 | self.filters = filters 66 | self.callback = callback 67 | self.options = options 68 | self.name = name 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Botter/Protocols/Replyable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 07.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import NIO 11 | 12 | public protocol Replyable { 13 | var destination: SendDestination? { get } 14 | } 15 | 16 | extension Message: Replyable { 17 | public var destination: SendDestination? { 18 | get { 19 | if let chatId = self.chatId { 20 | return .chatId(chatId) 21 | } 22 | if let userId = self.userId { 23 | return .userId(userId) 24 | } 25 | return nil 26 | } 27 | set { 28 | fatalError() 29 | } 30 | } 31 | 32 | public var userId: Int64? { 33 | get { fromId } 34 | set { fromId = newValue } 35 | } 36 | } 37 | 38 | public extension Message { 39 | func reply(_ params: Bot.SendMessageParams, context: BotContextProtocol) throws -> Future<[Message]> { 40 | try replyMessage(params, context: context) 41 | } 42 | } 43 | 44 | extension MessageEvent: Replyable { 45 | public var destination: SendDestination? { 46 | get { 47 | if let chatId = self.chatId { 48 | return .chatId(chatId) 49 | } 50 | if let userId = self.userId { 51 | return .userId(userId) 52 | } 53 | return nil 54 | } 55 | set { 56 | fatalError() 57 | } 58 | } 59 | 60 | public var userId: Int64? { 61 | get { fromId } 62 | set { fromId = newValue } 63 | } 64 | 65 | public var chatId: Int64? { 66 | get { peerId } 67 | set { peerId = newValue } 68 | } 69 | } 70 | 71 | extension Bot.SendMessageParams: Replyable {} 72 | 73 | public extension MessageEvent { 74 | func reply(_ params: Bot.SendMessageEventAnswerParams, platform: AnyPlatform? = nil, context: BotContextProtocol) throws -> Future { 75 | let platform = platform ?? context.platform 76 | var params = params 77 | params.event = self 78 | return try context.bot.sendMessageEventAnswer(params, platform: platform) 79 | } 80 | } 81 | 82 | public extension Replyable where Self: PlatformObject { 83 | func replyMessage(_ params: Bot.SendMessageParams, context: BotContextProtocol) throws -> Future<[Message]> { 84 | if let destination = destination { 85 | params.destination = destination 86 | } else { 87 | throw Bot.SendMessageError.destinationNotFound 88 | } 89 | return try context.bot.sendMessage(params, platform: platform.any, context: context) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Botter/Updater/Updater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 26.12.2020. 6 | // 7 | 8 | import Foundation 9 | import AsyncHTTPClient 10 | import NIO 11 | 12 | /** 13 | This class purpose is to receive the updates from Telegram and to deliver them to said dispatcher. 14 | It also runs in a separate thread, so the user can interact with the bot. 15 | The updater can be started as a polling service or, for production, use a webhook to receive updates. 16 | This is achieved using the Webhooks and Longpolling classes. 17 | */ 18 | public final class Updater { 19 | 20 | /// Bot instance which perform requests and establish http server 21 | public let bot: Bot 22 | 23 | /// Dispatcher instance, which handle all updates from Telegram 24 | public let dispatcher: Dispatcher 25 | 26 | /// EventLoopGroup for networking stuff 27 | public let worker: Worker 28 | 29 | //private var longpollingConnection: Longpolling! 30 | private var webhooksListener: Webhooks! 31 | 32 | @discardableResult 33 | public init(bot: Bot, dispatcher: Dispatcher, worker: Worker = MultiThreadedEventLoopGroup(numberOfThreads: 1)) { 34 | self.bot = bot 35 | self.dispatcher = dispatcher 36 | self.worker = worker 37 | } 38 | 39 | /** 40 | Call this method to start receiving Webhooks from Telegram servers. 41 | 42 | Note: Bot instance must being set up to receive Webhooks updates 43 | 44 | - Throws: Throws on errors 45 | - Returns: Future of `Void` type 46 | */ 47 | public func startWebhooks(vkServerName: String?) throws -> Future { 48 | webhooksListener = Webhooks(bot: bot, dispatcher: dispatcher, worker: worker) 49 | return try webhooksListener.start(vkServerName: vkServerName) 50 | } 51 | 52 | // /** 53 | // Call this method to start receiving updates from Telegram by longpolling. 54 | // 55 | // Note: Bot instance must being set up to receive Longpolling updates 56 | // 57 | // - Throws: Throws on errors 58 | // - Returns: Future of `Void` type 59 | // */ 60 | // public func startLongpolling() throws -> Future { 61 | // longpollingConnection = Longpolling(bot: bot, dispatcher: dispatcher, worker: worker) 62 | // return try longpollingConnection.start() 63 | // } 64 | 65 | /** 66 | Call this method to stop receiving updates from Telegram. 67 | */ 68 | public func stop() { 69 | // if let longpollingConnection = longpollingConnection { 70 | // longpollingConnection.stop() 71 | // } 72 | if let webhooksListener = webhooksListener { 73 | _ = try? webhooksListener.stop() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/String+Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Helper.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 22.04.2018. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | #if os(Linux) 11 | import Glibc 12 | #endif 13 | 14 | public extension String { 15 | 16 | static func random(ofLength length: Int) -> String { 17 | return random(minimumLength: length, maximumLength: length) 18 | } 19 | 20 | static func random(minimumLength min: Int, maximumLength max: Int) -> String { 21 | return random( 22 | withCharactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 23 | minimumLength: min, 24 | maximumLength: max 25 | ) 26 | } 27 | 28 | static func random(withCharactersInString string: String, ofLength length: Int) -> String { 29 | return random( 30 | withCharactersInString: string, 31 | minimumLength: length, 32 | maximumLength: length 33 | ) 34 | } 35 | 36 | static func random( 37 | withCharactersInString string: String, 38 | minimumLength min: Int, 39 | maximumLength max: Int 40 | ) -> String { 41 | guard min > 0 && max >= min else { 42 | return "" 43 | } 44 | 45 | let length: Int = (min < max) ? .random(min...max) : max 46 | var randomString = "" 47 | 48 | (1...length).forEach { _ in 49 | let randomIndex: Int = .random(0..) -> Int { 60 | return random(range.lowerBound, range.upperBound - 1) 61 | } 62 | 63 | static func random(_ range: ClosedRange) -> Int { 64 | return random(range.lowerBound, range.upperBound) 65 | } 66 | 67 | static func random(_ lower: Int = 0, _ upper: Int = 100) -> Int { 68 | #if os(Linux) 69 | return Int(Glibc.random() % (upper - lower + 1)) 70 | #else 71 | return Int(arc4random_uniform(UInt32(upper - lower + 1))) 72 | #endif 73 | } 74 | } 75 | 76 | public extension String { 77 | func matchRegexp(pattern: String) -> Bool { 78 | guard let regexp = try? NSRegularExpression(pattern: pattern, options: []) else { return false } 79 | let range = NSRange(location: 0, length: self.count) 80 | return regexp.numberOfMatches(in: self, options: [], range: range) != 0 81 | } 82 | } 83 | 84 | public extension String { 85 | var logMessage: Logger.Message { 86 | return Logger.Message(stringLiteral: self) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/UserFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFilter.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Filters messages to allow only those which are from specified user ID. 11 | //public struct UserFilter: Filter { 12 | // 13 | // var userIds: Set? 14 | // var usernames: Set? 15 | // 16 | // /** 17 | // Init filter with user id 18 | // */ 19 | // public init(userId: Int64) { 20 | // self.userIds = Set([userId]) 21 | // } 22 | // 23 | // /** 24 | // Init filter which username to allow through. 25 | // 26 | // Note: Which username to allow through. If username starts with ‘@’ symbol, it will be ignored. 27 | // */ 28 | // public init(username: String) { 29 | // self.usernames = Set([username]) 30 | // } 31 | // 32 | // /** 33 | // Init filter with user ids 34 | // */ 35 | // public init(userIds: [Int64]) { 36 | // self.userIds = Set(userIds) 37 | // } 38 | // 39 | // /** 40 | // Init filter which usernames to allow through. 41 | // 42 | // Note: If username starts with ‘@’ symbol, it will be ignored. 43 | // */ 44 | // public init(usernames: [String]) { 45 | // self.usernames = Set(usernames) 46 | // } 47 | // 48 | // /** 49 | // Init filter with user id or user name 50 | // 51 | // Note: If username starts with ‘@’ symbol, it will be ignored. 52 | // */ 53 | // public init(userIds: [Int64], usernames: [String]) { 54 | // self.userIds = Set(userIds) 55 | // self.usernames = Set(usernames) 56 | // } 57 | // 58 | // public var name: String = "user" 59 | // 60 | // public func filter(message: Message) -> Bool { 61 | // guard let user = message.from else { return false } 62 | // 63 | // if let ids = userIds, 64 | // !ids.contains(user.id) { 65 | // return false 66 | // } 67 | // 68 | // if let names = usernames, 69 | // let username = user.username, 70 | // !names.contains(username) { 71 | // return false 72 | // } 73 | // 74 | // return true 75 | // } 76 | //} 77 | // 78 | //public extension Filters { 79 | // static func user(userId: Int64) -> Filters { 80 | // return Filters(filter: UserFilter(userId: userId)) 81 | // } 82 | // 83 | // static func user(username: String) -> Filters { 84 | // return Filters(filter: UserFilter(username: username)) 85 | // } 86 | // 87 | // static func user(userIds: [Int64]) -> Filters { 88 | // return Filters(filter: UserFilter(userIds: userIds)) 89 | // } 90 | // 91 | // static func user(usernames: [String]) -> Filters { 92 | // return Filters(filter: UserFilter(usernames: usernames)) 93 | // } 94 | // 95 | // static func user(userIds: [Int64], usernames: [String]) -> Filters { 96 | // return Filters(filter: UserFilter(userIds: userIds, usernames: usernames)) 97 | // } 98 | //} 99 | -------------------------------------------------------------------------------- /Sources/Botter/Types/FileInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 11.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | public enum FileInfoType: String, Codable { 13 | case photo 14 | case document 15 | } 16 | 17 | public struct FileInfo: Codable { 18 | 19 | public let type: FileInfoType 20 | 21 | /// 22 | public enum Content: AutoCodable { 23 | case fileId(BotterAttachable) 24 | case url(String) 25 | case file(InputFile) 26 | 27 | var tg: Telegrammer.FileInfo? { 28 | switch self { 29 | case let .fileId(attachable): 30 | guard let attachmentId = attachable.attachmentId(for: .tg) else { return nil } 31 | return .fileId(attachmentId) 32 | case let .url(url): 33 | return .url(url) 34 | case let .file(file): 35 | return .file(file.tg) 36 | } 37 | } 38 | } 39 | 40 | let content: Content 41 | 42 | public init(type: FileInfoType, content: FileInfo.Content) { 43 | self.type = type 44 | self.content = content 45 | } 46 | 47 | var vk: Vkontakter.Attachment? { 48 | switch content { 49 | case let .fileId(attachable): 50 | switch type { 51 | case .photo: 52 | guard let attachmentId = attachable.attachmentId(for: .vk), 53 | let photo = Vkontakter.Photo(from: attachmentId) else { return nil } 54 | return .photo(photo) 55 | case .document: 56 | guard let attachmentId = attachable.attachmentId(for: .vk), 57 | let doc = Vkontakter.Doc(from: attachmentId) else { return nil } 58 | return .doc(doc) 59 | } 60 | 61 | case .url, .file: 62 | return nil 63 | } 64 | } 65 | 66 | func tgMedia(caption: String?) -> Telegrammer.InputMedia? { 67 | if case .file = content { 68 | debugPrint("files in groups not impletemnted yet") 69 | return nil 70 | } 71 | guard let mediaData = try? JSONEncoder().encode(content.tg), 72 | var media = String(data: mediaData, encoding: .utf8)?.trimmingCharacters(in: ["\""]) 73 | else { return nil } 74 | media.removeAll(where: { $0 == "\\" }) 75 | 76 | switch type { 77 | case .photo: 78 | return .inputMediaPhoto(.init(type: "photo", media: media, caption: caption, parseMode: nil)) 79 | default: 80 | return nil 81 | } 82 | } 83 | } 84 | 85 | extension Telegrammer.InputMedia { 86 | var photoAndVideo: Telegrammer.InputMediaPhotoAndVideo? { 87 | switch self { 88 | case let .inputMediaPhoto(photo): 89 | return .inputMediaPhoto(photo) 90 | 91 | case let .inputMediaVideo(video): 92 | return .inputMediaVideo(video) 93 | 94 | default: 95 | return nil 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Models/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 03.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Vkontakter 10 | import Telegrammer 11 | 12 | public struct Message { 13 | 14 | public let id: Int64 15 | 16 | public let text: String? 17 | 18 | public var fromId: Int64? 19 | 20 | public var chatId: Int64? 21 | 22 | public let command: String? 23 | 24 | public let attachments: [Attachment] 25 | 26 | public let platform: Platform 27 | 28 | } 29 | 30 | extension Message: PlatformObject { 31 | 32 | public typealias Tg = Telegrammer.Message 33 | public typealias Vk = Vkontakter.Message 34 | 35 | init(from tg: Tg) { 36 | platform = .tg(tg) 37 | 38 | id = Int64(tg.messageId) 39 | text = tg.text 40 | fromId = tg.from?.id 41 | chatId = tg.chat.id 42 | if let entity = tg.entities?.first(where: { $0.type == .botCommand }), let text = text { 43 | let startIndex = text.index(text.startIndex, offsetBy: entity.offset + 1) // remove "/" 44 | let endIndex = text.index(startIndex, offsetBy: entity.length - 1) 45 | command = .init(text[startIndex ..< endIndex]) 46 | } else { 47 | command = nil 48 | } 49 | attachments = tg.botterAttachments 50 | } 51 | 52 | init(from vk: Vk) { 53 | platform = .vk(vk) 54 | 55 | id = vk.id! 56 | text = vk.text 57 | chatId = nil 58 | fromId = vk.fromId 59 | if case let .input(command) = vk.payload { 60 | self.command = command 61 | } else { 62 | command = nil 63 | } 64 | attachments = vk.attachments?.botterAttachments ?? [] 65 | } 66 | 67 | init?(params: Vkontakter.Bot.SendMessageParams, resp: Vkontakter.Bot.SendMessageResp) { 68 | guard case let .id(messageId) = resp else { return nil } 69 | self.init(from: Vkontakter.Message( 70 | id: messageId, date: UInt64(Date().timeIntervalSince1970), peerId: params.peerId, fromId: nil, 71 | text: params.message, randomId: params.randomId != nil ? .init(params.randomId!) : nil, 72 | attachments: params.attachment, geo: nil, payload: params.payload, keyboard: params.keyboard, 73 | fwdMessages: params.forwardMessages?.array.map { Vkontakter.Message(id: $0) } ?? [], 74 | replyMessage: nil, action: nil, adminAuthorId: nil, conversationMessageId: nil, 75 | isCropped: nil, membersCount: nil, updateTime: nil, wasListened: nil, pinnedAt: nil 76 | )) 77 | } 78 | 79 | init?(params: Vkontakter.Bot.EditMessageParams, resp: VkFlag) { 80 | guard resp.bool else { return nil } 81 | self.init(from: Vkontakter.Message( 82 | id: params.messageId != nil ? Int64(params.messageId!) : nil, 83 | date: UInt64(Date().timeIntervalSince1970), 84 | peerId: params.peerId, 85 | fromId: nil, 86 | text: params.message, 87 | randomId: nil, 88 | attachments: params.attachment, 89 | keyboard: params.keyboard 90 | )) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Botter/Types/Attachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attachment.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 04.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | 12 | public enum Attachment: AutoCodable { 13 | case photo(Photo) 14 | case document(Document) 15 | } 16 | 17 | public extension Attachment { 18 | var attachmentId: String { 19 | switch self { 20 | case let .photo(photo): 21 | return photo.attachmentId 22 | 23 | case let .document(doc): 24 | return doc.attachmentId 25 | } 26 | } 27 | 28 | func getUrl(context: BotContextProtocol) throws -> Future? { 29 | let app = context.app 30 | switch self { 31 | case let .photo(photo): 32 | switch photo.platform { 33 | case let .tg(tg): 34 | return try tg.largerElement!.getUrl(context: context) 35 | 36 | case let .vk(vk): 37 | return app.eventLoopGroup.future(vk.sizes?.sorted { $0.width ?? 0 > $1.width ?? 0 }.first?.url) 38 | } 39 | 40 | case let .document(doc): 41 | switch doc.platform { 42 | case let .tg(tg): 43 | return try tg.getUrl(context: context) 44 | 45 | case let .vk(vk): 46 | return app.eventLoopGroup.future(vk.url) 47 | } 48 | } 49 | } 50 | } 51 | 52 | protocol TgFileIdentificable { 53 | var fileId: String { get } 54 | } 55 | 56 | extension Telegrammer.Document: TgFileIdentificable {} 57 | extension Telegrammer.File: TgFileIdentificable {} 58 | extension Telegrammer.PhotoSize: TgFileIdentificable {} 59 | 60 | extension TgFileIdentificable { 61 | func getUrl(context: BotContextProtocol) throws -> Future? { 62 | let bot = context.bot 63 | return try bot.tg?.getFile(params: .init(fileId: fileId)).map { file in 64 | guard let path = file.filePath else { return nil } 65 | return "https://api.telegram.org/file/bot\(bot.tg!.settings.token)/\(path)" 66 | } 67 | } 68 | } 69 | 70 | extension Telegrammer.Message { 71 | var botterAttachments: [Attachment] { 72 | var attachments = [Attachment]() 73 | 74 | if let doc = document, let botterDoc = Document(from: doc) { 75 | attachments.append(.document(botterDoc)) 76 | } 77 | 78 | if let photo = photo, let botterPhoto = Photo(from: photo) { 79 | attachments.append(.photo(botterPhoto)) 80 | } 81 | 82 | return attachments 83 | } 84 | } 85 | 86 | extension Vkontakter.ArrayByComma where Element == Vkontakter.Attachment { 87 | var botterAttachments: [Attachment] { 88 | array.compactMap { attachment in 89 | switch attachment { 90 | case let .photo(photoValue): 91 | guard let photo = Photo(from: photoValue) else { return nil } 92 | return .photo(photo) 93 | case let .doc(docValue): 94 | guard let doc = Document(from: docValue) else { return nil } 95 | return .document(doc) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Methods/Bot+sendMessageEventAnswer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bot+sendMessageEventAnswer.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 24.12.2020. 6 | // 7 | 8 | import Telegrammer 9 | import Vkontakter 10 | import Foundation 11 | 12 | public extension Bot { 13 | 14 | enum SendMessageEventAnserError: Error { 15 | case botNotFound 16 | case destinationNotFound 17 | } 18 | 19 | /// Parameters container struct for `sendMessageEventAnswer` method 20 | struct SendMessageEventAnswerParams { 21 | 22 | /// Идентификатор события 23 | var eventId: String? 24 | 25 | var userId: Int64? 26 | 27 | var peerId: Int64? 28 | 29 | public enum `Type` { 30 | case notification(text: String, isAlert: Bool? = nil) 31 | case link(url: String) 32 | 33 | var vk: Vkontakter.EventData { 34 | switch self { 35 | case let .notification(text, _): 36 | return .snackbar(.init(text: text)) 37 | case let .link(url): 38 | return .link(.init(link: url)) 39 | } 40 | } 41 | } 42 | 43 | let type: Type 44 | 45 | func tg() throws -> Telegrammer.Bot.AnswerCallbackQueryParams { 46 | guard let eventId = eventId else { throw SendMessageEventAnserError.destinationNotFound } 47 | switch type { 48 | case let .notification(text, isAlert): 49 | return .init(callbackQueryId: eventId, text: text, showAlert: isAlert, url: nil, cacheTime: nil) 50 | case let .link(url): 51 | return .init(callbackQueryId: eventId, text: nil, showAlert: nil, url: url, cacheTime: nil) 52 | } 53 | } 54 | 55 | func vk() throws -> Vkontakter.Bot.SendMessageEventAnswerParams { 56 | guard let peerId = peerId, let eventId = eventId, let userId = userId else { 57 | throw SendMessageEventAnserError.destinationNotFound 58 | } 59 | return .init(eventId: eventId, userId: userId, peerId: peerId, eventData: type.vk) 60 | } 61 | 62 | var event: MessageEvent? { 63 | get { 64 | nil 65 | } 66 | set { 67 | self.eventId = newValue?.id 68 | self.userId = newValue?.fromId 69 | self.peerId = newValue?.peerId 70 | } 71 | } 72 | 73 | public init(event: MessageEvent? = nil, type: Bot.SendMessageEventAnswerParams.`Type`) { 74 | self.type = type 75 | self.event = event 76 | } 77 | } 78 | 79 | @discardableResult 80 | func sendMessageEventAnswer(_ params: SendMessageEventAnswerParams, platform: AnyPlatform) throws -> Future { 81 | switch platform { 82 | case .vk: 83 | guard let vk = vk else { throw SendMessageEventAnserError.botNotFound } 84 | return try vk.sendMessageEventAnswer(params: try params.vk()).map { $0.bool } 85 | 86 | case .tg: 87 | guard let tg = tg else { throw SendMessageEventAnserError.botNotFound } 88 | return try tg.answerCallbackQuery(params: try params.tg()) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/BotterDemoEchoBot/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Botter 3 | import Vkontakter 4 | import Telegrammer 5 | import Vapor 6 | 7 | ///Getting token from enviroment variable (most safe, recommended) 8 | guard let vkToken = Enviroment.get("VK_BOT_TOKEN") else { 9 | print("VK_BOT_TOKEN variable wasn't found in enviroment variables") 10 | exit(1) 11 | } 12 | 13 | guard let tgToken = Enviroment.get("TG_BOT_TOKEN") else { 14 | print("TG_BOT_TOKEN variable wasn't found in enviroment variables") 15 | exit(1) 16 | } 17 | 18 | var vkSettings = Vkontakter.Bot.Settings(token: vkToken) 19 | let vkPort = Int(Enviroment.get("VK_PORT") ?? "1213")! 20 | 21 | vkSettings.webhooksConfig = .init( 22 | ip: "0.0.0.0", 23 | url: Enviroment.get("VK_BOT_WEBHOOK_URL")!, // or use openUrl(80) 24 | port: vkPort, 25 | groupId: UInt64(Enviroment.get("VK_GROUP_ID")!)! 26 | ) 27 | 28 | var tgSettings = Telegrammer.Bot.Settings(token: tgToken) 29 | let tgPort = Int(Enviroment.get("TG_PORT") ?? "1212")! 30 | 31 | tgSettings.webhooksConfig = .init( 32 | ip: "0.0.0.0", 33 | url: Enviroment.get("TG_WEBHOOK_URL")!, // or use openUrl(tgPort) 34 | port: tgPort 35 | //publicCert: .text(content: Enviroment.get("TG_PUBLIC_KEY")!) 36 | ) 37 | 38 | /// Initializind Bot settings (token, debugmode) 39 | var settings = Bot.Settings(vk: vkSettings, tg: tgSettings) 40 | 41 | let bot = try! Bot(settings: settings) 42 | 43 | /// Dictionary for user echo modes 44 | var userEchoModes: [Int64: Bool] = [:] 45 | 46 | ///Callback for Command handler 47 | func startHandle(_ update: Botter.Update, _ context: Botter.BotContextProtocol) throws { 48 | guard case let .message(message) = update.content else { return } 49 | 50 | _ = try message.reply(.init(text: "Hello!"), context: context) 51 | } 52 | 53 | ///Callback for Message with text handler 54 | func echoResponse(_ update: Botter.Update, _ context: Botter.BotContextProtocol) throws { 55 | guard case let .message(message) = update.content, 56 | let text = message.text, 57 | let userId = message.fromId else { return } 58 | 59 | if text.contains("/echo") { 60 | var onText = "" 61 | if let on = userEchoModes[userId] { 62 | onText = on ? "OFF" : "ON" 63 | userEchoModes[userId] = !on 64 | } else { 65 | onText = "ON" 66 | userEchoModes[userId] = true 67 | } 68 | 69 | _ = try message.reply(.init(text: "Echo mode turned \(onText)"), context: context) 70 | } else if let on = userEchoModes[userId], on == true { 71 | _ = try message.reply(.init(text: text), context: context) 72 | } 73 | } 74 | 75 | do { 76 | ///Dispatcher - handle all incoming messages 77 | let dispatcher = Dispatcher(bot: bot, app: Application()) 78 | 79 | ///Creating and adding handler for command /echo 80 | let commandHandler = CommandHandler(commands: ["start"], callback: startHandle) 81 | dispatcher.add(handler: commandHandler) 82 | 83 | ///Creating and adding handler for ordinary text messages 84 | let echoHandler = MessageHandler(filters: Filters.text, callback: echoResponse) 85 | dispatcher.add(handler: echoHandler) 86 | 87 | ///Handle updates 88 | _ = try Updater(bot: bot, dispatcher: dispatcher).startWebhooks(vkServerName: "title").wait() 89 | 90 | } catch { 91 | print(error.localizedDescription) 92 | } 93 | 94 | while true { } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Vkontakter logo

4 | 5 | # Botter 6 | 7 | Crossplatform Bot Framework written in Swift 5.3 with Vapor framework unifying [Telegrammer](https://github.com/givip/Telegrammer) and [Vkontakter](https://github.com/CoolONEOfficial/Vkontakter) 8 | 9 | [![MacOS](https://github.com/CoolONEOfficial/Botter/actions/workflows/macos.yml/badge.svg)](https://github.com/CoolONEOfficial/Botter/actions/workflows/macos.yml) 10 | [![Ubuntu](https://github.com/CoolONEOfficial/Botter/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/CoolONEOfficial/Botter/actions/workflows/ubuntu.yml) 11 | [![Version](https://img.shields.io/badge/version-0.1.0-blue.svg)](https://github.com/givip/Telegrammer/releases) 12 | [![Language](https://img.shields.io/badge/language-Swift%205.1-orange.svg)](https://swift.org/download/) 13 | [![Platform](https://img.shields.io/badge/platform-Linux%20/%20macOS-ffc713.svg)](https://swift.org/download/) 14 | [![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/CoolONEOfficial/Vkontakter/blob/master/LICENSE) 15 | 16 | 17 | What does it do 18 | --------------- 19 | 20 | Botter is open-source framework for crossplatform bots developers. 21 | It was built on top of Vapor framework 22 | 23 | The simplest code of Echo Bot looks like this: 24 | 25 | ------------- 26 | 27 | _main.swift_ 28 | 29 | ```swift 30 | import Foundation 31 | import Botter 32 | import Vkontakter 33 | import Telegrammer 34 | 35 | var vkSettings = Vkontakter.Bot.Settings(token: vkToken) 36 | let vkPort = Int(Enviroment.get("VK_PORT") ?? "1213")! 37 | 38 | vkSettings.webhooksConfig = .init( 39 | ip: "0.0.0.0", 40 | url: Enviroment.get("VK_BOT_WEBHOOK_URL")!, // or use openUrl(vkPort) 41 | port: vkPort, 42 | groupId: UInt64(Enviroment.get("VK_GROUP_ID")!)! 43 | ) 44 | 45 | var tgSettings = Telegrammer.Bot.Settings(token: tgToken) 46 | let tgPort = Int(Enviroment.get("TG_PORT") ?? "1212")! 47 | 48 | tgSettings.webhooksConfig = .init( 49 | ip: "0.0.0.0", 50 | url: Enviroment.get("TG_WEBHOOK_URL")!, // or use openUrl(tgPort) 51 | port: tgPort 52 | ) 53 | 54 | var settings = Bot.Settings(vk: vkSettings, tg: tgSettings) 55 | 56 | let bot = try Bot(settings: settings) 57 | 58 | let echoHandler = MessageHandler { (update, context) in 59 | guard case let .message(message) = update.content, 60 | let text = message.text else { return } 61 | 62 | _ = try bot.getUser(from: update, app: context.app)?.throwingFlatMap { user in 63 | try message.reply(.init(text: "Hello, \(user.firstName ?? "anonymous")"), context: context) 64 | } 65 | } 66 | 67 | let dispatcher = Dispatcher(bot: bot) 68 | dispatcher.add(handler: echoHandler) 69 | 70 | _ = try Updater(bot: bot, dispatcher: dispatcher).startWebhooks(serverName: "testserver") 71 | 72 | ``` 73 | 74 | Documentation 75 | --------------- 76 | 77 | - Read [our wiki](https://github.com/CoolONEOfficial/Botter/wiki) 78 | - Read [An official documentation of Vapor](https://docs.vapor.codes/4.0/) 79 | 80 | Requirements 81 | --------------- 82 | 83 | - Ubuntu 16.04 or later with [Swift 5.1 or later](https://swift.org/getting-started/) / macOS with [Xcode 11 or later](https://swift.org/download/) 84 | - Vk account and a Vk App for mobile platform or online (desktop client does not support some chatbot features) 85 | - [Swift Package Manager (SPM)](https://github.com/apple/swift-package-manager/blob/master/Documentation/Usage.md) for dependencies 86 | - [Vapor 4](https://vapor.codes) 87 | 88 | Contributing 89 | --------------- 90 | 91 | See [CONTRIBUTING.md](CONTRIBUTING.md) file. 92 | 93 | Author 94 | --------------- 95 | 96 | Nikolai Trukhin 97 | 98 | [coolone.official@gmail.com](mailto:coolone.official@gmail.com) 99 | [@cooloneofficial](tg://user?id=356008384) 100 | -------------------------------------------------------------------------------- /Sources/Botter/Filters/StatusUpdateFilters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusUpdateFilters.swift 3 | // Telegrammer 4 | // 5 | // Created by Givi Pataridze on 21.04.2018. 6 | // 7 | 8 | import Foundation 9 | 10 | ///** 11 | // Subset struct for messages containing a status update. 12 | // 13 | // Use these filters like: `StatusUpdateFilters.newChatMembers` etc. 14 | // */ 15 | //public struct StatusUpdateFilters { 16 | // /// Messages that contain Message.groupChatCreated, Message.supergroupChatCreated or Message.channelChatCreated 17 | // public static var chatCreated: Filters { return Filters(filter: ChatCreatedFilter()) } 18 | // 19 | // /// Messages that contain Message.deleteChatPhoto 20 | // public static var deleteChatPhoto: Filters { return Filters(filter: DeleteChatPhotoFilter()) } 21 | // 22 | // /// Messages that contain Message.leftChatMember 23 | // public static var leftChatMember: Filters { return Filters(filter: LeftChatMemberFilter()) } 24 | // 25 | // /// Messages that contain Message.migrateFromChatId 26 | // public static var migrate: Filters { return Filters(filter: MigrateFilter()) } 27 | // 28 | // /// Messages that contain Message.newChatMembers 29 | // public static var newChatMembers: Filters { return Filters(filter: NewChatMembersFilter()) } 30 | // 31 | // /// Messages that contain Message.newChatPhoto 32 | // public static var newChatPhoto: Filters { return Filters(filter: NewChatPhotoFilter()) } 33 | // 34 | // /// Messages that contain Message.newChatTitle 35 | // public static var newChatTitle: Filters { return Filters(filter: NewChatTitleFilter()) } 36 | // 37 | // /// Messages that contain Message.pinnedMessage 38 | // public static var pinnedMessage: Filters { return Filters(filter: PinnedMessageFilter()) } 39 | //} 40 | 41 | ///// Messages that contain Message.groupChatCreated, Message.supergroupChatCreated or Message.channelChatCreated 42 | //public struct ChatCreatedFilter: Filter { 43 | // 44 | // public var name: String = "chat_created" 45 | // 46 | // public func filter(message: Message) -> Bool { 47 | // return message.channelChatCreated != nil || 48 | // message.supergroupChatCreated != nil || 49 | // message.channelChatCreated != nil 50 | // } 51 | //} 52 | // 53 | ///// Messages that contain Message.deleteChatPhoto 54 | //public struct DeleteChatPhotoFilter: Filter { 55 | // 56 | // public var name: String = "delete_chat_photo" 57 | // 58 | // public func filter(message: Message) -> Bool { 59 | // return message.deleteChatPhoto != nil 60 | // } 61 | //} 62 | // 63 | ///// Messages that contain Message.leftChatMember 64 | //public struct LeftChatMemberFilter: Filter { 65 | // 66 | // public var name: String = "left_chat_member" 67 | // 68 | // public func filter(message: Message) -> Bool { 69 | // return message.leftChatMember != nil 70 | // } 71 | //} 72 | // 73 | ///// Messages that contain Message.migrateFromChatId 74 | //public struct MigrateFilter: Filter { 75 | // 76 | // public var name: String = "migrate" 77 | // 78 | // public func filter(message: Message) -> Bool { 79 | // return message.migrateFromChatId != nil || 80 | // message.migrateToChatId != nil 81 | // } 82 | //} 83 | // 84 | ///// Messages that contain Message.newChatMembers 85 | //public struct NewChatMembersFilter: Filter { 86 | // 87 | // public var name: String = "new_chat_members" 88 | // 89 | // public func filter(message: Message) -> Bool { 90 | // return message.newChatMembers != nil 91 | // } 92 | //} 93 | // 94 | ///// Messages that contain Message.newChatPhoto 95 | //public struct NewChatPhotoFilter: Filter { 96 | // 97 | // public var name: String = "new_chat_photo" 98 | // 99 | // public func filter(message: Message) -> Bool { 100 | // guard let photos = message.newChatPhoto else { return false } 101 | // return !photos.isEmpty 102 | // } 103 | //} 104 | // 105 | ///// Messages that contain Message.newChatTitle 106 | //public struct NewChatTitleFilter: Filter { 107 | // 108 | // public var name: String = "new_chat_title" 109 | // 110 | // public func filter(message: Message) -> Bool { 111 | // return message.newChatTitle != nil 112 | // } 113 | //} 114 | // 115 | ///// Messages that contain Message.pinnedMessage 116 | //public struct PinnedMessageFilter: Filter { 117 | // 118 | // public var name: String = "pinned_message" 119 | // 120 | // public func filter(message: Message) -> Bool { 121 | // return message.pinnedMessage != nil 122 | // } 123 | //} 124 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Methods/Bot+editMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 11.01.2021. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | import Vapor 12 | 13 | public extension Bot { 14 | 15 | /// Parameters container struct for `editMessage` method 16 | class EditMessageParams: Codable { 17 | 18 | /// Текст личного сообщения. 19 | public var message: String? 20 | 21 | /// Объект, описывающий клавиатуру бота. 22 | public var keyboard: Keyboard? 23 | 24 | /// Вложения прикрепленные к сообщению. 25 | public var attachments: [FileInfo]? 26 | 27 | public init(message: String? = nil, keyboard: Keyboard? = nil, attachments: [FileInfo]? = nil) { 28 | assert(message != nil || attachments != nil) 29 | self.message = message 30 | self.keyboard = keyboard 31 | self.attachments = attachments 32 | } 33 | 34 | func tgMedia(_ content: FileInfo, _ message: Message) -> Telegrammer.Bot.EditMessageMediaParams? { 35 | guard let media = content.tgMedia(caption: message.text), let chatId = message.chatId else { return nil } 36 | return .init(chatId: .chat(chatId), messageId: Int(message.id), media: media) 37 | } 38 | 39 | func tgText(_ content: String, _ message: Message) -> Telegrammer.Bot.EditMessageTextParams? { 40 | guard let chatId = message.chatId else { return nil } 41 | return .init(chatId: .chat(chatId), messageId: Int(message.id), text: content) 42 | } 43 | 44 | func tgCaption(_ content: String, _ message: Message) -> Telegrammer.Bot.EditMessageCaptionParams? { 45 | guard let chatId = message.chatId else { return nil } 46 | return .init(chatId: .chat(chatId), messageId: Int(message.id), caption: content) 47 | } 48 | 49 | func tgReplyMarkup(_ content: Keyboard, _ message: Message) -> Telegrammer.Bot.EditMessageReplyMarkupParams? { 50 | guard let chatId = message.chatId else { return nil } 51 | return .init(chatId: .chat(chatId), messageId: Int(message.id), replyMarkup: content.tgInline) 52 | } 53 | 54 | func vk(_ message: Message) -> Vkontakter.Bot.EditMessageParams? { 55 | guard let chatId = message.chatId else { return nil } 56 | return .init(peerId: chatId, message: message.text, attachment: attachments != nil ? .init(attachments!.compactMap { $0.vk }) : nil, keyboard: keyboard?.vk) 57 | } 58 | 59 | } 60 | 61 | @discardableResult 62 | func editMessage(_ message: Message, params: EditMessageParams, app: Application) throws -> Future? { 63 | switch message.platform { 64 | case .vk: 65 | guard let vk = vk else { return nil } 66 | 67 | guard let params = params.vk(message) else { return nil } 68 | return try vk.editMessage(params: params).map { Message(params: params, resp: $0) } 69 | 70 | case .tg: 71 | guard let tg = tg else { return nil } 72 | 73 | var futures = [Future]() 74 | 75 | if let attachments = params.attachments { 76 | futures.append(contentsOf: try attachments.compactMap { attachment in 77 | guard let params = params.tgMedia(attachment, message) else { return nil } 78 | return try tg.editMessageMedia(params: params) 79 | }) 80 | } 81 | 82 | if let keyboard = params.keyboard { 83 | futures.append(try tg.editMessageReplyMarkup(params: params.tgReplyMarkup(keyboard, message))) 84 | } 85 | 86 | if let text = params.message { 87 | if message.attachments.isEmpty { 88 | guard let params = params.tgText(text, message) else { return nil } 89 | futures.append(try tg.editMessageText(params: params)) 90 | } else { 91 | futures.append(try tg.editMessageCaption(params: params.tgCaption(text, message))) 92 | } 93 | } 94 | 95 | return futures.flatten(on: app.eventLoopGroup.next()).map { $0.last?.botterMessage } 96 | } 97 | } 98 | 99 | } 100 | 101 | extension Telegrammer.MessageOrBool { 102 | var botterMessage: Message? { 103 | switch self { 104 | case let .message(message): 105 | return Message(from: message) 106 | 107 | default: 108 | return nil 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Botter/Helpers/String+mimeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+mimeType.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 05.01.2021. 6 | // 7 | 8 | import Foundation 9 | 10 | internal let DEFAULT_MIME_TYPE = "application/octet-stream" 11 | 12 | internal let mimeTypes = [ 13 | "md": "text/markdown", 14 | "html": "text/html", 15 | "htm": "text/html", 16 | "shtml": "text/html", 17 | "css": "text/css", 18 | "xml": "text/xml", 19 | "gif": "image/gif", 20 | "jpeg": "image/jpeg", 21 | "jpg": "image/jpeg", 22 | "js": "application/javascript", 23 | "atom": "application/atom+xml", 24 | "rss": "application/rss+xml", 25 | "mml": "text/mathml", 26 | "txt": "text/plain", 27 | "jad": "text/vnd.sun.j2me.app-descriptor", 28 | "wml": "text/vnd.wap.wml", 29 | "htc": "text/x-component", 30 | "png": "image/png", 31 | "tif": "image/tiff", 32 | "tiff": "image/tiff", 33 | "wbmp": "image/vnd.wap.wbmp", 34 | "ico": "image/x-icon", 35 | "jng": "image/x-jng", 36 | "bmp": "image/x-ms-bmp", 37 | "svg": "image/svg+xml", 38 | "svgz": "image/svg+xml", 39 | "webp": "image/webp", 40 | "woff": "application/font-woff", 41 | "jar": "application/java-archive", 42 | "war": "application/java-archive", 43 | "ear": "application/java-archive", 44 | "json": "application/json", 45 | "hqx": "application/mac-binhex40", 46 | "doc": "application/msword", 47 | "pdf": "application/pdf", 48 | "ps": "application/postscript", 49 | "eps": "application/postscript", 50 | "ai": "application/postscript", 51 | "rtf": "application/rtf", 52 | "m3u8": "application/vnd.apple.mpegurl", 53 | "xls": "application/vnd.ms-excel", 54 | "eot": "application/vnd.ms-fontobject", 55 | "ppt": "application/vnd.ms-powerpoint", 56 | "wmlc": "application/vnd.wap.wmlc", 57 | "kml": "application/vnd.google-earth.kml+xml", 58 | "kmz": "application/vnd.google-earth.kmz", 59 | "7z": "application/x-7z-compressed", 60 | "cco": "application/x-cocoa", 61 | "jardiff": "application/x-java-archive-diff", 62 | "jnlp": "application/x-java-jnlp-file", 63 | "run": "application/x-makeself", 64 | "pl": "application/x-perl", 65 | "pm": "application/x-perl", 66 | "prc": "application/x-pilot", 67 | "pdb": "application/x-pilot", 68 | "rar": "application/x-rar-compressed", 69 | "rpm": "application/x-redhat-package-manager", 70 | "sea": "application/x-sea", 71 | "swf": "application/x-shockwave-flash", 72 | "sit": "application/x-stuffit", 73 | "tcl": "application/x-tcl", 74 | "tk": "application/x-tcl", 75 | "der": "application/x-x509-ca-cert", 76 | "pem": "application/x-x509-ca-cert", 77 | "crt": "application/x-x509-ca-cert", 78 | "xpi": "application/x-xpinstall", 79 | "xhtml": "application/xhtml+xml", 80 | "xspf": "application/xspf+xml", 81 | "zip": "application/zip", 82 | "bin": "application/octet-stream", 83 | "exe": "application/octet-stream", 84 | "dll": "application/octet-stream", 85 | "deb": "application/octet-stream", 86 | "dmg": "application/octet-stream", 87 | "iso": "application/octet-stream", 88 | "img": "application/octet-stream", 89 | "msi": "application/octet-stream", 90 | "msp": "application/octet-stream", 91 | "msm": "application/octet-stream", 92 | "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 93 | "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 94 | "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", 95 | "mid": "audio/midi", 96 | "midi": "audio/midi", 97 | "kar": "audio/midi", 98 | "mp3": "audio/mpeg", 99 | "ogg": "audio/ogg", 100 | "m4a": "audio/x-m4a", 101 | "ra": "audio/x-realaudio", 102 | "3gpp": "video/3gpp", 103 | "3gp": "video/3gpp", 104 | "ts": "video/mp2t", 105 | "mp4": "video/mp4", 106 | "mpeg": "video/mpeg", 107 | "mpg": "video/mpeg", 108 | "mov": "video/quicktime", 109 | "webm": "video/webm", 110 | "flv": "video/x-flv", 111 | "m4v": "video/x-m4v", 112 | "mng": "video/x-mng", 113 | "asx": "video/x-ms-asf", 114 | "asf": "video/x-ms-asf", 115 | "wmv": "video/x-ms-wmv", 116 | "avi": "video/x-msvideo" 117 | ] 118 | 119 | fileprivate func mimeType(for ext: String?) -> String { 120 | let lowercase_ext: String = ext!.lowercased() 121 | if ext != nil && mimeTypes.contains(where: { $0.0 == lowercase_ext }) { 122 | return mimeTypes[lowercase_ext]! 123 | } 124 | return DEFAULT_MIME_TYPE 125 | } 126 | 127 | extension URL { 128 | public func mimeType() -> String { 129 | return Botter.mimeType(for: self.pathExtension) 130 | } 131 | } 132 | 133 | extension NSString { 134 | public func mimeType() -> String { 135 | return Botter.mimeType(for: self.pathExtension) 136 | } 137 | } 138 | 139 | extension String { 140 | public var mimeType: String { 141 | (self as NSString).mimeType() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/Botter/Types/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 28.12.2020. 6 | // 7 | 8 | import Foundation 9 | import Telegrammer 10 | import Vkontakter 11 | import AnyCodable 12 | 13 | public struct Button: Codable { 14 | 15 | public enum Action: AutoCodable { 16 | 17 | public struct Link: Codable { 18 | public let link: String 19 | 20 | public init(link: String) { 21 | self.link = link 22 | } 23 | } 24 | 25 | public struct Pay: Codable { 26 | public let hash: String 27 | 28 | public init(hash: String) { 29 | self.hash = hash 30 | } 31 | } 32 | 33 | public struct App: Codable { 34 | public let appId: Int64 35 | 36 | public let ownerId: Int64? 37 | 38 | public let hash: String 39 | 40 | public init(appId: Int64, ownerId: Int64? = nil, hash: String) { 41 | self.appId = appId 42 | self.ownerId = ownerId 43 | self.hash = hash 44 | } 45 | } 46 | 47 | case text 48 | case link(Link) 49 | case location 50 | case pay(Pay) 51 | case app(App) 52 | case callback 53 | 54 | func vk(parent: Button) -> Vkontakter.Button.Action { 55 | let data = parent.payload?.description 56 | 57 | switch self { 58 | case .text: 59 | return .text(.init(payload: data, label: parent.text)) 60 | case let .link(linkValue): 61 | return .link(.init(payload: data, label: parent.text, link: linkValue.link)) 62 | case .location: 63 | return .location(.init(payload: data)) 64 | case let .pay(payValue): 65 | return .pay(.init(payload: data, hash: payValue.hash)) 66 | case let .app(appValue): 67 | return .app(.init(payload: data, label: parent.text, appId: appValue.appId, ownerId: appValue.ownerId, hash: appValue.hash)) 68 | case .callback: 69 | return .callback(.init(payload: data, label: parent.text)) 70 | } 71 | } 72 | } 73 | 74 | /// 75 | public let action: Action 76 | 77 | /// 78 | public let color: Vkontakter.Button.Color? 79 | 80 | /// 81 | public let text: String 82 | 83 | /// 84 | public var payload: String? 85 | 86 | public init(text: String, action: Action = .callback, color: Vkontakter.Button.Color? = nil, data: T? = nil, dataEncoder: JSONEncoder = .snakeCased) throws { 87 | self.init(text: text, action: action, color: color, payload: String(data: try dataEncoder.encode(data), encoding: .utf8)!) 88 | } 89 | 90 | public init(text: String, action: Action = .callback, color: Vkontakter.Button.Color? = nil, payload: String? = nil) { 91 | self.text = text 92 | self.action = action 93 | self.color = color 94 | self.payload = payload 95 | } 96 | 97 | var inlineTg: Telegrammer.InlineKeyboardButton? { 98 | switch action { 99 | case .text: 100 | log.warning(.init(stringLiteral: "Telegram doesn't support text inline keyboard button!")) 101 | return nil 102 | case let .link(linkValue): 103 | return .init(text: text, url: linkValue.link, callbackData: payload?.description) 104 | case .location: 105 | log.warning(.init(stringLiteral: "Telegram doesn't support location inline keyboard button!")) 106 | return nil 107 | case .pay: 108 | return .init(text: text, pay: true) 109 | case .app(_): 110 | return .init(text: text) // TODO: callbackGame: CallbackGame() 111 | case .callback: 112 | return .init(text: text, callbackData: payload ?? "nope") 113 | } 114 | } 115 | 116 | var tg: Telegrammer.KeyboardButton? { 117 | switch action { 118 | case .text: 119 | return .init(text: text) 120 | case .link: 121 | log.warning(.init(stringLiteral: "Telegram doesn't support link keyboard button!")) 122 | return nil 123 | case .location: 124 | return .init(text: text, requestContact: nil, requestLocation: true, requestPoll: nil) 125 | case .pay: 126 | log.warning(.init(stringLiteral: "Telegram doesn't support pay keyboard button!")) 127 | return nil 128 | case .app: 129 | log.warning(.init(stringLiteral: "Telegram doesn't support app keyboard button!")) 130 | return nil 131 | case .callback: 132 | log.warning(.init(stringLiteral: "Telegram doesn't support callback keyboard button!")) 133 | return nil 134 | } 135 | } 136 | 137 | var vk: Vkontakter.Button? { 138 | .init(action: action.vk(parent: self), color: color) 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /Sources/Botter/Types/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 13.12.2020. 6 | // 7 | 8 | import Foundation 9 | import AnyCodable 10 | 11 | // MARK: - Platform 12 | 13 | public enum Platform { 14 | case tg(Tg) 15 | case vk(Vk) 16 | } 17 | 18 | public extension Platform { 19 | var name: String { 20 | switch self { 21 | case .tg: 22 | return CodingKeys.tg.rawValue 23 | case .vk: 24 | return CodingKeys.vk.rawValue 25 | } 26 | } 27 | 28 | var any: AnyPlatform { 29 | convert(to: AnyCodable()) 30 | } 31 | 32 | func same(_ platform: AnyPlatform) -> Bool { 33 | switch self { 34 | case .tg: 35 | if case .tg = platform { 36 | return true 37 | } 38 | case .vk: 39 | if case .vk = platform { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | } 46 | 47 | extension Platform: CustomStringConvertible where Tg: CustomStringConvertible, Vk: CustomStringConvertible { 48 | public var description: String { 49 | switch self { 50 | case let .tg(tg): 51 | return tg.description 52 | case let .vk(vk): 53 | return vk.description 54 | } 55 | } 56 | } 57 | 58 | public extension Array { 59 | func first(for platform: AnyPlatform) -> Element? where Element == Platform { 60 | first { $0.any == platform } 61 | } 62 | 63 | func firstValue(for platform: AnyPlatform) -> T? where Element == TypedPlatform { 64 | first(for: platform)?.value 65 | } 66 | } 67 | 68 | public extension Platform where Tg == Vk { 69 | var value: Tg { 70 | switch self { 71 | case let .tg(tg): 72 | return tg 73 | 74 | case let .vk(vk): 75 | return vk 76 | } 77 | } 78 | } 79 | 80 | extension Platform: Equatable where Tg: Equatable, Vk: Equatable { 81 | public static func == (lhs: Platform, rhs: Platform) -> Bool { 82 | switch lhs { 83 | case let .tg(tg): 84 | switch rhs { 85 | case let .tg(innerTg): 86 | return tg == innerTg 87 | case .vk: 88 | return false 89 | } 90 | 91 | case let .vk(vk): 92 | switch rhs { 93 | case .tg: 94 | return false 95 | case let .vk(innerVk): 96 | return vk == innerVk 97 | } 98 | } 99 | } 100 | 101 | } 102 | 103 | public extension Platform { 104 | func convert(to value: T) -> Platform { 105 | switch self { 106 | case .tg: 107 | return .tg(value) 108 | case .vk: 109 | return .vk(value) 110 | } 111 | } 112 | } 113 | 114 | extension Platform: Codable { 115 | 116 | enum CodingKeys: String, CodingKey { 117 | case tg 118 | case vk 119 | } 120 | 121 | public init(from decoder: Decoder) throws { 122 | let container = try decoder.container(keyedBy: CodingKeys.self) 123 | 124 | if container.allKeys.contains(.tg), try container.decodeNil(forKey: .tg) == false { 125 | let tg = try container.decode(Tg.self, forKey: .tg) 126 | self = .tg(tg) 127 | return 128 | } 129 | if container.allKeys.contains(.vk), try container.decodeNil(forKey: .vk) == false { 130 | let vk = try container.decode(Vk.self, forKey: .vk) 131 | self = .vk(vk) 132 | return 133 | } 134 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 135 | } 136 | 137 | public func encode(to encoder: Encoder) throws { 138 | var container = encoder.container(keyedBy: CodingKeys.self) 139 | 140 | switch self { 141 | case let .tg(tg): 142 | try container.encode(tg, forKey: .tg) 143 | 144 | case let .vk(vk): 145 | try container.encode(vk, forKey: .vk) 146 | } 147 | } 148 | 149 | } 150 | 151 | extension Platform: Hashable where Tg: Hashable, Vk: Hashable { 152 | public func hash(into hasher: inout Hasher) { 153 | switch self { 154 | case let .tg(tg): 155 | hasher.combine(tg) 156 | 157 | case let .vk(vk): 158 | hasher.combine(vk) 159 | } 160 | } 161 | } 162 | 163 | // MARK: - AnyPlatform 164 | 165 | public typealias AnyPlatform = Platform 166 | 167 | public extension AnyPlatform { 168 | static let tg: Self = .tg(.init()) 169 | static let vk: Self = .vk(.init()) 170 | } 171 | 172 | public extension Array where Element == AnyPlatform { 173 | static let all: Self = [ .vk, .tg ] 174 | 175 | static func available(bot: Bot) -> Self { 176 | var platforms = Self() 177 | 178 | if bot.vk != nil { 179 | platforms.append(.vk) 180 | } 181 | 182 | if bot.tg != nil { 183 | platforms.append(.tg) 184 | } 185 | 186 | return platforms 187 | } 188 | } 189 | 190 | // MARK: - Typed platform 191 | 192 | public typealias TypedPlatform = Platform 193 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "anycodable", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Flight-School/AnyCodable.git", 7 | "state" : { 8 | "revision" : "38b05fc9f86501ef8018aa90cf3d83bd97f74067", 9 | "version" : "0.4.0" 10 | } 11 | }, 12 | { 13 | "identity" : "async-http-client", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/swift-server/async-http-client.git", 16 | "state" : { 17 | "revision" : "78db67e5bf4a8543075787f228e8920097319281", 18 | "version" : "1.18.0" 19 | } 20 | }, 21 | { 22 | "identity" : "async-kit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/vapor/async-kit.git", 25 | "state" : { 26 | "revision" : "a61da00d404ec91d12766f1b9aac7d90777b484d", 27 | "version" : "1.17.0" 28 | } 29 | }, 30 | { 31 | "identity" : "console-kit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/vapor/console-kit.git", 34 | "state" : { 35 | "revision" : "08f36a30e0893e6a52fefbf1c2db4a6bc1288ba2", 36 | "version" : "4.2.5" 37 | } 38 | }, 39 | { 40 | "identity" : "multipart-kit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/vapor/multipart-kit.git", 43 | "state" : { 44 | "revision" : "1adfd69df2da08f7931d4281b257475e32c96734", 45 | "version" : "4.5.4" 46 | } 47 | }, 48 | { 49 | "identity" : "routing-kit", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/vapor/routing-kit.git", 52 | "state" : { 53 | "revision" : "611bc45c5dfb1f54b84d99b89d1f72191fb6b71b", 54 | "version" : "4.7.2" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-algorithms", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-algorithms.git", 61 | "state" : { 62 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", 63 | "version" : "1.0.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-atomics", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-atomics.git", 70 | "state" : { 71 | "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", 72 | "version" : "1.1.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-backtrace", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/swift-server/swift-backtrace.git", 79 | "state" : { 80 | "revision" : "93b3d9a76454e05379a32a2f3b2a1f5a7794b414", 81 | "version" : "1.2.1" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-collections", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-collections.git", 88 | "state" : { 89 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 90 | "version" : "1.0.4" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-crypto", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-crypto.git", 97 | "state" : { 98 | "revision" : "3bea268b223651c4ab7b7b9ad62ef9b2d4143eb6", 99 | "version" : "1.1.6" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-log", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-log.git", 106 | "state" : { 107 | "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", 108 | "version" : "1.5.2" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-metrics", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-metrics.git", 115 | "state" : { 116 | "revision" : "e382458581b05839a571c578e90060fff499f101", 117 | "version" : "2.1.1" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-nio", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/apple/swift-nio.git", 124 | "state" : { 125 | "revision" : "a2e487b77f17edbce9a65f2b7415f2f479dc8e48", 126 | "version" : "2.57.0" 127 | } 128 | }, 129 | { 130 | "identity" : "swift-nio-extras", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/apple/swift-nio-extras.git", 133 | "state" : { 134 | "revision" : "0e0d0aab665ff1a0659ce75ac003081f2b1c8997", 135 | "version" : "1.19.0" 136 | } 137 | }, 138 | { 139 | "identity" : "swift-nio-http2", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/apple/swift-nio-http2.git", 142 | "state" : { 143 | "revision" : "a8ccf13fa62775277a5d56844878c828bbb3be1a", 144 | "version" : "1.27.0" 145 | } 146 | }, 147 | { 148 | "identity" : "swift-nio-ssl", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/apple/swift-nio-ssl.git", 151 | "state" : { 152 | "revision" : "e866a626e105042a6a72a870c88b4c531ba05f83", 153 | "version" : "2.24.0" 154 | } 155 | }, 156 | { 157 | "identity" : "swift-nio-transport-services", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 160 | "state" : { 161 | "revision" : "41f4098903878418537020075a4d8a6e20a0b182", 162 | "version" : "1.17.0" 163 | } 164 | }, 165 | { 166 | "identity" : "swift-numerics", 167 | "kind" : "remoteSourceControl", 168 | "location" : "https://github.com/apple/swift-numerics", 169 | "state" : { 170 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 171 | "version" : "1.0.2" 172 | } 173 | }, 174 | { 175 | "identity" : "swiftsoup", 176 | "kind" : "remoteSourceControl", 177 | "location" : "https://github.com/scinfu/SwiftSoup.git", 178 | "state" : { 179 | "revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6", 180 | "version" : "2.6.1" 181 | } 182 | }, 183 | { 184 | "identity" : "telegrammer", 185 | "kind" : "remoteSourceControl", 186 | "location" : "https://github.com/givip/Telegrammer.git", 187 | "state" : { 188 | "branch" : "master", 189 | "revision" : "459176871f1f52e34b1d20b22cdb4ee02cc98a30" 190 | } 191 | }, 192 | { 193 | "identity" : "telegrammer-vapor-middleware", 194 | "kind" : "remoteSourceControl", 195 | "location" : "https://github.com/CoolONEOfficial/telegrammer-vapor-middleware.git", 196 | "state" : { 197 | "branch" : "main", 198 | "revision" : "833b443375bf356ef9fc4663223ed6c4ab034f28" 199 | } 200 | }, 201 | { 202 | "identity" : "vapor", 203 | "kind" : "remoteSourceControl", 204 | "location" : "https://github.com/vapor/vapor.git", 205 | "state" : { 206 | "revision" : "e98077ddd1e3535bea6c9514f99d57463816c3bd", 207 | "version" : "4.77.2" 208 | } 209 | }, 210 | { 211 | "identity" : "vkontakter", 212 | "kind" : "remoteSourceControl", 213 | "location" : "https://github.com/CoolONEOfficial/Vkontakter.git", 214 | "state" : { 215 | "revision" : "67e01b14ebd97688eb05f309704d178133e84ed2", 216 | "version" : "0.1.4" 217 | } 218 | }, 219 | { 220 | "identity" : "vkontakter-vapor-middleware", 221 | "kind" : "remoteSourceControl", 222 | "location" : "https://github.com/CoolONEOfficial/vkontakter-vapor-middleware.git", 223 | "state" : { 224 | "branch" : "master", 225 | "revision" : "dcc92a6f03605d962bb73da16dbc9065afe538c0" 226 | } 227 | }, 228 | { 229 | "identity" : "websocket-kit", 230 | "kind" : "remoteSourceControl", 231 | "location" : "https://github.com/vapor/websocket-kit.git", 232 | "state" : { 233 | "revision" : "2b06a70dfcfa76a2e5079f60e3ae911511f09db0", 234 | "version" : "2.1.2" 235 | } 236 | } 237 | ], 238 | "version" : 2 239 | } 240 | -------------------------------------------------------------------------------- /Sources/Botter/Network/UpdatesServer.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// UpdatesServer.swift 3 | //// 4 | //// 5 | //// Created by Givi on 26.11.2019. 6 | //// 7 | // 8 | //import NIO 9 | //import NIOHTTP1 10 | // 11 | //private final class UpdatesHandler: ChannelInboundHandler { 12 | // 13 | // public typealias InboundIn = HTTPServerRequestPart 14 | // public typealias OutboundOut = HTTPServerResponsePart 15 | // 16 | // private enum State { 17 | // case idle 18 | // case waitingForRequestBody 19 | // case sendingResponse 20 | // 21 | // mutating func requestReceived() { 22 | // precondition(self == .idle, "Invalid state for request received: \(self)") 23 | // self = .waitingForRequestBody 24 | // } 25 | // 26 | // mutating func requestComplete() { 27 | // precondition(self == .waitingForRequestBody, "Invalid state for request complete: \(self)") 28 | // self = .sendingResponse 29 | // } 30 | // 31 | // mutating func responseComplete() { 32 | // precondition(self == .sendingResponse, "Invalid state for response complete: \(self)") 33 | // self = .idle 34 | // } 35 | // } 36 | // 37 | // private var buffer: ByteBuffer! = nil 38 | // private var keepAlive = false 39 | // private var state = State.idle 40 | // 41 | // private var infoSavedRequestHead: HTTPRequestHead? 42 | // private var infoSavedBodyBytes: Int = 0 43 | // 44 | // private var continuousCount: Int = 0 45 | // 46 | // private var handler: ((ChannelHandlerContext, HTTPServerRequestPart) -> Void)? 47 | // 48 | // private let dispatcher: Dispatcher 49 | // 50 | // init(dispatcher: Dispatcher) { 51 | // self.dispatcher = dispatcher 52 | // } 53 | // 54 | // private func completeResponse( 55 | // _ context: ChannelHandlerContext, 56 | // trailers: HTTPHeaders?, 57 | // promise: EventLoopPromise? 58 | // ) { 59 | // self.state.responseComplete() 60 | // 61 | // let promise = self.keepAlive ? promise : (promise ?? context.eventLoop.makePromise()) 62 | // if !self.keepAlive { 63 | // promise!.futureResult.whenComplete { (_: Result) in context.close(promise: nil) } 64 | // } 65 | // self.handler = nil 66 | // 67 | // context.writeAndFlush(self.wrapOutboundOut(.end(trailers)), promise: promise) 68 | // } 69 | // 70 | // private func httpResponseHead( 71 | // request: HTTPRequestHead, 72 | // status: HTTPResponseStatus, 73 | // headers: HTTPHeaders = HTTPHeaders() 74 | // ) -> HTTPResponseHead { 75 | // var head = HTTPResponseHead(version: request.version, status: status, headers: headers) 76 | // let connectionHeaders: [String] = head.headers[canonicalForm: "connection"].map { $0.lowercased() } 77 | // 78 | // if !connectionHeaders.contains("keep-alive") && !connectionHeaders.contains("close") { 79 | // // the user hasn't pre-set either 'keep-alive' or 'close', so we might need to add headers 80 | // switch (request.isKeepAlive, request.version.major, request.version.minor) { 81 | // case (true, 1, 0): 82 | // // HTTP/1.0 and the request has 'Connection: keep-alive', we should mirror that 83 | // head.headers.add(name: "Connection", value: "keep-alive") 84 | // case (false, 1, let n) where n >= 1: 85 | // // HTTP/1.1 (or treated as such) and the request has 'Connection: close', we should mirror that 86 | // head.headers.add(name: "Connection", value: "close") 87 | // default: 88 | // // we should match the default or are dealing with some HTTP that we don't support, let's leave as is 89 | // () 90 | // } 91 | // } 92 | // return head 93 | // } 94 | // 95 | // func channelRead(context: ChannelHandlerContext, data: NIOAny) { 96 | // let reqPart = self.unwrapInboundIn(data) 97 | // if let handler = self.handler { 98 | // handler(context, reqPart) 99 | // return 100 | // } 101 | // 102 | // switch reqPart { 103 | // case .head(let request): 104 | // self.keepAlive = request.isKeepAlive 105 | // self.state.requestReceived() 106 | // 107 | // var responseHead = httpResponseHead( 108 | // request: request, 109 | // status: HTTPResponseStatus.ok 110 | // ) 111 | // 112 | // self.buffer.clear() 113 | // responseHead.headers.add(name: "content-length", value: "\(self.buffer!.readableBytes)") 114 | // let response = HTTPServerResponsePart.head(responseHead) 115 | // context.write(self.wrapOutboundOut(response), promise: nil) 116 | // case .body(let bytes): 117 | // dispatcher.enqueue(bytebuffer: bytes) 118 | // case .end: 119 | // self.state.requestComplete() 120 | // let content = HTTPServerResponsePart.body(.byteBuffer(buffer!.slice())) 121 | // context.write(self.wrapOutboundOut(content), promise: nil) 122 | // self.completeResponse(context, trailers: nil, promise: nil) 123 | // } 124 | // } 125 | // 126 | // func channelReadComplete(context: ChannelHandlerContext) { 127 | // context.flush() 128 | // } 129 | // 130 | // func handlerAdded(context: ChannelHandlerContext) { 131 | // self.buffer = context.channel.allocator.buffer(capacity: 0) 132 | // } 133 | // 134 | // func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 135 | // switch event { 136 | // case let evt as ChannelEvent where evt == ChannelEvent.inputClosed: 137 | // // The remote peer half-closed the channel. At this time, any 138 | // // outstanding response will now get the channel closed, and 139 | // // if we are idle or waiting for a request body to finish we 140 | // // will close the channel immediately. 141 | // switch self.state { 142 | // case .idle, .waitingForRequestBody: 143 | // context.close(promise: nil) 144 | // case .sendingResponse: 145 | // self.keepAlive = false 146 | // } 147 | // default: 148 | // context.fireUserInboundEventTriggered(event) 149 | // } 150 | // } 151 | //} 152 | // 153 | //final class UpdatesServer { 154 | // let host: String 155 | // let port: Int 156 | // var channel: Channel? 157 | // var handler: Dispatcher 158 | // 159 | // private var group: EventLoopGroup? 160 | // private var socketBootstrap: ServerBootstrap? 161 | // 162 | // init(host: String, port: Int, handler: Dispatcher) { 163 | // self.host = host 164 | // self.port = port 165 | // self.handler = handler 166 | // } 167 | // 168 | // func childChannelInitializer(channel: Channel) -> EventLoopFuture { 169 | // return channel.pipeline 170 | // .configureHTTPServerPipeline(withErrorHandling: true) 171 | // .flatMap { 172 | // channel.pipeline.addHandler( 173 | // UpdatesHandler(dispatcher: self.handler) 174 | // ) 175 | // } 176 | // } 177 | // 178 | // func start() throws -> Future { 179 | // group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) 180 | // socketBootstrap = ServerBootstrap(group: group!) 181 | // // Specify backlog and enable SO_REUSEADDR for the server itself 182 | // .serverChannelOption(ChannelOptions.backlog, value: 256) 183 | // .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 184 | // 185 | // // Set the handlers that are applied to the accepted Channels 186 | // .childChannelInitializer(childChannelInitializer) 187 | // 188 | // // Enable SO_REUSEADDR for the accepted Channels 189 | // .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 190 | // .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) 191 | // .childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: true) 192 | // 193 | // let promise = group!.next().makePromise(of: Void.self) 194 | // 195 | // socketBootstrap! 196 | // .bind(host: host, port: port) 197 | // .whenSuccess { (channel) in 198 | // self.channel = channel 199 | // promise.succeed(()) 200 | // } 201 | // return promise.futureResult 202 | // } 203 | // 204 | // func stop() throws -> Future { 205 | // guard let channel = channel else { 206 | // throw BotError() 207 | // } 208 | // try group?.syncShutdownGracefully() 209 | // return channel.close() 210 | // } 211 | //} 212 | -------------------------------------------------------------------------------- /Sources/Botter/Bot/Methods/Bot+sendMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Nickolay Truhin on 02.12.2020. 6 | // 7 | 8 | import Telegrammer 9 | import Vkontakter 10 | import Foundation 11 | import NIO 12 | import Vapor 13 | 14 | extension Vkontakter.Bot.SavedDoc { 15 | var fileInfoType: FileInfoType { 16 | switch self { 17 | case .graffiti(_): 18 | fatalError() 19 | case .audio(_): 20 | fatalError() 21 | case .doc(_): 22 | return .document 23 | case .photo(_): 24 | return .photo 25 | } 26 | } 27 | } 28 | 29 | public enum SendDestination: AutoCodable { 30 | case chatId(Int64) 31 | case username(String) 32 | case userId(Int64) 33 | } 34 | 35 | public extension SendDestination { 36 | init(platform: AnyPlatform, id: Int64) { 37 | switch platform { 38 | case .tg: 39 | self = .chatId(id) 40 | case .vk: 41 | self = .userId(id) 42 | } 43 | } 44 | 45 | var userId: Int64? { 46 | if case let .userId(id) = self { 47 | return id 48 | } 49 | return nil 50 | } 51 | 52 | var chatId: Int64? { 53 | if case let .chatId(id) = self { 54 | return id 55 | } 56 | return nil 57 | } 58 | 59 | var peerId: Int64? { 60 | switch self { 61 | case let .chatId(chatId): 62 | return chatId 63 | case let .userId(userId): 64 | return userId 65 | default: 66 | return nil 67 | } 68 | } 69 | 70 | func tgChatId() throws -> Telegrammer.ChatId { 71 | switch self { 72 | case let .chatId(chatId): 73 | return .chat(chatId) 74 | case .userId, .username: 75 | throw Bot.SendMessageError.destinationNotFound 76 | } 77 | } 78 | } 79 | 80 | public extension Bot { 81 | 82 | enum SendMessageError: Error { 83 | case destinationNotFound 84 | case textNotFound 85 | case platformAttachmentIdNotFound 86 | } 87 | 88 | /// Parameters container struct for `sendMessage` method 89 | class SendMessageParams: Codable { 90 | 91 | public var destination: SendDestination? 92 | 93 | /// Текст личного сообщения. 94 | public var text: String? 95 | 96 | /// Объект, описывающий клавиатуру бота. 97 | public var keyboard: Keyboard? 98 | 99 | /// Вложения прикрепленные к сообщению. 100 | public var attachments: [FileInfo]? 101 | 102 | public convenience init?(to replyable: Replyable, text: String? = nil, keyboard: Keyboard? = nil, attachments: [FileInfo]? = nil) { 103 | guard let destination = replyable.destination else { return nil } 104 | self.init(destination: destination, text: text, keyboard: keyboard, attachments: attachments) 105 | } 106 | 107 | public init(destination: SendDestination? = nil, text: String? = nil, keyboard: Keyboard? = nil, attachments: [FileInfo]? = nil) { 108 | self.destination = destination 109 | self.text = text 110 | self.keyboard = keyboard 111 | self.attachments = attachments 112 | } 113 | 114 | func tgMessage(destination: SendDestination, _ content: String) throws -> Telegrammer.Bot.SendMessageParams { 115 | .init(chatId: try destination.tgChatId(), text: content, replyMarkup: keyboard?.tg) 116 | } 117 | 118 | func tgPhoto(destination: SendDestination, _ content: FileInfo.Content) throws -> Telegrammer.Bot.SendPhotoParams { 119 | guard let photo = content.tg else { throw SendMessageError.platformAttachmentIdNotFound } 120 | return .init(chatId: try destination.tgChatId(), photo: photo, caption: text, parseMode: nil, disableNotification: nil, replyToMessageId: nil, replyMarkup: keyboard?.tg) 121 | } 122 | 123 | func tgGroup(destination: SendDestination, _ content: [FileInfo]) throws -> Telegrammer.Bot.SendMediaGroupParams { 124 | .init(chatId: try destination.tgChatId(), media: content.compactMap { $0.tgMedia(caption: text) }) 125 | } 126 | 127 | func tgDocument(destination: SendDestination, _ content: FileInfo.Content) throws -> Telegrammer.Bot.SendDocumentParams { 128 | guard let photo = content.tg else { throw SendMessageError.platformAttachmentIdNotFound } 129 | return .init(chatId: try destination.tgChatId(), document: photo, caption: text, parseMode: nil, disableNotification: nil, replyToMessageId: nil, replyMarkup: keyboard?.tg) 130 | } 131 | 132 | func vk(peerId: Int64) -> Vkontakter.Bot.SendMessageParams { 133 | .init(randomId: .random(), peerId: peerId, message: text, attachment: attachments != nil ? .init(attachments!.compactMap { $0.vk }) : nil, keyboard: keyboard?.vk) 134 | } 135 | 136 | } 137 | 138 | @discardableResult 139 | func sendMessage(_ params: SendMessageParams, platform: AnyPlatform, context: BotContextProtocol) throws -> Future<[Message]> { 140 | guard let destination = params.destination else { throw SendMessageError.destinationNotFound } 141 | switch platform { 142 | case .vk: 143 | let vk = try requireVkBot() 144 | 145 | switch destination { 146 | case let .chatId(peerId), 147 | let .userId(peerId): 148 | return try sendVkMessage(vk: vk, params: params, peerId: peerId, app: context.app).map { [$0] } 149 | 150 | case let .username(username): 151 | return try vk.getUser(params: .init(userIds: [ .username(username) ])).map(\.first?.id).unwrap(orError: SendMessageError.destinationNotFound).flatMap { userId in 152 | try! self.sendVkMessage(vk: vk, params: params, peerId: userId, app: context.app).map { [$0] } 153 | } 154 | } 155 | 156 | case .tg: 157 | let tg = try requireTgBot() 158 | 159 | return try sendTgMessage(tg: tg, params: params, destination: destination, app: context.app) 160 | } 161 | } 162 | 163 | private func sendVkMessage(vk: Vkontakter.Bot, params: SendMessageParams, peerId: Int64, app: Application) throws -> Future { 164 | 165 | let vkParams = params.vk(peerId: peerId) 166 | 167 | if let attachments = params.attachments, !attachments.isEmpty { 168 | 169 | var uploadedAttachments: [FileInfo?] = .init(repeating: nil, count: attachments.count) 170 | 171 | let futures: [Future] = try attachments.enumerated() 172 | .compactMap { (index, attachment) -> Future? in 173 | 174 | let vkFile: Vkontakter.InputFile 175 | switch attachment.content { 176 | case .fileId: return nil 177 | 178 | case let .url(url): 179 | guard let url = URL(string: url) else { return nil } 180 | guard let data = try? Data(contentsOf: url) else { return nil } 181 | vkFile = .init(data: data, filename: url.lastPathComponent) 182 | 183 | case let .file(file): 184 | vkFile = file.vk 185 | } 186 | 187 | let uploadFuture: Future<[Vkontakter.Bot.SavedDoc]> 188 | switch attachment.type { 189 | case .photo: 190 | uploadFuture = try vk.upload(vkFile, as: .photo, for: .message) 191 | case .document: 192 | uploadFuture = try vk.upload(vkFile, as: .doc(peerId: peerId), for: .message) 193 | } 194 | 195 | let uploadSingleFuture = uploadFuture.map({ res in res.first! }) 196 | uploadSingleFuture.whenSuccess { res in 197 | uploadedAttachments[index] = .init( 198 | type: res.fileInfoType, 199 | content: .fileId(res.attachable.botterAttachable) 200 | ) 201 | } 202 | return uploadSingleFuture 203 | } 204 | 205 | return futures.flatten(on: app.eventLoopGroup.next()).flatMap { attachments in 206 | 207 | vkParams.attachment?.array.append(contentsOf: uploadedAttachments.compactMap { $0?.vk }) 208 | return try! vk.sendMessage(params: vkParams).map { Message(params: vkParams, resp: $0)! } 209 | } 210 | } 211 | 212 | return try vk.sendMessage(params: vkParams).map { Message(params: vkParams, resp: $0)! } 213 | } 214 | 215 | private func sendTgMessage(tg: Telegrammer.Bot, params: SendMessageParams, destination: SendDestination, app: Application) throws -> Future<[Message]> { 216 | if let attachments = params.attachments, !attachments.isEmpty { 217 | if attachments.count == 1 { 218 | let attachment = attachments.first! 219 | let future: Future<[Message]> 220 | switch attachment.type { 221 | case .photo: 222 | let params = try params.tgPhoto(destination: destination, attachment.content) 223 | future = try tg.sendPhoto(params: params).map { [Message(from: $0)] } 224 | case .document: 225 | let params = try params.tgDocument(destination: destination, attachment.content) 226 | future = try tg.sendDocument(params: params).map { [Message(from: $0)] } 227 | } 228 | return future 229 | } else { 230 | var future: Future<[Message]> 231 | 232 | let mediaGroupParams = try params.tgGroup(destination: destination, attachments) 233 | future = try tg.sendMediaGroup(params: mediaGroupParams).map { $0.map { Message(from: $0) } } 234 | 235 | if let text = params.text { 236 | let messageParams = try params.tgMessage(destination: destination, text) 237 | future = future.flatMap { messages in 238 | try! tg.sendMessage(params: messageParams).map { [Message(from: $0)] + messages } 239 | } 240 | } 241 | 242 | return future 243 | } 244 | } 245 | 246 | guard let text = params.text else { throw SendMessageError.textNotFound } 247 | let params = try params.tgMessage(destination: destination, text) 248 | return try tg.sendMessage(params: params).map { [Message(from: $0)] } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Sources/Botter/Generated/AutoCodable.generated.swift: -------------------------------------------------------------------------------- 1 | // Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery 2 | // DO NOT EDIT 3 | 4 | 5 | extension Attachment { 6 | 7 | enum CodingKeys: String, CodingKey { 8 | case photo 9 | case document 10 | } 11 | 12 | public init(from decoder: Decoder) throws { 13 | let container = try decoder.container(keyedBy: CodingKeys.self) 14 | 15 | if container.allKeys.contains(.photo), try container.decodeNil(forKey: .photo) == false { 16 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .photo) 17 | let associatedValue0 = try associatedValues.decode(Photo.self) 18 | self = .photo(associatedValue0) 19 | return 20 | } 21 | if container.allKeys.contains(.document), try container.decodeNil(forKey: .document) == false { 22 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .document) 23 | let associatedValue0 = try associatedValues.decode(Document.self) 24 | self = .document(associatedValue0) 25 | return 26 | } 27 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 28 | } 29 | 30 | public func encode(to encoder: Encoder) throws { 31 | var container = encoder.container(keyedBy: CodingKeys.self) 32 | 33 | switch self { 34 | case let .photo(associatedValue0): 35 | var associatedValues = container.nestedUnkeyedContainer(forKey: .photo) 36 | try associatedValues.encode(associatedValue0) 37 | case let .document(associatedValue0): 38 | var associatedValues = container.nestedUnkeyedContainer(forKey: .document) 39 | try associatedValues.encode(associatedValue0) 40 | } 41 | } 42 | 43 | } 44 | 45 | extension Button.Action { 46 | 47 | enum CodingKeys: String, CodingKey { 48 | case text 49 | case link 50 | case location 51 | case pay 52 | case app 53 | case callback 54 | } 55 | 56 | public init(from decoder: Decoder) throws { 57 | let container = try decoder.container(keyedBy: CodingKeys.self) 58 | 59 | if container.allKeys.contains(.text), try container.decodeNil(forKey: .text) == false { 60 | self = .text 61 | return 62 | } 63 | if container.allKeys.contains(.link), try container.decodeNil(forKey: .link) == false { 64 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .link) 65 | let associatedValue0 = try associatedValues.decode(Link.self) 66 | self = .link(associatedValue0) 67 | return 68 | } 69 | if container.allKeys.contains(.location), try container.decodeNil(forKey: .location) == false { 70 | self = .location 71 | return 72 | } 73 | if container.allKeys.contains(.pay), try container.decodeNil(forKey: .pay) == false { 74 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .pay) 75 | let associatedValue0 = try associatedValues.decode(Pay.self) 76 | self = .pay(associatedValue0) 77 | return 78 | } 79 | if container.allKeys.contains(.app), try container.decodeNil(forKey: .app) == false { 80 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .app) 81 | let associatedValue0 = try associatedValues.decode(App.self) 82 | self = .app(associatedValue0) 83 | return 84 | } 85 | if container.allKeys.contains(.callback), try container.decodeNil(forKey: .callback) == false { 86 | self = .callback 87 | return 88 | } 89 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 90 | } 91 | 92 | public func encode(to encoder: Encoder) throws { 93 | var container = encoder.container(keyedBy: CodingKeys.self) 94 | 95 | switch self { 96 | case .text: 97 | _ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .text) 98 | case let .link(associatedValue0): 99 | var associatedValues = container.nestedUnkeyedContainer(forKey: .link) 100 | try associatedValues.encode(associatedValue0) 101 | case .location: 102 | _ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .location) 103 | case let .pay(associatedValue0): 104 | var associatedValues = container.nestedUnkeyedContainer(forKey: .pay) 105 | try associatedValues.encode(associatedValue0) 106 | case let .app(associatedValue0): 107 | var associatedValues = container.nestedUnkeyedContainer(forKey: .app) 108 | try associatedValues.encode(associatedValue0) 109 | case .callback: 110 | _ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .callback) 111 | } 112 | } 113 | 114 | } 115 | 116 | extension FileInfo.Content { 117 | 118 | enum CodingKeys: String, CodingKey { 119 | case fileId 120 | case url 121 | case file 122 | } 123 | 124 | public init(from decoder: Decoder) throws { 125 | let container = try decoder.container(keyedBy: CodingKeys.self) 126 | 127 | if container.allKeys.contains(.fileId), try container.decodeNil(forKey: .fileId) == false { 128 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .fileId) 129 | let associatedValue0 = try associatedValues.decode(BotterAttachable.self) 130 | self = .fileId(associatedValue0) 131 | return 132 | } 133 | if container.allKeys.contains(.url), try container.decodeNil(forKey: .url) == false { 134 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .url) 135 | let associatedValue0 = try associatedValues.decode(String.self) 136 | self = .url(associatedValue0) 137 | return 138 | } 139 | if container.allKeys.contains(.file), try container.decodeNil(forKey: .file) == false { 140 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .file) 141 | let associatedValue0 = try associatedValues.decode(InputFile.self) 142 | self = .file(associatedValue0) 143 | return 144 | } 145 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 146 | } 147 | 148 | public func encode(to encoder: Encoder) throws { 149 | var container = encoder.container(keyedBy: CodingKeys.self) 150 | 151 | switch self { 152 | case let .fileId(associatedValue0): 153 | var associatedValues = container.nestedUnkeyedContainer(forKey: .fileId) 154 | try associatedValues.encode(associatedValue0) 155 | case let .url(associatedValue0): 156 | var associatedValues = container.nestedUnkeyedContainer(forKey: .url) 157 | try associatedValues.encode(associatedValue0) 158 | case let .file(associatedValue0): 159 | var associatedValues = container.nestedUnkeyedContainer(forKey: .file) 160 | try associatedValues.encode(associatedValue0) 161 | } 162 | } 163 | 164 | } 165 | 166 | extension SendDestination { 167 | 168 | enum CodingKeys: String, CodingKey { 169 | case chatId 170 | case username 171 | case userId 172 | } 173 | 174 | public init(from decoder: Decoder) throws { 175 | let container = try decoder.container(keyedBy: CodingKeys.self) 176 | 177 | if container.allKeys.contains(.chatId), try container.decodeNil(forKey: .chatId) == false { 178 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .chatId) 179 | let associatedValue0 = try associatedValues.decode(Int64.self) 180 | self = .chatId(associatedValue0) 181 | return 182 | } 183 | if container.allKeys.contains(.username), try container.decodeNil(forKey: .username) == false { 184 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .username) 185 | let associatedValue0 = try associatedValues.decode(String.self) 186 | self = .username(associatedValue0) 187 | return 188 | } 189 | if container.allKeys.contains(.userId), try container.decodeNil(forKey: .userId) == false { 190 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .userId) 191 | let associatedValue0 = try associatedValues.decode(Int64.self) 192 | self = .userId(associatedValue0) 193 | return 194 | } 195 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 196 | } 197 | 198 | public func encode(to encoder: Encoder) throws { 199 | var container = encoder.container(keyedBy: CodingKeys.self) 200 | 201 | switch self { 202 | case let .chatId(associatedValue0): 203 | var associatedValues = container.nestedUnkeyedContainer(forKey: .chatId) 204 | try associatedValues.encode(associatedValue0) 205 | case let .username(associatedValue0): 206 | var associatedValues = container.nestedUnkeyedContainer(forKey: .username) 207 | try associatedValues.encode(associatedValue0) 208 | case let .userId(associatedValue0): 209 | var associatedValues = container.nestedUnkeyedContainer(forKey: .userId) 210 | try associatedValues.encode(associatedValue0) 211 | } 212 | } 213 | 214 | } 215 | 216 | extension Update.Content { 217 | 218 | enum CodingKeys: String, CodingKey { 219 | case message 220 | case event 221 | } 222 | 223 | public init(from decoder: Decoder) throws { 224 | let container = try decoder.container(keyedBy: CodingKeys.self) 225 | 226 | if container.allKeys.contains(.message), try container.decodeNil(forKey: .message) == false { 227 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .message) 228 | let associatedValue0 = try associatedValues.decode(Message.self) 229 | self = .message(associatedValue0) 230 | return 231 | } 232 | if container.allKeys.contains(.event), try container.decodeNil(forKey: .event) == false { 233 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .event) 234 | let associatedValue0 = try associatedValues.decode(MessageEvent.self) 235 | self = .event(associatedValue0) 236 | return 237 | } 238 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 239 | } 240 | 241 | public func encode(to encoder: Encoder) throws { 242 | var container = encoder.container(keyedBy: CodingKeys.self) 243 | 244 | switch self { 245 | case let .message(associatedValue0): 246 | var associatedValues = container.nestedUnkeyedContainer(forKey: .message) 247 | try associatedValues.encode(associatedValue0) 248 | case let .event(associatedValue0): 249 | var associatedValues = container.nestedUnkeyedContainer(forKey: .event) 250 | try associatedValues.encode(associatedValue0) 251 | } 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /Templates/AutoCodable.swifttemplate: -------------------------------------------------------------------------------- 1 | <% 2 | func capitalizedName(for variable: Variable) -> String { 3 | return "\(String(variable.name.first!).capitalized)\(String(variable.name.dropFirst()))" 4 | } 5 | func customDecodingMethod(for variable: Variable, of type: Type) -> SourceryMethod? { 6 | return type.staticMethods.first { $0.selectorName == "decode\(capitalizedName(for: variable))(from:)" } 7 | } 8 | func defaultDecodingValue(for variable: Variable, of type: Type) -> Variable? { 9 | return type.staticVariables.first { $0.name == "default\(capitalizedName(for: variable))" } 10 | } 11 | func decodingContainerMethod(for type: Type) -> SourceryMethod? { 12 | if let enumType = type as? Enum, !enumType.hasAssociatedValues { 13 | return SourceryMethod(name: "singleValueContainer", throws: true) 14 | } 15 | return type.staticMethods.first { $0.selectorName == "decodingContainer(_:)" } 16 | } 17 | func customEncodingMethod(for variable: Variable, of type: Type) -> SourceryMethod? { 18 | return type.instanceMethods.first { $0.selectorName == "encode\(capitalizedName(for: variable))(to:)" } 19 | } 20 | func encodeAdditionalVariablesMethod(for type: Type) -> SourceryMethod? { 21 | return type.instanceMethods.first { $0.selectorName == "encodeAdditionalValues(to:)" } 22 | } 23 | func encodingContainerMethod(for type: Type) -> SourceryMethod? { 24 | if let enumType = type as? Enum, !enumType.hasAssociatedValues { 25 | return SourceryMethod(name: "singleValueContainer") 26 | } 27 | return type.instanceMethods.first { $0.selectorName == "encodingContainer(_:)" } 28 | } 29 | func typeHasMoreCodingKeysThanStoredProperties(_ type: Type, codingKeys: [String]) -> Bool { 30 | let allKeysSet = Set(codingKeys) 31 | let allStoredPropertiesNames = Set(type.storedVariables.map({ $0.name })) 32 | let hasMoreKeys = allKeysSet.subtracting(allStoredPropertiesNames).count > 0 33 | return hasMoreKeys 34 | } 35 | func needsDecodableImplementation(for type: Type, codingKeys: (generated: [String], all: [String])) -> Bool { 36 | guard type.implements["AutoDecodable"] != nil else { return false } 37 | if let enumType = type as? Enum, enumType.rawTypeName == nil { return true } 38 | 39 | if type.storedVariables.contains(where: { customDecodingMethod(for: $0, of: type) != nil }) { return true } 40 | if type.storedVariables.contains(where: { defaultDecodingValue(for: $0, of: type) != nil }) { return true } 41 | if decodingContainerMethod(for: type) != nil { return true } 42 | if typeHasMoreCodingKeysThanStoredProperties(type, codingKeys: codingKeys.all) { return true } 43 | return false 44 | } 45 | func needsEncodableImplementation(for type: Type, codingKeys: (generated: [String], all: [String])) -> Bool { 46 | guard type.implements["AutoEncodable"] != nil else { return false } 47 | if let enumType = type as? Enum, enumType.rawTypeName == nil { return true } 48 | 49 | if type.variables.contains(where: { customEncodingMethod(for: $0, of: type) != nil }) { return true } 50 | if encodeAdditionalVariablesMethod(for: type) != nil { return true } 51 | if decodingContainerMethod(for: type) != nil { return true } 52 | if typeHasMoreCodingKeysThanStoredProperties(type, codingKeys: codingKeys.all) { return true } 53 | if ((type.containedType["SkipEncodingKeys"] as? Enum)?.cases.count ?? 0) > 0 { return true } 54 | return false 55 | } 56 | func codingKeysFor(_ type: Type) -> (generated: [String], all: [String]) { 57 | var generatedKeys = [String]() 58 | var allCodingKeys = [String]() 59 | if type is Struct { 60 | if let codingKeysType = type.containedType["CodingKeys"] as? Enum { 61 | allCodingKeys = codingKeysType.cases.map({ $0.name }) 62 | let definedKeys = Set(allCodingKeys) 63 | let storedVariablesKeys = type.storedVariables.filter({ $0.defaultValue == nil }).map({ $0.name }) 64 | let computedVariablesKeys = type.computedVariables.filter({ customEncodingMethod(for: $0, of: type) != nil }).map({ $0.name }) 65 | 66 | if (storedVariablesKeys.count + computedVariablesKeys.count) > definedKeys.count { 67 | for key in storedVariablesKeys where !definedKeys.contains(key) { 68 | generatedKeys.append(key) 69 | allCodingKeys.append(key) 70 | } 71 | for key in computedVariablesKeys where !definedKeys.contains(key) { 72 | generatedKeys.append(key) 73 | allCodingKeys.append(key) 74 | } 75 | } 76 | } else { 77 | for variable in type.storedVariables { 78 | generatedKeys.append(variable.name) 79 | allCodingKeys.append(variable.name) 80 | } 81 | for variable in type.computedVariables { 82 | guard customEncodingMethod(for: variable, of: type) != nil else { continue } 83 | generatedKeys.append(variable.name) 84 | allCodingKeys.append(variable.name) 85 | } 86 | } 87 | } else if let enumType = type as? Enum { 88 | var casesKeys: [String] = enumType.cases.map({ $0.name }) 89 | if enumType.hasAssociatedValues { 90 | enumType.cases 91 | .flatMap({ $0.associatedValues }) 92 | .compactMap({ $0.localName }) 93 | .forEach({ 94 | if !casesKeys.contains($0) { 95 | casesKeys.append($0) 96 | } 97 | }) 98 | } 99 | if let codingKeysType = type.containedType["CodingKeys"] as? Enum { 100 | allCodingKeys = codingKeysType.cases.map({ $0.name }) 101 | let definedKeys = Set(allCodingKeys) 102 | if casesKeys.count > definedKeys.count { 103 | for key in casesKeys where !definedKeys.contains(key) { 104 | generatedKeys.append(key) 105 | allCodingKeys.append(key) 106 | } 107 | } 108 | } else { 109 | allCodingKeys = casesKeys 110 | generatedKeys = allCodingKeys 111 | } 112 | } 113 | return (generated: generatedKeys, all: allCodingKeys) 114 | } 115 | -%> 116 | <%_ for type in types.all 117 | where (type is Struct || type is Enum) 118 | && (type.implements["AutoDecodable"] != nil || type.implements["AutoEncodable"] != nil) { -%> 119 | <%_ let codingKeys = codingKeysFor(type) -%> 120 | <%_ if let codingKeysType = type.containedType["CodingKeys"] as? Enum, codingKeys.generated.count > 0 { -%> 121 | // sourcery:inline:auto:<%= codingKeysType.name %>.AutoCodable 122 | <%_ for key in codingKeys.generated { -%> 123 | case <%= key %> 124 | <%_ } -%> 125 | // sourcery:end 126 | <%_ } -%> 127 | 128 | <%_ let typeNeedsDecodableImplementation = needsDecodableImplementation(for: type, codingKeys: codingKeys) -%> 129 | <%_ let typeNeedsEncodableImplementation = needsEncodableImplementation(for: type, codingKeys: codingKeys) -%> 130 | <%_ guard typeNeedsDecodableImplementation || typeNeedsEncodableImplementation else { continue } -%> 131 | extension <%= type.name %> { 132 | <%_ if type.containedType["CodingKeys"] as? Enum == nil { -%> 133 | 134 | enum CodingKeys: String, CodingKey { 135 | <%_ for key in codingKeys.generated { -%> 136 | case <%= key %> 137 | <%_ } -%> 138 | } 139 | <%_ }-%> 140 | 141 | <%_ if typeNeedsDecodableImplementation { -%> 142 | <%= type.accessLevel %> init(from decoder: Decoder) throws { 143 | <%_ if let containerMethod = decodingContainerMethod(for: type) { -%> 144 | let container = <% if containerMethod.throws { %>try <% } -%> 145 | <%_ if containerMethod.callName == "singleValueContainer" { %>decoder<% } else { -%><%= type.name %><% } -%> 146 | <%_ %>.<%= containerMethod.callName %>(<% if !containerMethod.parameters.isEmpty { %>decoder<% } %>) 147 | <%_ } else { -%> 148 | let container = try decoder.container(keyedBy: CodingKeys.self) 149 | <%_ } -%> 150 | 151 | <%_ if let enumType = type as? Enum { -%> 152 | <%_ if enumType.hasAssociatedValues { -%> 153 | <%_ if codingKeys.all.contains("enumCaseKey") { -%> 154 | let enumCase = try container.decode(String.self, forKey: .enumCaseKey) 155 | switch enumCase { 156 | <%_ for enumCase in enumType.cases { -%> 157 | case CodingKeys.<%= enumCase.name %>.rawValue: 158 | <%_ if enumCase.associatedValues.isEmpty { -%> 159 | self = .<%= enumCase.name %> 160 | <%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> 161 | // Enum cases with unnamed associated values can't be decoded 162 | throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Can't decode '\(enumCase)'") 163 | <%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> 164 | <%_ for associatedValue in enumCase.associatedValues { -%> 165 | let <%= associatedValue.localName! %> = try container.decode(<%= associatedValue.typeName %>.self, forKey: .<%= associatedValue.localName! %>) 166 | <%_ } -%> 167 | self = .<%= enumCase.name %>(<% -%> 168 | <%_ %><%= enumCase.associatedValues.map({ "\($0.localName!): \($0.localName!)" }).joined(separator: ", ") %>) 169 | <%_ } else { -%> 170 | // Enum cases with mixed named and unnamed associated values can't be decoded 171 | throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Can't decode '\(enumCase)'") 172 | <%_ } -%> 173 | <%_ } -%> 174 | default: 175 | throw DecodingError.dataCorruptedError(forKey: .enumCaseKey, in: container, debugDescription: "Unknown enum case '\(enumCase)'") 176 | } 177 | <%_ } else { -%> 178 | <%_ for enumCase in enumType.cases { -%> 179 | if container.allKeys.contains(.<%= enumCase.name %>), try container.decodeNil(forKey: .<%= enumCase.name %>) == false { 180 | <%_ if enumCase.associatedValues.isEmpty { -%> 181 | self = .<%= enumCase.name %> 182 | return 183 | <%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> 184 | var associatedValues = try container.nestedUnkeyedContainer(forKey: .<%= enumCase.name %>) 185 | <%_ for (index, associatedValue) in enumCase.associatedValues.enumerated() { -%> 186 | let associatedValue<%= index %> = try associatedValues.decode(<%= associatedValue.typeName %>.self) 187 | <%_ } -%> 188 | self = .<%= enumCase.name %>(<% -%> 189 | <%_ %><%= enumCase.associatedValues.indices.map({ "associatedValue\($0)" }).joined(separator: ", ") %>) 190 | return 191 | <%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> 192 | let associatedValues = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) 193 | <%_ for associatedValue in enumCase.associatedValues { -%> 194 | let <%= associatedValue.localName! %> = try associatedValues.decode(<%= associatedValue.typeName %>.self, forKey: .<%= associatedValue.localName! %>) 195 | <%_ } -%> 196 | self = .<%= enumCase.name %>(<% -%> 197 | <%_ %><%= enumCase.associatedValues.map({ "\($0.localName!): \($0.localName!)" }).joined(separator: ", ") %>) 198 | return 199 | <%_ } else { -%> 200 | // Enum cases with mixed named and unnamed associated values can't be decoded 201 | throw DecodingError.dataCorruptedError(forKey: .<%= enumCase.name %>, in: container, debugDescription: "Can't decode `.<%= enumCase.name %>`") 202 | <%_ } -%> 203 | } 204 | <%_ } -%> 205 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) 206 | <%_ } -%> 207 | <%_ } else { -%> 208 | let enumCase = try container.decode(String.self) 209 | switch enumCase { 210 | <%_ for enumCase in enumType.cases { -%> 211 | case CodingKeys.<%= enumCase.name %>.rawValue: self = .<%= enumCase.name %> 212 | <%_ } -%> 213 | default: throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case '\(enumCase)'")) 214 | } 215 | <%_ } -%> 216 | <%_ } else { -%> 217 | <%_ for key in codingKeys.all { -%> 218 | <%_ guard let variable = type.instanceVariables.first(where: { $0.name == key && !$0.isComputed }) else { continue } -%> 219 | <%_ let defaultValue = defaultDecodingValue(for: variable, of: type) -%> 220 | <%_ let customMethod = customDecodingMethod(for: variable, of: type) -%> 221 | <%_ let shouldTry = customMethod?.throws == true || customMethod == nil -%> 222 | <%_ let shouldWrapTry = shouldTry && defaultValue != nil -%> 223 | <%= variable.name %> = <% if shouldWrapTry { %>(try? <% } else if shouldTry { %>try <% } -%> 224 | <%_ if let customMethod = customMethod { -%> 225 | <%_ %><%= type.name %>.<%= customMethod.callName %>(from: <% if customMethod.parameters.first?.name == "decoder" { %>decoder<% } else { %>container<% } %>)<% -%> 226 | <%_ } else { -%> 227 | <%_ %>container.decode<% if variable.isOptional { %>IfPresent<% } %>(<%= variable.unwrappedTypeName %>.self, forKey: .<%= variable.name %>)<% -%> 228 | <%_ } -%> 229 | <%_ %><% if shouldWrapTry { %>)<% } -%> 230 | <%_ if let defaultValue = defaultValue { %> ?? <%= type.name %>.<%= defaultValue.name -%><%_ } %> 231 | <%_ } -%> 232 | <%_ } -%> 233 | } 234 | 235 | <%_ } -%> 236 | <%_ if typeNeedsEncodableImplementation { -%> 237 | <%= type.accessLevel %> func encode(to encoder: Encoder) throws { 238 | <%_ if let containerMethod = encodingContainerMethod(for: type) { -%> 239 | var container = <% if containerMethod.callName == "singleValueContainer" { %>encoder.<% } %><%= containerMethod.callName %>(<% if !containerMethod.parameters.isEmpty { %>encoder<% } %>) 240 | <%_ } else { -%> 241 | var container = encoder.container(keyedBy: CodingKeys.self) 242 | <%_ } -%> 243 | 244 | <%_ if let enumType = type as? Enum { -%> 245 | <%_ if enumType.hasAssociatedValues { -%> 246 | <%_ if codingKeys.all.contains("enumCaseKey") { -%> 247 | switch self { 248 | <%_ for enumCase in enumType.cases { -%> 249 | <%_ if enumCase.associatedValues.isEmpty { -%> 250 | case .<%= enumCase.name %>: 251 | try container.encode(CodingKeys.<%= enumCase.name %>.rawValue, forKey: .enumCaseKey) 252 | <%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> 253 | case .<%= enumCase.name %>: 254 | // Enum cases with unnamed associated values can't be encoded 255 | throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) 256 | <%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> 257 | case let .<%= enumCase.name %>(<%= enumCase.associatedValues.map({ "\($0.localName!)" }).joined(separator: ", ") %>): 258 | try container.encode(CodingKeys.<%= enumCase.name %>.rawValue, forKey: .enumCaseKey) 259 | <%_ for accociatedValue in enumCase.associatedValues { -%> 260 | try container.encode(<%= accociatedValue.localName! %>, forKey: .<%= accociatedValue.localName! %>) 261 | <%_ } -%> 262 | <%_ } else { -%> 263 | case .<%= enumCase.name %>: 264 | // Enum cases with mixed named and unnamed associated values can't be encoded 265 | throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) 266 | <%_ } -%> 267 | <%_ } -%> 268 | } 269 | <%_ } else { -%> 270 | switch self { 271 | <%_ for enumCase in enumType.cases { -%> 272 | <%_ if enumCase.associatedValues.isEmpty { -%> 273 | case .<%= enumCase.name %>: 274 | _ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) 275 | <%_ } else if enumCase.associatedValues.filter({ $0.localName == nil }).count == enumCase.associatedValues.count { -%> 276 | case let .<%= enumCase.name %>(<%= enumCase.associatedValues.indices.map({ "associatedValue\($0)" }).joined(separator: ", ") %>): 277 | var associatedValues = container.nestedUnkeyedContainer(forKey: .<%= enumCase.name %>) 278 | <%_ for (index, associatedValue) in enumCase.associatedValues.enumerated() { -%> 279 | try associatedValues.encode(associatedValue<%= index %>) 280 | <%_ } -%> 281 | <%_ } else if enumCase.associatedValues.filter({ $0.localName != nil }).count == enumCase.associatedValues.count { -%> 282 | case let .<%= enumCase.name %>(<%= enumCase.associatedValues.map({ "\($0.localName!)" }).joined(separator: ", ") %>): 283 | var associatedValues = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .<%= enumCase.name %>) 284 | <%_ for accociatedValue in enumCase.associatedValues { -%> 285 | try associatedValues.encode(<%= accociatedValue.localName! %>, forKey: .<%= accociatedValue.localName! %>) 286 | <%_ } -%> 287 | <%_ } else { -%> 288 | case .<%= enumCase.name %>: 289 | // Enum cases with mixed named and unnamed associated values can't be encoded 290 | throw EncodingError.invalidValue(self, .init(codingPath: encoder.codingPath, debugDescription: "Can't encode '\(self)'")) 291 | <%_ } -%> 292 | <%_ } -%> 293 | } 294 | <%_ } -%> 295 | <%_ } else { -%> 296 | switch self { 297 | <%_ for enumCase in enumType.cases { -%> 298 | case .<%= enumCase.name %>: try container.encode(CodingKeys.<%= enumCase.name %>.rawValue) 299 | <%_ } -%> 300 | } 301 | <%_ } -%> 302 | <%_ } else { -%> 303 | <%_ let skipKeys = type.containedType["SkipEncodingKeys"] as? Enum -%> 304 | <%_ for key in codingKeys.all { -%> 305 | <%_ if let skipKeys = skipKeys, skipKeys.cases.contains(where: { $0.name == key }) { continue } -%> 306 | <%_ guard let variable = type.instanceVariables.first(where: { $0.name == key }) ?? type.computedVariables.first(where: { $0.name == key }) else { continue } -%> 307 | <%_ let customMethod = customEncodingMethod(for: variable, of: type) -%> 308 | <%_ if let customMethod = customMethod { -%> 309 | <% if customMethod.throws { %>try <% } %><%= customMethod.callName %>(to: <% if customMethod.parameters.first?.name == "encoder" { %>encoder<% } else { %>&container<% } %>) 310 | <%_ } else { -%> 311 | try container.encode<% if variable.isOptional { %>IfPresent<% } %>(<%= variable.name %>, forKey: .<%= variable.name %>) 312 | <%_ } -%> 313 | <%_ } -%> 314 | <%_ if let encodeAdditional = encodeAdditionalVariablesMethod(for: type) { -%> 315 | <% if encodeAdditional.throws { %>try <% } %><%= encodeAdditional.callName %>(to: <% if encodeAdditional.parameters.first?.name == "encoder" { %>encoder<% } else { %>&container<% } %>) 316 | <%_ } -%> 317 | <%_ } -%> 318 | } 319 | 320 | <%_ } -%> 321 | } 322 | <% } -%> 323 | --------------------------------------------------------------------------------