├── .gitignore ├── Sources └── Pelican │ ├── Pelican │ ├── Client │ │ ├── ClientConnection+Error.swift │ │ ├── Result.swift │ │ └── Client.swift │ ├── Log │ │ ├── Log+Debug.swift │ │ ├── LogLevel.swift │ │ ├── Log+Enum.swift │ │ ├── Log.swift │ │ └── Log+Prints.swift │ ├── Files │ │ ├── Cache+Error.swift │ │ └── CacheFile.swift │ ├── Pelican+Delegates.swift │ ├── Pelican+Files.swift │ ├── Update │ │ └── UpdateType.swift │ ├── Pelican+Errors.swift │ ├── Schedule │ │ └── Schedule.swift │ └── Queues │ │ └── LoopQueue.swift │ ├── API │ ├── Types │ │ ├── Message │ │ │ ├── MessageParseMode.swift │ │ │ ├── MessageType.swift │ │ │ └── MessageEntity.swift │ │ ├── Markup │ │ │ ├── MarkupType.swift │ │ │ ├── Inline │ │ │ │ └── InlineKeyType.swift │ │ │ ├── ForceReply.swift │ │ │ └── Keyboard │ │ │ │ ├── KeyboardKey.swift │ │ │ │ └── KeyboardRemove.swift │ │ ├── Inline │ │ │ ├── InlineResult.swift │ │ │ ├── InputMessageContent │ │ │ │ ├── InputMessageContent_Any.swift │ │ │ │ ├── InputMessageContentType.swift │ │ │ │ ├── InputMessageContent_Contact.swift │ │ │ │ ├── InputMessageContent_Location.swift │ │ │ │ ├── InputMessageContent_Text.swift │ │ │ │ ├── InputMessageContent_Venue.swift │ │ │ │ └── InputMessageContent.swift │ │ │ ├── InlineQueryResult │ │ │ │ ├── InlineResultGame.swift │ │ │ │ ├── InlineResultSticker.swift │ │ │ │ ├── InlineResultAudio.swift │ │ │ │ ├── InlineResultLocation.swift │ │ │ │ ├── InlineResultVoice.swift │ │ │ │ ├── InlineResultVenue.swift │ │ │ │ ├── InlineResultContact.swift │ │ │ │ ├── InlineResultGif.swift │ │ │ │ ├── InlineResultPhoto.swift │ │ │ │ ├── InlineResultMpeg4Gif.swift │ │ │ │ ├── InlineResultDocument.swift │ │ │ │ ├── InlineResultVideo.swift │ │ │ │ └── InlineResultArticle.swift │ │ │ ├── InlineQuery.swift │ │ │ ├── ChosenInlineResult.swift │ │ │ ├── InlineResultType.swift │ │ │ └── InlineResultAny.swift │ │ ├── Stickers │ │ │ ├── MaskPositionPoint.swift │ │ │ ├── StickerSet.swift │ │ │ ├── MaskPosition.swift │ │ │ └── Sticker.swift │ │ ├── Type+Error.swift │ │ ├── Payments │ │ │ ├── ShippingOption.swift │ │ │ ├── LabeledPrice.swift │ │ │ ├── OrderInfo.swift │ │ │ ├── ShippingAddress.swift │ │ │ ├── ShippingQuery.swift │ │ │ ├── Invoice.swift │ │ │ ├── PreCheckoutQuery.swift │ │ │ └── SuccessfulPayment.swift │ │ ├── Chat │ │ │ ├── ChatPhoto.swift │ │ │ └── ChatAction.swift │ │ ├── Game │ │ │ ├── GameHighScore.swift │ │ │ ├── Animation.swift │ │ │ └── Game.swift │ │ ├── Albums │ │ │ ├── InputMediaPhoto.swift │ │ │ ├── InputMedia.swift │ │ │ └── InputMediaVideo.swift │ │ ├── User │ │ │ ├── UserProfilePhotos.swift │ │ │ └── User.swift │ │ ├── FileDownload.swift │ │ ├── Message Content │ │ │ ├── Location.swift │ │ │ ├── Contact.swift │ │ │ ├── Venue.swift │ │ │ ├── Photo.swift │ │ │ ├── Voice.swift │ │ │ ├── Document.swift │ │ │ ├── VideoNote.swift │ │ │ ├── Video.swift │ │ │ └── Audio.swift │ │ ├── Callback │ │ │ └── CallbackQuery.swift │ │ ├── Type+Extensions.swift │ │ ├── Type+Standard.swift │ │ ├── Type+Custom.swift │ │ └── Type+Protocols.swift │ ├── Request │ │ ├── Codable+String.swift │ │ ├── Request+Payments.swift │ │ ├── Request+Chat.swift │ │ └── Request+Answer.swift │ └── Response │ │ ├── TelegramResponseError.swift │ │ └── TelegramResponse.swift │ ├── Session │ ├── Core │ │ ├── SessionDelegate.swift │ │ └── SessionIDType.swift │ └── Modules │ │ ├── Event │ │ └── SessionEvent.swift │ │ ├── API Request │ │ ├── Sync │ │ │ ├── MethodRequestSync.swift │ │ │ ├── Sync+Answer.swift │ │ │ ├── Sync+Payments.swift │ │ │ ├── Sync+Edit.swift │ │ │ └── Sync+Stickers.swift │ │ ├── Async │ │ │ ├── MethodRequestAsync.swift │ │ │ ├── Async+Answer.swift │ │ │ └── Async+Edit.swift │ │ └── MethodRequest.swift │ │ ├── DispatchQueue │ │ └── SessionDispatchQueue.swift │ │ ├── Router │ │ └── Types │ │ │ ├── RouteManual.swift │ │ │ ├── RoutePass.swift │ │ │ └── RouteListen.swift │ │ └── Monitor │ │ ├── TimeoutMonitor.swift │ │ └── FloodMonitor.swift │ ├── Utilities │ ├── CasedEnum.swift │ └── Utilities.swift │ └── HTTP │ └── Version.swift ├── Package.swift ├── Tests └── PelicanTests │ ├── LogTest.swift │ ├── SetupTest.swift │ ├── MessageSendingTest.swift │ ├── TestTemplates.swift │ └── ConnectionTest.swift ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcodeproj 2 | Packages 3 | .build 4 | Package.pins 5 | Package.resolved 6 | Config 7 | Public -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Client/ClientConnection+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientConnection+Error.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 17/03/2018. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Log/Log+Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pelican+Debug.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension PelicanBot { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message/MessageParseMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageParseMode.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum MessageParseMode: String { 11 | case html = "HTML" 12 | case markdown = "Markdown" 13 | case none = "" 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Markup/MarkupType.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | /** 5 | Represents a Telegram "Markup Type", that defines additional special actions and interfaces 6 | alongside a message, such as creating a custom keyboard or forcing a userto reply to the sent 7 | message. 8 | */ 9 | public protocol MarkupType: Codable { 10 | 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Core/SessionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDelegate.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 17/10/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Conform to this type to provide a custom type that will be used to construct a Session and setup initial user interactions and responses. 12 | */ 13 | protocol SessionDelegate { 14 | 15 | 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineResult.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | 6 | /** 7 | Used for a result/your response of an inline query. 8 | */ 9 | public protocol InlineResult: Codable { 10 | 11 | /// Defines the metatype, purely used for encoding and decoding. 12 | var metatype: InlineResultType { get } 13 | 14 | /// Defines the type of the result being given. 15 | var type: String { get } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Stickers/MaskPositionPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskPositionPoint.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines a collection of positions that a mask should automatically be moved to when added to an image. 12 | */ 13 | public enum MaskPositionPoint: String, Codable { 14 | case forehead 15 | case eyes 16 | case mouth 17 | case chin 18 | } 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Pelican", 7 | products: [ 8 | .library(name: "Pelican", targets: ["Pelican"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", .upToNextMajor(from: "17.0.0")) 12 | ], 13 | targets: [ 14 | .target(name: "Pelican", dependencies: ["SwiftyJSON"]), 15 | ] 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Type+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type+Error.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 29/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | // Errors related to update processing. 11 | enum TypeError: String, Error { 12 | case ExtractFailed = "The extraction failed." 13 | } 14 | 15 | enum MarkupError: String, Error { 16 | case ExceededByteLimit = "The data assigned to this key exceeds the maximum byte limit." 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent_Any.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 22/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Used as a basic means by which to link all InputMessageContent types 12 | */ 13 | public protocol InputMessageContent_Any: Codable { 14 | 15 | // FIXME: An unusual hold-over for Codable, not sure if I need this anymore. 16 | static var type: InputMessageContentType { get } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/ShippingOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShippingOption.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents one shipping option for the Telegram Payment API. 13 | */ 14 | public struct ShippingOption: Codable { 15 | 16 | /// The shipping option identifier. 17 | var id: String = "" 18 | 19 | /// The name of the shipping option. 20 | var title: String = "" 21 | 22 | /// A list of costs associated with the shipping option. 23 | var prices: [LabeledPrice] = [] 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Log/LogLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogLevel.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 18/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | ??? 12 | */ 13 | public enum LogLevel: String, CasedEnum { 14 | 15 | /// The update/upload/responce thread cycles 16 | case verbose 17 | 18 | /// Requests or responses to and from the Telegram API 19 | case info 20 | 21 | /// Logs relating to Telegram API types 22 | case warning 23 | 24 | /// Session-related logs 25 | case error 26 | 27 | /// Logs relating to Session's modules 28 | case fatal 29 | 30 | public func string() -> String { 31 | return rawValue 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Chat/ChatPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatPhoto.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public struct ChatPhoto: Codable { 12 | 13 | /// Unique file identifier of small (160x160) chat photo. This file_id can be used only for photo download. 14 | public var smallFileID: String 15 | 16 | /// Unique file identifier of big (640x640) chat photo. This file_id can be used only for photo download. 17 | public var bigFileID: String 18 | 19 | public init(smallFileID: String, bigFileID: String) { 20 | self.smallFileID = smallFileID 21 | self.bigFileID = bigFileID 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Game/GameHighScore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameHighScore.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** This object represents one row of the high scores table for a game. 13 | */ 14 | public struct GameHighScore: Codable { 15 | 16 | /// The position in the high score table for the game. 17 | public var position: Int 18 | 19 | /// The user who made the score entry. 20 | public var user: User 21 | 22 | /// The score set. 23 | public var score: Int 24 | 25 | 26 | public init(user: User, score: Int, position: Int) { 27 | self.user = user 28 | self.score = score 29 | self.position = position 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Chat/ChatAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatAction.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 06/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines a type of chat action you can use via `setChatAction`, if an action your bot is taking will 12 | take some time but you wish to inform the people using it that the bot is still processing their request. 13 | */ 14 | public enum ChatAction: String { 15 | case typing = "typing" 16 | case photo = "upload_photo" 17 | case uploadVideo = "upload_video" 18 | case recordVideo = "record_video" 19 | case uploadAudio = "upload_audio" 20 | case recordAudio = "record_audio" 21 | case document = "upload_document" 22 | case location = "find_location" 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Albums/InputMediaPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMediaPhoto.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a photo to be sent in the context of an album. 12 | */ 13 | struct InputMediaPhoto: InputMedia { 14 | 15 | // PROTOCOL INHERITANCE 16 | public var type = "photo" 17 | public var media: String 18 | public var caption: String? 19 | 20 | /// Coding keys to map values when Encoding and Decoding. 21 | enum CodingKeys: String, CodingKey { 22 | case type 23 | case media 24 | case caption 25 | } 26 | 27 | public init(mediaLink media: String, caption: String?) { 28 | self.media = media 29 | self.caption = caption 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/PelicanTests/LogTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogTest.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 23/08/2017. 6 | // 7 | 8 | import XCTest 9 | 10 | class LogTest: XCTestCase { 11 | 12 | /// The template to be operated with. 13 | var ghost = try! GhostPelican() 14 | 15 | override func setUp() { 16 | 17 | // Build a new template 18 | ghost = try! GhostPelican() 19 | } 20 | 21 | override func tearDown() { 22 | 23 | } 24 | 25 | func testExample() { 26 | 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageContentType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 22/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum InputMessageContentType: String { 11 | 12 | case contact 13 | case location 14 | case text 15 | case venue 16 | 17 | /// Helps us define what we need to encode and decode later. 18 | var metatype: InputMessageContent_Any.Type { 19 | switch self { 20 | case .contact: 21 | return InputMessageContent_Contact.self 22 | case .location: 23 | return InputMessageContent_Location.self 24 | case .text: 25 | return InputMessageContent_Text.self 26 | case .venue: 27 | return InputMessageContent_Venue.self 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Core/SessionIDType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionIDType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines what kind of identifier a SessionTag is holding, which is important for interactions between a Session and other models 12 | like the Moderator, whose job is to manage titles and blacklists only for Chat and User ID types. 13 | */ 14 | public enum SessionIDType: String { 15 | 16 | /// Defines a single user on Telegram. 17 | case chat = "Chat" 18 | 19 | /// Defines a single chat on Telegram. 20 | case user = "User" 21 | 22 | /// Defines any other type of ID, typically only existing for that specific update. This ID will not work for any Moderator operations. 23 | case temporary = "Temporary" 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/User/UserProfilePhotos.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfilePhotos.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | public struct UserProfilePhotos: Codable { 13 | 14 | /// The total number of photos the user has on their profile. 15 | public var totalCount: Int 16 | 17 | /// The requested photos for the user (up to 4 sizes per unique photo). 18 | public var photos: [[Photo]] = [] 19 | 20 | /// Coding keys to map values when Encoding and Decoding. 21 | enum CodingKeys: String, CodingKey { 22 | case totalCount = "total_count" 23 | case photos 24 | } 25 | 26 | public init(photoSets: [Photo]...) { 27 | for photo in photoSets { 28 | photos.append(photo) 29 | } 30 | totalCount = photos.count 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Albums/InputMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMedia.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a type of media to be sent in the format of an album. 12 | */ 13 | public protocol InputMedia: Codable { 14 | 15 | /// The content type that this media represents. 16 | var type: String { get } 17 | 18 | /// The file to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. 19 | var media: String { get set } 20 | 21 | /// A caption for the document to be sent, 200 characters maximum. 22 | var caption: String? { get set } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Pelican/Utilities/CasedEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CasedEnum.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/10/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines convenience iteration and string extraction methods for various enumeration types, such as UpdateType. 12 | */ 13 | public protocol CasedEnum : Hashable { 14 | 15 | func string() -> String 16 | } 17 | 18 | extension CasedEnum { 19 | static func cases() -> AnySequence { 20 | typealias S = Self 21 | return AnySequence { () -> AnyIterator in 22 | var raw = 0 23 | return AnyIterator { 24 | let current : Self = withUnsafePointer(to: &raw) { $0.withMemoryRebound(to: S.self, capacity: 1) { $0.pointee } } 25 | guard current.hashValue == raw else { return nil } 26 | raw += 1 27 | return current 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/LabeledPrice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabeledPrice.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents a portion of the price for goods and services being sold (eg. item, discounts, taxes, shipping). 13 | */ 14 | public struct LabeledPrice: Codable { 15 | 16 | /// The name of the portion. 17 | var label: String 18 | 19 | /** 20 | The price of the portion in the smallest units of the currency. For example, for a price of US$ 1.45 pass amount = 145. 21 | 22 | See the exp parameter in [currencies.json](https://core.telegram.org/bots/payments/currencies.json) for more information, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). 23 | */ 24 | var amount: Int 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/OrderInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderInfo.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | This object represents information about an order for the Telegram Payment API. 13 | */ 14 | public struct OrderInfo: Codable { 15 | 16 | /// The user's name 17 | var name: String? 18 | 19 | /// The user's phone number. 20 | var phoneNumber: String? 21 | 22 | /// The user's email. 23 | var email: String? 24 | 25 | /// The user's shipping address. 26 | var shippingAddress: String? 27 | 28 | 29 | /// Coding keys to map values when Encoding and Decoding. 30 | enum CodingKeys: String, CodingKey { 31 | case name 32 | case phoneNumber = "phone_number" 33 | case email 34 | case shippingAddress = "shipping_address" 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Log/Log+Enum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug+Enums.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Sets the log content categories, for when you wish to debug specific things. 12 | */ 13 | enum DebugType: String, CasedEnum { 14 | 15 | /// The update/upload/responce thread cycles 16 | case cycles 17 | /// Requests or responses to and from the Telegram API 18 | case apiMethods 19 | /// Logs relating to Telegram API types 20 | case apiTypes 21 | /// Session-related logs 22 | case session 23 | /// Logs relating to Session's modules 24 | case modules 25 | /// Logs relating to the Pelican type, and other types it controls. 26 | case pelican 27 | /// Logs not belonging to any particular category 28 | case other 29 | 30 | public func string() -> String { 31 | return rawValue 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/ShippingAddress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShippingAddress.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents a shipping address for the Telegram Payment API. 13 | */ 14 | public struct ShippingAddress: Codable { 15 | 16 | /// ISO 3166-1 alpha-2 country code. 17 | var countryCode: String = "" 18 | 19 | /// The state, if applicable. 20 | var state: String? 21 | 22 | var city: String = "" 23 | var streetLine1: String = "" 24 | var streetLine2: String = "" 25 | var postCode: String = "" 26 | 27 | /// Coding keys to map values when Encoding and Decoding. 28 | enum CodingKeys: String, CodingKey { 29 | case countryCode = "country_code" 30 | case state 31 | case city 32 | case streetLine1 = "street_line_1" 33 | case streetLine2 = "street_line_2" 34 | case postCode = "post_code" 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Files/Cache+Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheError.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | // Errors related to update processing. Might merge the two? 11 | enum CacheError: String, Error { 12 | case BadPath = "Can't build URL from path that was provided." 13 | case BadBundle = "The cache path for Telegram could not be found. Please ensure Public/ is a folder in your project directory." 14 | case WrongType = "The file could not be added because it has the wrong type." 15 | case LocalNotFound = "The local resource you attempted to upload could not be found." 16 | case RemoteNotFound = "The remote resource you attempted to upload could not be found." 17 | case NoBytes = "The resource could not be obtained." 18 | case unableToMakeFoundationURL 19 | } 20 | 21 | enum CacheFormError: String, Error { 22 | case LinkNotFound = "The MessageFile provided had no URL or fileID to use." 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/ShippingQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShippingQuery.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Contains information about an incoming shipping query for the Telegram Payment API. 13 | */ 14 | public struct ShippingQuery: Codable, UpdateModel { 15 | 16 | /// Unique query identifier. 17 | var id: String = "" 18 | 19 | /// The user who sent this query. 20 | var from: User = User(id: "0", isBot: false, firstName: "fixme") 21 | 22 | /// The bot-specified invoice payload. 23 | var invoicePayload: String = "" 24 | 25 | /// The user-specified shipping address. 26 | var shippingAddress: ShippingAddress = ShippingAddress() 27 | 28 | /// Coding keys to map values when Encoding and Decoding. 29 | enum CodingKeys: String, CodingKey { 30 | case id 31 | case from 32 | case invoicePayload = "invoice_payload" 33 | case shippingAddress = "shipping_address" 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultGame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultGame.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a game. 12 | */ 13 | public struct InlineResultGame: InlineResult { 14 | 15 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 16 | public var metatype: InlineResultType = .game 17 | 18 | /// Type of the result being given. 19 | public var type: String = "game" 20 | 21 | /// Unique Identifier for the result, 1-64 bytes. 22 | public var id: String 23 | 24 | /// Inline keyboard attached to the message 25 | public var markup: MarkupInline? 26 | 27 | /// Short name of the game. 28 | public var name: String 29 | 30 | /// Coding keys to map values when Encoding and Decoding. 31 | enum CodingKeys: String, CodingKey { 32 | case type 33 | case id 34 | case markup = "reply_markup" 35 | case name 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/FileDownload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileDownload.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 22/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a file ready to be downloaded. Use the API method `getFile` to request a FileDownload type. 12 | */ 13 | public struct FileDownload: Codable { 14 | 15 | /// Unique identifier for the file. 16 | var fileID: String 17 | 18 | /// The file size, if known 19 | var fileSize: Int? 20 | 21 | /// The path that can be used to download the file, using https://api.telegram.org/file/bot/. 22 | var filePath: String? 23 | 24 | /// Coding keys to map values when Encoding and Decoding. 25 | enum CodingKeys: String, CodingKey { 26 | case fileID = "file_id" 27 | case fileSize = "file_size" 28 | case filePath = "file_path" 29 | } 30 | 31 | public init(fileID: String, fileSize: Int? = nil, filePath: String? = nil) { 32 | self.fileID = fileID 33 | self.fileSize = fileSize 34 | self.filePath = filePath 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineQuery.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents an incoming inline query. When the user sends an empty query, your bot could return some default or trending results. 13 | */ 14 | public struct InlineQuery: UpdateModel, Codable { 15 | 16 | // Unique identifier for this query. 17 | public var id: String 18 | 19 | // The sender. 20 | public var from: User 21 | 22 | // Text of the query (up to 512 characters). 23 | public var query: String 24 | 25 | // Offset of the results to be returned, is bot-controllable. 26 | public var offset: String 27 | 28 | // Sender location, only for bots that request it. 29 | public var location: Location? 30 | 31 | public init(id: String, user: User, query: String, offset: String, location: Location? = nil) { 32 | self.id = id 33 | self.from = user 34 | self.query = query 35 | self.offset = offset 36 | self.location = location 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent_Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageContact.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct InputMessageContent_Contact: InputMessageContent_Any { 11 | 12 | // The type of the input content, used for Codable. 13 | public static var type: InputMessageContentType = .contact 14 | 15 | // Contact's phone number. 16 | public var phoneNumber: String 17 | 18 | // Contact's first name. 19 | public var firstName: String 20 | 21 | // Contact's last name. 22 | public var lastName: String 23 | 24 | 25 | /// Coding keys to map values when Encoding and Decoding. 26 | enum CodingKeys: String, CodingKey { 27 | case phoneNumber = "phone_number" 28 | case firstName = "first_name" 29 | case lastName = "last_name" 30 | } 31 | 32 | public init(phoneNumber: String, firstName: String, lastName: String) { 33 | self.phoneNumber = phoneNumber 34 | self.firstName = firstName 35 | self.lastName = lastName 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a real-world location, that when sent as the contents of a message, is represented by a map preview. 14 | */ 15 | public struct Location: TelegramType, MessageContent { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "location" 19 | public var method: String = "sendLocation" 20 | 21 | // PARAMETERS 22 | /// The latitude of the location. 23 | public var latitude: Float 24 | /// The longitude of the location. 25 | public var longitude: Float 26 | 27 | 28 | public init(latitude: Float, longitude: Float) { 29 | self.latitude = latitude 30 | self.longitude = longitude 31 | } 32 | 33 | // SendType conforming methods to send itself to Telegram under the provided method. 34 | public func getQuery() -> [String: Codable] { 35 | let keys: [String: Codable] = [ 36 | "longitude": longitude, 37 | "latitude": latitude] 38 | 39 | return keys 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/ChosenInlineResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChosenInlineResult.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents a result of an inline query that was chosen by the user and sent to the chat. 13 | */ 14 | public struct ChosenInlineResult: UpdateModel, Codable { 15 | 16 | /// The unique identifier for the result that was chosen. 17 | var resultID: String 18 | 19 | /// The user that chose the result. 20 | var from: User 21 | 22 | /// The query that was used to obtain the result 23 | var query: String 24 | 25 | /// Sender location, only for bots that require user location. 26 | var location: Location? 27 | 28 | /// Identifier of the sent inline message. Available only if there is an inline keyboard attached to the message. 29 | var inlineMessageID: String? 30 | 31 | /// Coding keys to map values when Encoding and Decoding. 32 | enum CodingKeys: String, CodingKey { 33 | case resultID = "inline_query_id" 34 | case from 35 | case query 36 | case location 37 | case inlineMessageID = "inline_message_id" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Files/CacheFile.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | /** 5 | Represents a file that has been uploaded and is stored in the cache for. 6 | Used for obtaining and re-using the resource, as well as 7 | */ 8 | struct CacheFile: Equatable { 9 | 10 | /// The message file being stored 11 | var file: MessageFile 12 | 13 | /// The time it was last uploaded (useful for predicting when it needs to be uploaded again). 14 | var uploadTime: Date 15 | 16 | /** 17 | Attempts to create the type for the given file. Warning: This will fail if the file has no file ID, thus 18 | indicating it has never been uploaded to Telegram. A file must already be uploaded to be cached. 19 | */ 20 | init?(file: MessageFile) { 21 | 22 | if file.fileID == nil { return nil } 23 | 24 | self.file = file 25 | self.uploadTime = Date() 26 | } 27 | 28 | public static func ==(lhs: CacheFile, rhs: CacheFile) -> Bool { 29 | 30 | if lhs.uploadTime != rhs.uploadTime { return false } 31 | if lhs.file.fileID != rhs.file.fileID { return false } 32 | if lhs.file.url != rhs.file.url { return false } 33 | 34 | return true 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Stickers/StickerSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StickerSet.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a sticker set - a collection of stickers that are stored on the Telegram servers. 12 | */ 13 | public class StickerSet: Codable { 14 | 15 | /// The username for the sticker set (?) 16 | public var username: String 17 | 18 | /// The name of the sticker set. 19 | public var title: String 20 | 21 | /// If true, this set contains sticker masks. 22 | public var containsMasks: Bool 23 | 24 | /// An array of all the stickers that this set contains. 25 | public var stickers: [Sticker] 26 | 27 | /// Coding keys to map values when Encoding and Decoding. 28 | enum CodingKeys: String, CodingKey { 29 | case username = "name" 30 | case title 31 | case containsMasks = "contains_masks" 32 | case stickers 33 | 34 | } 35 | 36 | init(withUsername username: String, title: String, containsMasks: Bool, stickers: [Sticker]) { 37 | self.username = username 38 | self.title = title 39 | self.containsMasks = containsMasks 40 | self.stickers = stickers 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Albums/InputMediaVideo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMediaVideo.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a video to be sent in the context of an album. 12 | */ 13 | struct InputMediaVideo: InputMedia { 14 | 15 | // PROTOCOL INHERITANCE 16 | public var type = "video" 17 | public var media: String 18 | public var caption: String? 19 | 20 | // DETAILS 21 | /// The width of the video in pixels 22 | public var width: Int? 23 | 24 | /// The height of the video in pixels. 25 | public var height: Int? 26 | 27 | /// The duration of the video in seconds. 28 | public var duration: Int? 29 | 30 | /// Coding keys to map values when Encoding and Decoding. 31 | enum CodingKeys: String, CodingKey { 32 | case type 33 | case media 34 | case caption 35 | 36 | case width 37 | case height 38 | case duration 39 | } 40 | 41 | public init(mediaLink media: String, 42 | caption: String?, 43 | width: Int? = nil, 44 | height: Int? = nil, 45 | duration: Int? = nil) { 46 | 47 | self.media = media 48 | self.caption = caption 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineResultType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 22/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum InlineResultType: String, Codable { 11 | case article, audio, contact, document, game, gif, location 12 | case mpeg4Gif = "mpeg4_gif" 13 | case photo, sticker, venue, video, voice 14 | 15 | var metatype: InlineResult.Type { 16 | switch self { 17 | case .article: 18 | return InlineResultArticle.self 19 | case .audio: 20 | return InlineResultAudio.self 21 | case .contact: 22 | return InlineResultContact.self 23 | case .document: 24 | return InlineResultDocument.self 25 | case .game: 26 | return InlineResultGame.self 27 | case .gif: 28 | return InlineResultGIF.self 29 | case .location: 30 | return InlineResultLocation.self 31 | case .mpeg4Gif: 32 | return InlineResultMpeg4GIF.self 33 | case .photo: 34 | return InlineResultPhoto.self 35 | case .sticker: 36 | return InlineResultSticker.self 37 | case .venue: 38 | return InlineResultVenue.self 39 | case .video: 40 | return InlineResultVideo.self 41 | case .voice: 42 | return InlineResultVoice.self 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent_Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageLocation.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents the content of a location message to be sent as the result of an inline query. 12 | */ 13 | public struct InputMessageContent_Location: InputMessageContent_Any { 14 | 15 | // The type of the input content, used for Codable. 16 | public static var type: InputMessageContentType = .location 17 | 18 | /// Latitude of the venue in degrees. 19 | public var latitude: Float 20 | 21 | /// Longitude of the venue in degrees. 22 | public var longitude: Float 23 | 24 | /// Period in seconds for which the location can be updated, should be between 60 and 86400 seconds. (Used for Live Locations). 25 | public var livePeriod: Int? 26 | 27 | /// Coding keys to map values when Encoding and Decoding. 28 | enum CodingKeys: String, CodingKey { 29 | case latitude 30 | case longitude 31 | case livePeriod = "live_period" 32 | } 33 | 34 | public init(latitude: Float, longitude: Float, livePeriod: Int?) { 35 | self.latitude = latitude 36 | self.longitude = longitude 37 | self.livePeriod = livePeriod 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message/MessageType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines a message type, and in most cases also contains the contents of that message. 12 | */ 13 | public enum MessageType { 14 | 15 | case audio(Audio) 16 | case contact(Contact) 17 | case document(Document) 18 | case game(Game) 19 | case photo([Photo]) 20 | case location(Location) 21 | case sticker(Sticker) 22 | case venue(Venue) 23 | case video(Video) 24 | case videoNote(VideoNote) 25 | case voice(Voice) 26 | case text 27 | 28 | /// Returns the name of the type as a string. 29 | func name() -> String { 30 | switch self { 31 | case .audio(_): 32 | return "audio" 33 | case .contact(_): 34 | return "contact" 35 | case .document(_): 36 | return "document" 37 | case .game(_): 38 | return "game" 39 | case .photo(_): 40 | return "photo" 41 | case .location(_): 42 | return "location" 43 | case .sticker(_): 44 | return "sticker" 45 | case .venue(_): 46 | return "venue" 47 | case .video(_): 48 | return "video" 49 | case .videoNote(_): 50 | return "video_note" 51 | case .voice(_): 52 | return "voice" 53 | case .text: 54 | return "text" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Log/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Internally handles debug switches and prints for Pelican. Provides a clean interface that categorises and sorts prints 13 | while also ensuring the logs don't get included in any release builds. 14 | 15 | If you want to see any logs before the Droplet is initialised, add "-DPELICAN_DEBUG" as a custom Swift compile flag. 16 | */ 17 | public class PLog { 18 | 19 | /// Use this to set entities that will be logged during testing. 20 | public static var displayLogTypes: [LogLevel] = [] 21 | 22 | /// A callback to the console logger associated with the first droplet run. 23 | // static var console: LogProtocol? 24 | 25 | init() {} 26 | 27 | 28 | // Disabled until controls can be added to prevent unwanted levels being shown. 29 | /** 30 | Attempts to print the text based on what kind of debug it's associated with. 31 | Privately used from the convenience methods laid out in Debug+Prints. 32 | */ 33 | static internal func addPrint(level: LogLevel, text: String, file: String, function: String, line: Int) { 34 | 35 | if displayLogTypes.contains(level) == true { 36 | print("[\(function) : \(line)]: \(text)") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Event/SessionEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionEvent.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 06/07/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Defines an event that has occurred within a Session that Pelican needs to know about, based on an 13 | automatic trigger called by one of it's delegates or something else. 14 | */ 15 | public class SessionEvent { 16 | 17 | /// The tag that belongs to the session responsible for the event. 18 | var tag: SessionTag 19 | 20 | /// The type of event that has occurred. 21 | var type: SessionEventType 22 | 23 | /// The action that should take place as a result of the event taking place. 24 | var action: SessionEventAction 25 | 26 | init(tag: SessionTag, type: SessionEventType, action: SessionEventAction) { 27 | self.tag = tag 28 | self.type = type 29 | self.action = action 30 | } 31 | } 32 | 33 | /** 34 | Defines an event that can occur to a Session which can be signalled to Pelican. 35 | */ 36 | public enum SessionEventType: String { 37 | case timeout 38 | case flood 39 | case blacklist 40 | case other 41 | } 42 | 43 | /** 44 | Defines an action to take when a SessionEvent is sent as a result of a `SessionEventType`. 45 | */ 46 | public enum SessionEventAction: String { 47 | case remove 48 | case blacklist 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent_Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageText.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents the content of a text message to be sent as the result of an inline query. 12 | */ 13 | public struct InputMessageContent_Text: InputMessageContent_Any { 14 | 15 | // The type of the input content, used for Codable. 16 | public static var type: InputMessageContentType = .text 17 | 18 | /// Text of the message to be sent. 1-4096 characters. 19 | public var text: String 20 | 21 | /// Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. 22 | public var parseMode: String? 23 | 24 | /// Disables link previews for links in the sent message. 25 | public var disableWebPreview: Bool? 26 | 27 | public init(text: String, parseMode: String?, disableWebPreview: Bool?) { 28 | self.text = text 29 | self.parseMode = parseMode 30 | self.disableWebPreview = disableWebPreview 31 | } 32 | 33 | /// Coding keys to map values when Encoding and Decoding. 34 | enum CodingKeys: String, CodingKey { 35 | case text = "message_text" 36 | case parseMode = "parse_mode" 37 | case disableWebPreview = "disable_web_page_preview" 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Request/Codable+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable+String.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 16/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encodable { 11 | 12 | /** 13 | A convenience method to convert any codable type into a JSON format, or a single descriptive 14 | string of the type if it contains only one variable. 15 | */ 16 | func encodeToUTF8() throws -> String? { 17 | 18 | var jsonData = Data() 19 | 20 | do { 21 | jsonData = try JSONEncoder().encode(self) 22 | 23 | } catch { 24 | if self is String { 25 | let result = self as! String 26 | return result 27 | } 28 | 29 | else if self is Int { 30 | let result = self as! Int 31 | return result.description 32 | } 33 | 34 | else if self is Double { 35 | let result = self as! Double 36 | return result.description 37 | } 38 | 39 | else if self is Decimal { 40 | let result = self as! Decimal 41 | return result.description 42 | } 43 | 44 | else if self is Bool { 45 | let result = self as! Bool 46 | if result == true { return "true" } 47 | else { return "false" } 48 | } 49 | 50 | 51 | PLog.error("Serialisation Error - \(error)") 52 | return nil 53 | } 54 | 55 | return String(data: jsonData, encoding: .utf8) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineResultAny.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Defines a wrapper for the InlineResult protocol, that allows ambiguous InlineResult types to be encoded and decoded. 13 | */ 14 | public struct InlineResultAny: Codable { 15 | 16 | public var base: InlineResult 17 | public var type: InlineResultType 18 | 19 | // These values are only ever used to infer what content type is being held. 20 | enum CodingKeys: String, CodingKey { 21 | case type 22 | } 23 | 24 | init(_ base: InlineResult) { 25 | self.base = base 26 | self.type = base.metatype 27 | } 28 | 29 | public init(from decoder: Decoder) throws { 30 | let container = try decoder.container(keyedBy: CodingKeys.self) 31 | 32 | /// Create a new result type from the convenient "type" property. 33 | if let type = InlineResultType(rawValue: try container.decode(String.self, forKey: .type)) { 34 | 35 | /// Use that metatype to initialise the base. 36 | self.type = type 37 | self.base = try type.metatype.init(from: decoder) 38 | return 39 | 40 | } else { 41 | throw PError_Codable.InlineResultAnyDecodable 42 | } 43 | } 44 | 45 | public func encode(to encoder: Encoder) throws { 46 | try base.encode(to: encoder) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/Invoice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Invoice.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Contains basic information about an invoice for the Telegram Payment API. 14 | */ 15 | public struct Invoice: Codable { 16 | 17 | /// Product name. 18 | var title: String = "" 19 | 20 | /// Product description. 21 | var description: String = "" 22 | 23 | /// A unique bot deep-linking parameter that can be used to generate this invoice. 24 | var startParameter: String = "" 25 | 26 | /// Three-letter [ISO 4217 currency code](https://core.telegram.org/bots/payments#supported-currencies). 27 | var currency: String = "" 28 | 29 | /** 30 | Total price in the smallest units of the currency. For example, for a price of US$ 1.45 pass amount = 145. 31 | 32 | See the exp parameter in [currencies.json](https://core.telegram.org/bots/payments/currencies.json) for more information, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). 33 | */ 34 | var totalAmount: Int = 0 35 | 36 | /// Coding keys to map values when Encoding and Decoding. 37 | enum CodingKeys: String, CodingKey { 38 | case title 39 | case description 40 | case startParameter = "start_parameter" 41 | case currency 42 | case totalAmount = "total_amount" 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Pelican+Delegates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pelican+Delegates.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 24/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PelicanBot { 11 | /** 12 | Handles a specific event sent from a Session. 13 | */ 14 | func sendEvent(_ event: SessionEvent) { 15 | 16 | switch event.action { 17 | 18 | // In this event, the session just needs removing without any other tasks. 19 | case .remove: 20 | sessionManager.deleteSession(tag: event.tag) 21 | 22 | 23 | // In a blacklist event, first make sure the Session ID type matches. If not, return. 24 | case .blacklist: 25 | 26 | switch event.tag.idType { 27 | 28 | case .chat: 29 | mod.addToBlacklist(chatIDs: event.tag.id) 30 | case .user: 31 | mod.addToBlacklist(userIDs: event.tag.id) 32 | default: 33 | return 34 | } 35 | sessionManager.deleteSession(tag: event.tag) 36 | 37 | } 38 | } 39 | 40 | /** 41 | Submits work for a Session's DispatchQueue to handle. 42 | */ 43 | func requestSessionWork(tag: SessionTag, work: @escaping () -> ()) { 44 | sessionManager.submitSessionWork(withTag: tag, work: work) 45 | } 46 | 47 | /** 48 | Allows a Session to create other sessions. 49 | */ 50 | func createSession(tag: SessionTag, telegramID: String) -> Session? { 51 | return sessionManager.createNewSession(tag: tag, bot: self, telegramID: telegramID) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent_Venue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageVenue.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents the content of a venue message to be sent as the result of an inline query. 12 | */ 13 | public struct InputMessageContent_Venue: InputMessageContent_Any { 14 | 15 | // The type of the input content, used for Codable. 16 | public static var type: InputMessageContentType = .venue 17 | 18 | /// Latitude of the venue in degrees. 19 | public var latitude: Float 20 | 21 | /// Longitude of the venue in degrees. 22 | public var longitude: Float 23 | 24 | /// Name of the venue. 25 | public var title: String 26 | 27 | /// Address of the venue. 28 | public var address: String 29 | 30 | /// Foursquare identifier of the venue, if known. 31 | public var foursquareID: String? 32 | 33 | 34 | /// Coding keys to map values when Encoding and Decoding. 35 | enum CodingKeys: String, CodingKey { 36 | case latitude 37 | case longitude 38 | case title 39 | case address 40 | case foursquareID = "foursquare_id" 41 | } 42 | 43 | public init(latitude: Float, longitude: Float, title: String, address: String, foursquareID: String?) { 44 | self.latitude = latitude 45 | self.longitude = longitude 46 | self.title = title 47 | self.address = address 48 | self.foursquareID = foursquareID 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Tests/PelicanTests/SetupTest.swift: -------------------------------------------------------------------------------- 1 | 2 | /// Import Vapor and get a droplet 3 | import Vapor 4 | import Pelican 5 | import Foundation 6 | import XCTest 7 | 8 | 9 | class BotSetup: TestCase { 10 | 11 | func testPelicanSetup() throws { 12 | 13 | class TestBot: ChatSession { 14 | 15 | // Do your setup here, this is when the session gets created. 16 | override func postInit() { 17 | 18 | super.postInit() 19 | 20 | let start = RouteCommand(commands: "start") { update in 21 | if update.content == "" || update.from == nil { return false } 22 | 23 | _ = self.send.message("Hi there \(update.from!.firstName)!", markup: nil) 24 | 25 | return true 26 | } 27 | 28 | routes.add(start) 29 | } 30 | } 31 | 32 | // Make sure you set up Pelican manually so you can assign it variables. 33 | let config = try! Config() 34 | let pelican = try! Pelican(config: config) 35 | 36 | // Add Builder 37 | pelican.addBuilder(SessionBuilder(spawner: Spawn.perChatID(updateType: [.message], chatType: [.private]), idType: .chat, session: TestBot.self, setup: nil) ) 38 | 39 | pelican.setPoll(interval: 1) 40 | 41 | // This defines what message types your bot can receive. 42 | pelican.allowedUpdates = [.message, .callbackQuery, .inlineQuery, .chosenInlineResult] 43 | pelican.timeout = 0 44 | 45 | // START IT UP! 46 | try config.addProvider(pelican) 47 | let drop = try Droplet(config) 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Game/Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | This object represents an animation file to be displayed in a Telegram message containing a game, used for additional visual flair and to better preview what your game is to 14 | */ 15 | public struct Animation: Codable { 16 | 17 | /// A unique file identifier for the animation. 18 | public var fileID: String 19 | 20 | /// Animation thumbnail as defined by the sender. 21 | public var thumb: Photo? 22 | 23 | /// Original animation filename as defined by the sender. 24 | public var fileName: String? 25 | 26 | /// MIME type of the file as defined by sender. 27 | public var mimeType: String? 28 | 29 | /// File size. 30 | public var fileSize: Int? 31 | 32 | 33 | /// Coding keys to map values when Encoding and Decoding. 34 | enum CodingKeys: String, CodingKey { 35 | case fileID = "file_id" 36 | case thumb 37 | case fileName = "file_name" 38 | case mimeType = "mime_type" 39 | case fileSize = "file_size" 40 | } 41 | 42 | 43 | public init(fileID: String, 44 | thumb: Photo? = nil, 45 | fileName: String? = nil, 46 | mimeType: String? = nil, 47 | fileSize: Int? = nil) { 48 | 49 | self.fileID = fileID 50 | self.thumb = thumb 51 | self.fileName = fileName 52 | self.mimeType = mimeType 53 | self.fileSize = fileSize 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Markup/Inline/InlineKeyType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineKeyType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines what type of function a InlineButtonKey has. 12 | */ 13 | public enum InlineKeyType: String { 14 | 15 | /// HTTP url to be opened by the client when button is pressed. 16 | case url 17 | 18 | /// Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes 19 | case callbackData 20 | 21 | /// Prompts the user to select one of their chats, open it and insert the bot‘s username and the specified query in the input field. 22 | case switchInlineQuery 23 | 24 | /// If set, pressing the button will insert the bot‘s username and the specified inline query in the current chat's input field. Can be empty. 25 | case switchInlineQuery_currentChat 26 | 27 | /** 28 | Description of the game that will be launched when the user presses the button. 29 | - note: This type of button must always be the first button in the first row. 30 | */ 31 | case callbackGame 32 | 33 | /** 34 | Specify True, to send a Pay button. 35 | - note: This type of button must always be the first button in the first row. 36 | */ 37 | case payButton 38 | 39 | /** 40 | If this type is defined, Pelican was unable to infer what type the received InlineKey was, either due to a bug, a bad response or a change in the Telegram API that Pelican doesn't yet support. 41 | */ 42 | case unknown 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Pelican+Files.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pinched from Vapor's Core framework <3 3 | // 4 | 5 | import Foundation 6 | 7 | extension PelicanBot { 8 | 9 | /** 10 | This function will attempt to get the current working directory of the application. 11 | */ 12 | static public func workingDirectory() -> String { 13 | let fileBasedWorkDir: String? 14 | 15 | #if Xcode 16 | // attempt to find working directory through #file 17 | let file = #file 18 | 19 | if file.contains(".build") { 20 | // most dependencies are in `./.build/` 21 | fileBasedWorkDir = file.components(separatedBy: "/.build").first 22 | } else if file.contains("Packages") { 23 | // when editing a dependency, it is in `./Packages/` 24 | fileBasedWorkDir = file.components(separatedBy: "/Packages").first 25 | } else { 26 | // when dealing with current repository, file is in `./Sources/` 27 | fileBasedWorkDir = file.components(separatedBy: "/Sources").first 28 | } 29 | #else 30 | fileBasedWorkDir = nil 31 | #endif 32 | 33 | let workDir: String 34 | if let fileBasedWorkDir = fileBasedWorkDir { 35 | workDir = fileBasedWorkDir 36 | } else { 37 | // get actual working directory 38 | let cwd = getcwd(nil, Int(PATH_MAX)) 39 | defer { 40 | free(cwd) 41 | } 42 | 43 | if let cwd = cwd, let string = String(validatingUTF8: cwd) { 44 | workDir = string 45 | } else { 46 | workDir = "./" 47 | } 48 | } 49 | 50 | return workDir + "/" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Sync/MethodRequestSync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequestync.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 18/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A delegate for creating and sending TelegramRequest types in a synchronous manner, 12 | where your code execution will wait until a response from Telegram is received. 13 | 14 | Useful if an event requires a result from Telegram like receiving the result of a sent message, if your code 15 | needs to do something with it immediately afterwards. 16 | 17 | - note: If you only need to make a request to Telegram and can either deal with the result later or don't need to know the results, 18 | use the `async` property that can be found in `MethodRequest`, as it will improve the responsiveness and performance of 19 | your app. 20 | */ 21 | public struct MethodRequestSync { 22 | 23 | /// The tag of the session that this request instance belongs to. 24 | var tag: SessionTag 25 | 26 | public init(tag: SessionTag) { 27 | self.tag = tag 28 | } 29 | 30 | /** 31 | Allows you to make a custom request to Telegram, using a method name and set of arguments as a dictionary. 32 | 33 | Use this if Pelican hasn't yet implemented a new API method, but also submit an issue [right about here](https://github.com/Takanu/Pelican) 34 | here so I can add it 👌👍. 35 | */ 36 | func customRequest(methodName: String, queries: [String: Codable]) -> TelegramResponse? { 37 | 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pelican 0.8 2 | ## Telegram API Wrapper for Swift. 3 | 4 | Hi, 2-3 people that somehow both like Telegram and Swift! This package gives you a complete set of tools for making bots. 5 | **It's still a little rough and not everything has been finished, so be careful** 6 | 7 | If you’ve never heard of [Telegram](https://telegram.org) it’s an awesome non-profit, secure messaging service that offers a lot of great features, one of them being a free Bot API where you can make bots for all kinds of tasks, from helping schedule channel posts to operating your own online HTML5 games that are playable inside the app. 8 | 9 | Pelican is a framework I've developed personally for my own Telegram games and has a lot of great features: 10 | 11 | - Pelican automates the handling and distribution of updates through a unique session system that you can customise for your own specific needs - no need to make your own. 12 | - It's multi-threaded by default, with thread-safe session update handling and quick APIs for delaying code and Telegram API calls. 13 | - Features modular, tree-based routing systems for more advanced and interchangeable update sorting. 14 | 15 | The code has extensive API documentation to help you use it, and I will include up-to-date tests and demo code soon. If you have any questions, feel free to message me on GitHub or on Telegram [@takanu](https://t.me/takanu). 16 | 17 | 18 | If you're interested in creating games with Pelican, I've also made a great game framework called [TrashBoat](https://github.com/Takanu/TrashBoat). 19 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Stickers/MaskPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskPosition.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Defines a point and set of coordinate and scaling parameters to determine where and how a sticker should be placed when added to an image. 12 | */ 13 | public class MaskPosition: Codable { 14 | 15 | /// The part of the face relative to which the mask should be placed. 16 | public var point: MaskPositionPoint 17 | 18 | /// The offset by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing -1.0 will place mask just to the left of the default mask position. 19 | public var offsetX: Float 20 | 21 | /// The offset by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. For example, 1.0 will place the mask just below the default mask position. 22 | public var offsetY: Float 23 | 24 | /// How much the mask will be scaled by when used. Eg. 2.0 will double the scale of the mask. 25 | public var maskScale: Float 26 | 27 | 28 | /// Coding keys to map values when Encoding and Decoding. 29 | enum CodingKeys: String, CodingKey { 30 | case point 31 | case offsetX = "x_shift" 32 | case offsetY = "y_shift" 33 | case maskScale = "scale" 34 | } 35 | 36 | public init(point: MaskPositionPoint, offsetX: Float, offsetY: Float, maskScale: Float) { 37 | self.point = point 38 | self.offsetX = offsetX 39 | self.offsetY = offsetY 40 | self.maskScale = maskScale 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultSticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultSticker.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents a link to a sticker stored on the Telegram servers. 13 | 14 | By default, this sticker will be sent by the user. Alternatively, you can use the 'content' property to send a message with the specified content instead of the sticker. 15 | 16 | Stickers can only ever be cached, you cannot currently define an external URL link to a sticker as an inline result. 17 | */ 18 | public struct InlineResultSticker: InlineResult { 19 | 20 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 21 | public var metatype: InlineResultType = .sticker 22 | 23 | /// Type of the result being given. 24 | public var type: String = "sticker" 25 | 26 | /// Unique Identifier for the result, 1-64 bytes. 27 | public var id: String 28 | 29 | /// Content of the message to be sent. 30 | public var content: InputMessageContent? 31 | 32 | /// Inline keyboard attached to the message 33 | public var markup: MarkupInline? 34 | 35 | 36 | 37 | /// A valid file identifier for the sticker. 38 | public var fileID: String? 39 | 40 | /// Coding keys to map values when Encoding and Decoding. 41 | enum CodingKeys: String, CodingKey { 42 | case type 43 | case id 44 | case content = "input_message_content" 45 | case markup = "reply_markup" 46 | 47 | case fileID = "sticker_file_id" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contact.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | public struct Contact: TelegramType, MessageContent { 13 | 14 | // STORAGE AND IDENTIFIERS 15 | public var contentType: String = "contact" 16 | public var method: String = "sendContact" 17 | 18 | // PARAMETERS 19 | /// The contact's phone number. 20 | public var phoneNumber: String 21 | 22 | /// The contact's first name. 23 | public var firstName: String 24 | 25 | /// The contact's last name 26 | public var lastName: String? 27 | 28 | /// The contact's user ID 29 | public var userID: Int? 30 | 31 | /// Coding keys to map values when Encoding and Decoding. 32 | enum CodingKeys: String, CodingKey { 33 | case phoneNumber = "phone_number" 34 | case firstName = "first_name" 35 | case lastName = "last_name" 36 | case userID = "user_id" 37 | 38 | } 39 | 40 | public init(phoneNumber: String, 41 | firstName: String, 42 | lastName: String? = nil, 43 | userID: Int? = nil) { 44 | 45 | self.phoneNumber = phoneNumber 46 | self.firstName = firstName 47 | self.lastName = lastName 48 | self.userID = userID 49 | } 50 | 51 | 52 | // SendType conforming methods to send itself to Telegram under the provided method. 53 | public func getQuery() -> [String: Codable] { 54 | var keys: [String: Codable] = [ 55 | "phone_number": phoneNumber, 56 | "first_name": firstName 57 | ] 58 | 59 | if lastName != nil { keys["last_name"] = lastName } 60 | 61 | return keys 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Async/MethodRequestAsync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequestAsync.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 18/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A delegate for creating and sending TelegramRequest types in a synchronous manner, 12 | where your code execution will continue immediately after the request is made and sent. 13 | 14 | Use this if you don't need to handle the response immediately after making the request or don't need to know 15 | the result of a request. 16 | */ 17 | public struct MethodRequestAsync { 18 | 19 | public typealias CallbackBoolean = ((Bool) -> ())? 20 | public typealias CallbackString = ((String?) -> ())? 21 | 22 | /// The tag of the session that this request instance belongs to. 23 | var tag: SessionTag 24 | 25 | public init(tag: SessionTag) { 26 | self.tag = tag 27 | } 28 | 29 | /** 30 | Allows you to make a custom request to Telegram, using a method name and set of arguments as a dictionary. 31 | 32 | Use this if Pelican hasn't yet implemented a new API method, but also submit an issue [right about here](https://github.com/Takanu/Pelican) 33 | here so I can add it 👌👍. 34 | */ 35 | func customRequest(methodName: String, queries: [String: Codable], file: MessageFile?, callback: ((TelegramResponse?) -> ())? ) { 36 | 37 | let request = TelegramRequest() 38 | request.method = methodName 39 | request.query = queries 40 | request.file = file 41 | 42 | tag.sendAsyncRequest(request) { response in 43 | if callback != nil { 44 | callback!(response) 45 | } 46 | } 47 | 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Venue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Venue.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a different type of location, for venue-like venuing. 14 | */ 15 | public struct Venue: TelegramType, MessageContent { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "venue" 19 | public var method: String = "sendVenue" 20 | 21 | // PARAMETERS 22 | /// The location of the venue. 23 | public var location: Location 24 | 25 | /// Location title. 26 | public var title: String 27 | 28 | /// Address of the venue. 29 | public var address: String 30 | 31 | /// Foursquare identifier of the venue if known. 32 | public var foursquareID: String? 33 | 34 | 35 | /// Coding keys to map values when Encoding and Decoding. 36 | enum CodingKeys: String, CodingKey { 37 | case location 38 | case title 39 | case address 40 | case foursquareID = "foursquare_id" 41 | } 42 | 43 | public init(location: Location, title: String, address: String, foursquareID: String? = nil) { 44 | self.location = location 45 | self.title = title 46 | self.address = address 47 | } 48 | 49 | // SendType conforming methods to send itself to Telegram under the provided method. 50 | public func getQuery() -> [String: Codable] { 51 | var keys: [String: Codable] = [ 52 | "latitude": location.latitude, 53 | "longitude": location.longitude, 54 | "title": title, 55 | "address": address 56 | ] 57 | 58 | if foursquareID != nil { keys["foursquare_id"] = foursquareID } 59 | return keys 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Update/UpdateType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateType.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Categorises the types of requests that can be made by a user to the bot. 13 | */ 14 | public enum UpdateType: String, CasedEnum { 15 | 16 | /// This defines the update as an incoming message of any kind. 17 | case message = "Message" 18 | 19 | /// This defines the update as a new version of a message that is known to the bot and was edited. 20 | case editedMessage = "Edited Message" 21 | 22 | /// This defines the update as an incoming channel post of any kind. 23 | case channelPost = "Channel Post" 24 | 25 | /// This defines the update as a new version of a channel post that is known to the bot and was edited. 26 | case editedChannelPost = "Edited Channel Post" 27 | 28 | /// This defines the update as a new incoming callback query. 29 | case callbackQuery = "Callback Query" 30 | 31 | /// This defines the update as a new incoming inline query. 32 | case inlineQuery = "Inline Query" 33 | 34 | /// This defines the update as the result of an inline query that was chosen by a user and sent to their chat partner. 35 | case chosenInlineResult = "Chosen Inline Result" 36 | 37 | /// This defines the update as a new incoming shipping query. 38 | case shippingQuery = "Shipping Query" 39 | 40 | /// This defines the update as a new incoming pre-checkout query. Contains full information from the checkout. 41 | case preCheckoutQuery = "Pre-Checkout Query" 42 | 43 | public func string() -> String { 44 | return rawValue 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Game/Game.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Game.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | This object represents a game. Use BotFather to create and edit games, their short names will act as unique identifiers. 14 | */ 15 | public struct Game: Codable { 16 | 17 | /// Title of the game. 18 | public var title: String 19 | 20 | /// Description of the game. 21 | public var description: String 22 | 23 | /// Photo that will be displayed in the game message in chats. 24 | public var photo: [Photo] 25 | 26 | /// Brief description of the game as well as provide space for high scores. 27 | public var text: String? 28 | 29 | /// Special entities that appear in text, such as usernames. 30 | public var textEntities: [MessageEntity]? 31 | 32 | /// Animation type that will be displayed in the game message in chats. Upload via BotFather 33 | public var animation: Animation? 34 | 35 | 36 | /// Coding keys to map values when Encoding and Decoding. 37 | enum CodingKeys: String, CodingKey { 38 | case title 39 | case description 40 | case photo 41 | case text 42 | case textEntities = "text_entities" 43 | case animation 44 | } 45 | 46 | 47 | public init(title: String, 48 | description: String, 49 | photo: [Photo], 50 | text: String? = nil, 51 | textEntities: [MessageEntity]? = nil, 52 | animation: Animation? = nil) { 53 | 54 | self.title = title 55 | self.description = description 56 | self.photo = photo 57 | self.text = text 58 | self.textEntities = textEntities 59 | self.animation = animation 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Markup/ForceReply.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardForceReply.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a special action that when sent with a message, will force Telegram clients to display 12 | a reply interface to all or a selected group of people in the chat. 13 | */ 14 | public struct MarkupForceReply: MarkupType, Codable { 15 | 16 | /// Shows reply interface to the user, as if they manually selected the bot‘s message and tapped ’Reply' 17 | public var forceReply: Bool = true 18 | 19 | /// (Optional) Use this parameter if you want to force reply from specific users only. 20 | public var selective: Bool = false 21 | 22 | /** 23 | Creates a `MarkupForceReply` instance, to force Telegram clients to display 24 | a reply interface to all or a selected group of people in the chat. 25 | 26 | If isSelective is true, the reply interface will only be displayed to targets of the message it is being sent with. 27 | 28 | **Targets:** 29 | 1) users that are @mentioned in the text of the Message object; 30 | 2) if the bot's message is a reply (has reply_to_message_id), sender of the original message. 31 | */ 32 | 33 | /// Coding keys to map values when Encoding and Decoding. 34 | enum CodingKeys: String, CodingKey { 35 | case forceReply = "force_reply" 36 | case selective 37 | } 38 | 39 | /* 40 | - parameter isSelective: If false, the reply interface will appear for all users. If true however, the 41 | reply interface will only appear for the targets you specify. 42 | */ 43 | public init(isSelective sel: Bool) { 44 | selective = sel 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/PreCheckoutQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Contains information about an incoming pre-checked query for the Telegram Payment API. 13 | */ 14 | public struct PreCheckoutQuery: Codable, UpdateModel { 15 | 16 | /// A unique query identifier. 17 | var id: String = "" 18 | 19 | /// The user who sent the query. 20 | var from: User = User(id: "0", isBot: false, firstName: "fixme") 21 | 22 | /// Three-letter [ISO 4217 currency code](https://core.telegram.org/bots/payments#supported-currencies). 23 | var currency: String = "" 24 | 25 | /** 26 | Total price in the smallest units of the currency. For example, for a price of US$ 1.45 pass amount = 145. 27 | 28 | See the exp parameter in [currencies.json](https://core.telegram.org/bots/payments/currencies.json) for more information, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). 29 | */ 30 | var totalAmount: Int = 0 31 | 32 | /// The bot-specified invoice payload. 33 | var invoicePayload: String = "" 34 | 35 | /// The identifier of the shipping option chosen by the user. 36 | var shippingOptionID: String? 37 | 38 | /// Order information provided by the user. 39 | var orderInfo: OrderInfo? 40 | 41 | /// Coding keys to map values when Encoding and Decoding. 42 | enum CodingKeys: String, CodingKey { 43 | case id 44 | case from 45 | case currency 46 | case totalAmount = "total_amount" 47 | case invoicePayload = "invoice_payload" 48 | case shippingOptionID = "shipping_option_id" 49 | case orderInfo = "order_info" 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Client/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // Pelican 4 | // 5 | // Taken from Vapor's HTTP Module (https://github.com/vapor/engine) 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Result { 11 | case success(T) 12 | case failure(Error) 13 | 14 | public init(value: T) { 15 | self = .success(value) 16 | } 17 | 18 | public init(error: Error) { 19 | self = .failure(error) 20 | } 21 | } 22 | 23 | extension Result { 24 | public func extract() throws -> T { 25 | switch self { 26 | case .success(let val): 27 | return val 28 | case .failure(let e): 29 | throw e 30 | } 31 | } 32 | } 33 | 34 | extension Result { 35 | public var value: T? { 36 | guard case let .success(val) = self else { return nil } 37 | return val 38 | } 39 | 40 | public var error: Error? { 41 | guard case let .failure(err) = self else { return nil } 42 | return err 43 | } 44 | } 45 | 46 | extension Result { 47 | public var succeeded: Bool { 48 | return value != nil 49 | } 50 | 51 | public var isSuccess: Bool { 52 | switch self { 53 | case .success: 54 | return true 55 | default: 56 | return false 57 | } 58 | } 59 | 60 | public var isFailure: Bool { 61 | switch self { 62 | case .failure: 63 | return true 64 | default: 65 | return false 66 | } 67 | } 68 | } 69 | 70 | extension Result: CustomStringConvertible { 71 | public var description: String { 72 | switch self { 73 | case let .success(value): return ".success(\(value))" 74 | case let .failure(error): return ".failure(\(error))" 75 | } 76 | } 77 | } 78 | 79 | extension Result: CustomDebugStringConvertible { 80 | public var debugDescription: String { 81 | return self.description 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Markup/Keyboard/KeyboardKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardKey.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /// Represents a single key of a MarkupKeyboard. 13 | public struct MarkupKeyboardKey: Codable, Equatable { 14 | 15 | /// The text displayed on the button. If no other optional is used, this will be sent to the bot when pressed. 16 | public var text: String 17 | 18 | /// (Optional) If True, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only. 19 | public var requestContact: Bool = false 20 | 21 | // (Optional) If True, the user's current location will be sent when the button is pressed. Available in private chats only. 22 | public var requestLocation: Bool = false 23 | 24 | /// Coding keys to map values when Encoding and Decoding. 25 | enum CodingKeys: String, CodingKey { 26 | case text 27 | case requestContact = "request_contact" 28 | case requestLocation = "request_location" 29 | } 30 | 31 | 32 | init(text: String) { 33 | self.text = text 34 | } 35 | 36 | init(withLocationRequest requestLocation: Bool, text: String) { 37 | self.text = text 38 | self.requestLocation = requestLocation 39 | } 40 | 41 | init(withContactRequest requestContact: Bool, text: String) { 42 | self.text = text 43 | self.requestContact = requestContact 44 | } 45 | 46 | static public func ==(lhs: MarkupKeyboardKey, rhs: MarkupKeyboardKey) -> Bool { 47 | if lhs.text != rhs.text { return false } 48 | if lhs.requestContact != rhs.requestContact { return false } 49 | if lhs.requestLocation != rhs.requestLocation { return false } 50 | 51 | return true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Sync/Sync+Answer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequest+Answer.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 17/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MethodRequestSync { 11 | 12 | /** 13 | ??? 14 | */ 15 | @discardableResult 16 | public func answerCallbackQuery(queryID: String, 17 | text: String?, 18 | showAlert: Bool, 19 | url: String = "", 20 | cacheTime: Int = 0) -> Bool { 21 | 22 | let request = TelegramRequest.answerCallbackQuery(queryID: queryID, text: text, showAlert: showAlert, url: url, cacheTime: cacheTime) 23 | let response = tag.sendSyncRequest(request) 24 | return MethodRequest.decodeResponse(response) ?? false 25 | } 26 | 27 | /** 28 | ??? 29 | */ 30 | @discardableResult 31 | public func answerInlineQuery(queryID: String, 32 | results: [InlineResult], 33 | cacheTime: Int = 0, 34 | isPersonal: Bool = false, 35 | nextOffset: String?, 36 | switchPM: String?, 37 | switchPMParam: String?) -> Bool { 38 | 39 | let request = TelegramRequest.answerInlineQuery(queryID: queryID, 40 | results: results, 41 | cacheTime: cacheTime, 42 | isPersonal: isPersonal, 43 | nextOffset: nextOffset, 44 | switchPM: switchPM, 45 | switchPMParam: switchPMParam) 46 | 47 | if request != nil { 48 | let response = tag.sendSyncRequest(request!) 49 | return MethodRequest.decodeResponse(response) ?? false 50 | } 51 | 52 | return false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultAudio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultAudio.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents either a link to a MP3 audio file stored on the Telegram servers, or an external URL link to one. 12 | */ 13 | public struct InlineResultAudio: InlineResult { 14 | 15 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 16 | public var metatype: InlineResultType = .audio 17 | 18 | // Type of the result being given. 19 | public var type: String = "audio" 20 | 21 | // Unique Identifier for the result, 1-64 bytes. 22 | var id: String 23 | 24 | // Content of the message to be sent. 25 | var content: InputMessageContent? 26 | 27 | // Inline keyboard attached to the message 28 | var markup: MarkupInline? 29 | 30 | 31 | 32 | // A valid URL for the audio file. 33 | var url: String? 34 | 35 | /// A valid file identifier for the audio file. 36 | var fileID: String? 37 | 38 | /// A caption for the Audio file to be sent, 200 characters maximum. 39 | var caption: String? 40 | 41 | /// Title. 42 | var title: String? 43 | 44 | /// Performer. 45 | var performer: String? 46 | 47 | /// Audio duration in seconds. 48 | var duration: Int? 49 | 50 | 51 | /// Coding keys to map values when Encoding and Decoding. 52 | enum CodingKeys: String, CodingKey { 53 | case type 54 | case id 55 | case content = "input_message_content" 56 | case markup = "reply_markup" 57 | 58 | case url = "audio_url" 59 | case fileID = "audio_file_id" 60 | 61 | case caption 62 | case title 63 | case performer 64 | case duration = "audio_duration" 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Response/TelegramResponseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // PelicanPackageDescription 4 | // 5 | // Created by Takanu Kyriako on 19/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// I might use this later, but currently I have no need to manually define many kinds of errors. 12 | /* 13 | class TelegramResponseError { 14 | 15 | /// The error code given by Telegram. This error code is not unique. 16 | var internalErrorCode: Int 17 | /// The error code given by Pelican to ensure errors are uniquely defined. 18 | var pelicanErrorCode: String 19 | /// A description of the error given by Telegram. 20 | var description: String 21 | 22 | init(telegramCode: Int, pelicanCode: String, description: String) { 23 | self.internalErrorCode = telegramCode 24 | self.pelicanErrorCode = pelicanCode 25 | self.description = description 26 | } 27 | 28 | } 29 | 30 | extension TelegramResponse { 31 | /** 32 | Defines a collection of generic Telegram Request errors that can be received. 33 | 34 | * This is not a complete list. 35 | * Error codes and descriptions can change at any time, and the ones stated here may become outdated at any time. 36 | */ 37 | static var errorList: [TelegramResponseError] = [ 38 | TelegramResponseError(telegramCode: 400, pelicanCode: "400-A", description: "Bad Request"), 39 | TelegramResponseError(telegramCode: 401, pelicanCode: "400-A", description: "Unauthorised"), 40 | TelegramResponseError(telegramCode: 402, pelicanCode: "400-A", description: "Unautorised"), 41 | TelegramResponseError(telegramCode: 403, pelicanCode: "400-A", description: "Unautorised"), 42 | TelegramResponseError(telegramCode: 401, pelicanCode: "400-A", description: "Unautorised"), 43 | 44 | 45 | 46 | ] 47 | 48 | } 49 | */ 50 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Photo.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | 13 | /** 14 | Represents one size of a photo sent from a message, a photo you want to send to a chat, or a file/sticker. 15 | */ 16 | public struct Photo: TelegramType, MessageFile { 17 | 18 | // STORAGE AND IDENTIFIERS 19 | public var contentType: String = "photo" 20 | public var method: String = "sendPhoto" 21 | 22 | // FILE SOURCE 23 | public var fileID: String? 24 | public var url: String? 25 | 26 | // PARAMETERS 27 | /// The width of the photo in pixels. 28 | public var width: Int? 29 | 30 | /// Height of the photo in pixels. 31 | public var height: Int? 32 | 33 | /// The file size of the photo. 34 | public var fileSize: Int? 35 | 36 | 37 | /// Coding keys to map values when Encoding and Decoding. 38 | enum CodingKeys: String, CodingKey { 39 | case fileID = "file_id" 40 | case url 41 | 42 | case width 43 | case height 44 | case fileSize = "file_size" 45 | } 46 | 47 | 48 | public init(fileID: String, width: Int? = nil, height: Int? = nil, fileSize: Int? = nil) { 49 | self.fileID = fileID 50 | self.width = width 51 | self.height = height 52 | self.fileSize = fileSize 53 | } 54 | 55 | public init?(url: String, width: Int? = nil, height: Int? = nil, fileSize: Int? = nil) { 56 | 57 | if url.checkURLValidity(acceptedExtensions: ["png", "jpg"]) == false { return nil } 58 | 59 | self.url = url 60 | self.width = width 61 | self.height = height 62 | self.fileSize = fileSize 63 | } 64 | 65 | public func getQuery() -> [String : Codable] { 66 | let keys: [String: Codable] = [ 67 | "photo": fileID] 68 | 69 | return keys 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Async/Async+Answer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Async+Answer.swift 3 | // Pelican 4 | // 5 | // Created by Ido Constantine on 26/03/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MethodRequestAsync { 11 | 12 | /** 13 | ??? 14 | */ 15 | public func answerCallbackQuery(queryID: String, 16 | text: String?, 17 | showAlert: Bool, 18 | url: String = "", 19 | cacheTime: Int = 0, 20 | callback: CallbackBoolean = nil) { 21 | 22 | let request = TelegramRequest.answerCallbackQuery(queryID: queryID, text: text, showAlert: showAlert, url: url, cacheTime: cacheTime) 23 | tag.sendAsyncRequest(request) { response in 24 | 25 | if callback != nil { 26 | callback!(MethodRequest.decodeResponse(response) ?? false) 27 | } 28 | } 29 | } 30 | 31 | /** 32 | ??? 33 | */ 34 | public func answerInlineQuery(queryID: String, 35 | results: [InlineResult], 36 | cacheTime: Int = 0, 37 | isPersonal: Bool = false, 38 | nextOffset: String?, 39 | switchPM: String?, 40 | switchPMParam: String?, 41 | callback: CallbackBoolean = nil) { 42 | 43 | let request = TelegramRequest.answerInlineQuery(queryID: queryID, results: results, cacheTime: cacheTime, isPersonal: isPersonal, nextOffset: nextOffset, switchPM: switchPM, switchPMParam: switchPMParam) 44 | 45 | if request != nil { 46 | tag.sendAsyncRequest(request!) { response in 47 | 48 | if callback != nil { 49 | callback!(MethodRequest.decodeResponse(response) ?? false) 50 | return 51 | } 52 | } 53 | } 54 | 55 | if callback != nil { 56 | callback!(false) 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Sync/Sync+Payments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequest+Payments.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MethodRequestSync { 11 | 12 | /** 13 | Use this method to send invoices. 14 | - parameter prices: An array of costs involved in the transaction (eg. product price, taxes, discounts). 15 | */ 16 | public func sendInvoice(title: String, 17 | description: String, 18 | payload: String, 19 | providerToken: String, 20 | startParameter: String, 21 | currency: String, 22 | prices: [String: Int], 23 | chatID: String) { 24 | 25 | } 26 | 27 | /** 28 | If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. 29 | Use this method to reply to shipping queries. On success, True is returned. 30 | */ 31 | public func answerShippingQuery(shippingQueryID: String, 32 | acceptShippingAddress: Bool, 33 | shippingOptionsFIXME: [String]?, 34 | errorMessage: String?) { 35 | 36 | 37 | 38 | } 39 | 40 | /** 41 | Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to respond to such pre-checkout queries. On success, True is returned. 42 | 43 | - note: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent. 44 | */ 45 | public func answerPreCheckoutQuery(preCheckoutQueryID: String, 46 | acceptPaymentQuery: Bool, 47 | errorMessage: String?) { 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/DispatchQueue/SessionDispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDispatchQueue.swift 3 | // App 4 | // 5 | // Created by Takanu Kyriako on 05/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Dispatch // Required on Linux platforms. 10 | 11 | /** 12 | Encapsulates a DispatchQueue with a synchronised array of all work currently queued to it. 13 | Allows for all work queued on a DispatchQueue to be executed without race conditions or manual work comparison. 14 | */ 15 | public class SessionDispatchQueue { 16 | 17 | /// The current queue of DispatchWorkItems waiting to be executed. 18 | private var workItems = SynchronizedArray() 19 | 20 | /// The DispatchQueue used to queue update handler work. 21 | private var queue: DispatchQueue 22 | 23 | init(tag: SessionTag, label: String, qos: DispatchQoS) { 24 | self.queue = DispatchQueue(label: "\(label)-\(tag.id)", 25 | qos: qos, 26 | autoreleaseFrequency: .inherit, 27 | target: nil) 28 | } 29 | 30 | /** 31 | Wraps a DispatchWorkItem around the given block set at the same QoS level as the DispatchQueue, and adds it to the queue to be executed. 32 | */ 33 | public func async(_ block: @escaping () -> ()) { 34 | 35 | /// Wraps the block around a request to remove the first item in the work queue. 36 | let newWorkBlock = { 37 | self.workItems.remove(at: 0) 38 | block() 39 | } 40 | 41 | let newWorkItem = DispatchWorkItem(qos: queue.qos, 42 | flags: .inheritQoS, 43 | block: newWorkBlock) 44 | 45 | workItems.append(newWorkItem) 46 | queue.async(execute: newWorkItem) 47 | } 48 | 49 | /** 50 | Cancels all DispatchWorkItems not currently being executed by the queue. 51 | */ 52 | public func cancelAll() { 53 | workItems.forEach { 54 | $0.cancel() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Markup/Keyboard/KeyboardRemove.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keyboard+Actions.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a special action that when sent with a message, will remove any `MarkupKeyboard` 14 | currently active, for either all of or a specified group of users. 15 | */ 16 | public struct MarkupKeyboardRemove: MarkupType, Codable, Equatable { 17 | 18 | /// Requests clients to remove the custom keyboard (user will not be able to summon this keyboard) 19 | var removeKeyboard: Bool = true 20 | 21 | /// (Optional) Use this parameter if you want to remove the keyboard from specific users only. 22 | public var selective: Bool = false 23 | 24 | /// Coding keys to map values when Encoding and Decoding. 25 | enum CodingKeys: String, CodingKey { 26 | case removeKeyboard = "remove_keyboard" 27 | case selective 28 | } 29 | 30 | 31 | /** 32 | Creates a `MarkupKeyboardRemove` instance, to remove an active `MarkupKeyboard` from the current chat. 33 | 34 | If isSelective is true, the keyboard will only be removed for the targets of the message. 35 | 36 | **Targets:** 37 | 1) users that are @mentioned in the text of the Message object; 38 | 2) if the bot's message is a reply (has reply_to_message_id), sender of the original message. 39 | 40 | - parameter isSelective: If false, the keyboard will be removed for all users. If true however, the 41 | keyboard will only be cleared for the targets you specify. 42 | */ 43 | public init(isSelective sel: Bool) { 44 | selective = sel 45 | } 46 | 47 | public static func ==(lhs: MarkupKeyboardRemove, rhs: MarkupKeyboardRemove) -> Bool { 48 | if lhs.removeKeyboard != rhs.removeKeyboard { return false } 49 | if lhs.selective != rhs.selective { return false } 50 | 51 | return true 52 | } 53 | 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultLocation.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a location on a map. By default, the location will be sent by the user. 12 | 13 | Alternatively, you can use `content` property to send a message with the specified content instead of the location. 14 | */ 15 | public struct InlineResultLocation: InlineResult { 16 | 17 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 18 | public var metatype: InlineResultType = .location 19 | 20 | /// Type of the result being given. 21 | public var type: String = "location" 22 | 23 | /// Unique Identifier for the result, 1-64 bytes. 24 | public var id: String 25 | 26 | /// Content of the message to be sent. 27 | public var content: InputMessageContent? 28 | 29 | /// Inline keyboard attached to the message 30 | public var markup: MarkupInline? 31 | 32 | 33 | /// Location title. 34 | public var title: String 35 | 36 | /// Location latitude in degrees. 37 | public var latitude: Float 38 | 39 | /// Location longitude in degrees. 40 | public var longitude: Float 41 | 42 | 43 | /// URL of the thumbnail to use for the result. 44 | public var thumbURL: String? 45 | 46 | /// Thumbnail width. 47 | public var thumbWidth: Int? 48 | 49 | /// Thumbnail height. 50 | public var thumbHeight: Int? 51 | 52 | 53 | /// Coding keys to map values when Encoding and Decoding. 54 | enum CodingKeys: String, CodingKey { 55 | case type 56 | case id 57 | case content = "input_message_content" 58 | case markup = "reply_markup" 59 | 60 | case title 61 | case latitude 62 | case longitude 63 | 64 | case thumbURL = "thumb_url" 65 | case thumbWidth = "thumb_width" 66 | case thumbHeight = "thumb_height" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Payments/SuccessfulPayment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SuccessfulPayment.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Contains basic information about a successful payment for the Telegram Payment API. 13 | */ 14 | public struct SuccessfulPayment: Codable { 15 | 16 | /// Three-letter [ISO 4217 currency code](https://core.telegram.org/bots/payments#supported-currencies). 17 | var currency: String = "" 18 | 19 | /** 20 | Total price in the smallest units of the currency. For example, for a price of US$ 1.45 pass amount = 145. 21 | 22 | See the exp parameter in [currencies.json](https://core.telegram.org/bots/payments/currencies.json) for more information, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). 23 | */ 24 | var totalAmount: Int = 0 25 | 26 | /// The bot-specified invoice payload. 27 | var invoicePayload: String = "" 28 | 29 | /// The identifier of the shipping option chosen by the user, if applicable. 30 | var shippingOptionID: String? 31 | 32 | /// The order information provided by the user, if applicable. 33 | var orderInfo: OrderInfo? 34 | 35 | /// Telegram's payment identifier for the payment. 36 | var telegramPaymentChargeID: String = "" 37 | 38 | /// The external payment identifier for the payment, determined by the provider chosen. 39 | var providerPaymentChargeID: String = "" 40 | 41 | 42 | /// Coding keys to map values when Encoding and Decoding. 43 | enum CodingKeys: String, CodingKey { 44 | case currency 45 | case totalAmount = "total_amount" 46 | case invoicePayload = "invoice_payload" 47 | case shippingOptionID = "shipping_option_id" 48 | case orderInfo = "order_info" 49 | case telegramPaymentChargeID = "telegram_payment_charge_id" 50 | case providerPaymentChargeID = "provider_payment_charge_id" 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InputMessageContent/InputMessageContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputMessageContent.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents the content of a message to be sent as a result of an inline query. This contains all types 12 | */ 13 | public struct InputMessageContent: Codable { 14 | 15 | public var type: InputMessageContentType 16 | public var base: InputMessageContent_Any 17 | 18 | // These values are only ever used to detect what content type is being held. 19 | /// Coding keys to map values when Encoding and Decoding. 20 | enum CodingKeys: String, CodingKey { 21 | case type 22 | case base 23 | case phoneNumber = "phone_number" 24 | case latitude 25 | case text = "message_text" 26 | case address 27 | } 28 | 29 | public init(content: InputMessageContent_Any) { 30 | 31 | base = content 32 | 33 | if content is InputMessageContent_Venue { type = .venue } 34 | 35 | else if content is InputMessageContent_Location { type = .location } 36 | 37 | else if content is InputMessageContent_Contact { type = .contact } 38 | 39 | else { type = .text } 40 | } 41 | 42 | 43 | public init(from decoder: Decoder) throws { 44 | 45 | // This decoder attempts to find a unique and required key from the given content, and uses that to 46 | // initialise the correct 47 | let values = try decoder.container(keyedBy: CodingKeys.self) 48 | let keys = values.allKeys 49 | 50 | if keys.contains(.phoneNumber) { 51 | type = .contact 52 | } 53 | 54 | else if keys.contains(.address) { 55 | type = .venue 56 | } 57 | 58 | else if keys.contains(.latitude) { 59 | type = .location 60 | } 61 | 62 | else { 63 | type = .text 64 | } 65 | 66 | base = try type.metatype.init(from: decoder) 67 | } 68 | 69 | public func encode(to encoder: Encoder) throws { 70 | try base.encode(to: encoder) 71 | } 72 | } 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultVoice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultVoice.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents either a link to a voice recording in an .ogg container encoded with OPUS that's stored on the Telegram servers, or an external URL link to one. 13 | 14 | By default, this voice recording will be sent by the user. Alternatively, you can use the `content` property to send a message with the specified content instead of the the voice message. 15 | */ 16 | public struct InlineResultVoice: InlineResult { 17 | 18 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 19 | public var metatype: InlineResultType = .voice 20 | 21 | /// Type of the result being given. 22 | public var type: String = "voice" 23 | 24 | /// Unique Identifier for the result, 1-64 bytes. 25 | public var id: String 26 | 27 | /// Content of the message to be sent. 28 | public var content: InputMessageContent? 29 | 30 | /// Inline keyboard attached to the message 31 | public var markup: MarkupInline? 32 | 33 | 34 | /// A valid URL for the voice recording or .ogg file encoded with OPUS. 35 | public var url: String? 36 | 37 | /// A valid file identifier for the file. 38 | public var fileID: String? 39 | 40 | /// A caption for the audio file to be sent, 200 characters maximum. 41 | public var caption: String? 42 | 43 | /// The title of the inline result. 44 | public var title: String? 45 | 46 | /// Recording duration in seconds. 47 | public var duration: Int? 48 | 49 | /// Coding keys to map values when Encoding and Decoding. 50 | enum CodingKeys: String, CodingKey { 51 | case type 52 | case id 53 | case content = "input_message_content" 54 | case markup = "reply_markup" 55 | 56 | case url = "voice_url" 57 | case fileID = "voice_file_id" 58 | case caption 59 | 60 | case title 61 | case duration 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Voice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Voice.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a Voice type, which will appear as if it was a user-recorded voice message if sent. 14 | */ 15 | public struct Voice: TelegramType, MessageFile { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "voice" 19 | public var method: String = "sendVoice" 20 | 21 | // FILE SOURCE 22 | public var fileID: String? 23 | public var url: String? 24 | 25 | // PARAMETERS 26 | /// The duration of the voice note, in seconds. 27 | public var duration: Int? 28 | 29 | /// The mime type of the voice file. 30 | public var mimeType: String? 31 | 32 | /// The file size. 33 | public var fileSize: Int? 34 | 35 | /// Coding keys to map values when Encoding and Decoding. 36 | enum CodingKeys: String, CodingKey { 37 | case fileID = "file_id" 38 | case url 39 | 40 | case duration 41 | case mimeType = "mime_type" 42 | case fileSize = "file_size" 43 | } 44 | 45 | public init(fileID: String, 46 | duration: Int? = nil, 47 | mimeType: String? = nil, 48 | fileSize: Int? = nil) { 49 | 50 | self.fileID = fileID 51 | self.duration = duration 52 | self.mimeType = mimeType 53 | self.fileSize = fileSize 54 | } 55 | 56 | public init?(url: String, 57 | duration: Int? = nil, 58 | mimeType: String? = nil, 59 | fileSize: Int? = nil) { 60 | 61 | if url.checkURLValidity(acceptedExtensions: ["ogg"]) == false { return nil } 62 | 63 | self.url = url 64 | self.duration = duration 65 | self.mimeType = mimeType 66 | self.fileSize = fileSize 67 | } 68 | 69 | // SendType conforming methods to send itself to Telegram under the provided method. 70 | public func getQuery() -> [String: Codable] { 71 | var keys: [String: Codable] = [ 72 | "voice": fileID] 73 | 74 | if duration != 0 { keys["duration"] = duration } 75 | 76 | return keys 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/PelicanTests/MessageSendingTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestMessageSending.swift 3 | // PelicanTests 4 | // 5 | // Created by Ido Constantine on 31/08/2017. 6 | // 7 | 8 | import XCTest 9 | import Pelican 10 | import Vapor 11 | 12 | class TestMessageSending: XCTestCase { 13 | 14 | 15 | override func setUp() { 16 | 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | 28 | func testPelicanSetup() throws { 29 | 30 | class TestBot: ChatSession { 31 | 32 | // Do your setup here, this is when the session gets created. 33 | override func postInit() { 34 | 35 | super.postInit() 36 | 37 | let start = RouteCommand(commands: "start") { update in 38 | if update.content == "" || update.from == nil { return false } 39 | 40 | let audio = Audio(url: "ferriswheel.mp3") 41 | self.send.file(audio, caption: "Hey check out this kool tuun.", markup: nil) 42 | 43 | return true 44 | } 45 | 46 | routes.add(start) 47 | } 48 | } 49 | 50 | // Make sure you set up Pelican manually so you can assign it variables. 51 | let config = try! Config() 52 | let pelican = try! Pelican(config: config) 53 | 54 | // Add Builder 55 | pelican.addBuilder(SessionBuilder(spawner: Spawn.perChatID(updateType: [.message], chatType: [.private]), idType: .chat, session: TestBot.self, setup: nil) ) 56 | 57 | pelican.setPoll(interval: 1) 58 | 59 | // This defines what message types your bot can receive. 60 | pelican.allowedUpdates = [.message, .callbackQuery, .inlineQuery, .chosenInlineResult] 61 | pelican.timeout = 0 62 | 63 | // START IT UP! 64 | try config.addProvider(pelican) 65 | let drop = try Droplet(config) 66 | try drop.run() 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Pelican+Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pelican+Errors.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Errors relating to Pelican setup. 12 | */ 13 | public enum PError_Codable: String, Error { 14 | case InlineResultAnyDecodable = "InlineResultAny was unable to be decoded, InlineResultAny is only a wrapper and should not be treated as an entity to encode and decode to." 15 | } 16 | 17 | /** 18 | Errors relating to Pelican setup. 19 | */ 20 | public enum TGBotError: String, Error { 21 | case WorkingDirNotFound = "The working directory couldn't be found." 22 | case ConfigMissing = "The config file is missing. Make sure you include a \"config.json\" file in the project bundle that contains your API token." 23 | case KeyMissing = "The API key hasn't been provided. Please provide a \"bot_token\" in your bundle's config.json, containing your bot token." 24 | case EntryMissing = "Pelican hasn't been given an session setup closure. Please provide one using `sessionSetupAction`." 25 | case NoPollingInterval = "Pelican hasn't been given a polling interval. Please set one using `Pelican.pollInterval`." 26 | } 27 | 28 | /** 29 | Errors related to request fetching. 30 | */ 31 | public enum TGReqError: String, Error { 32 | case NoResponse = "The request received no response." 33 | case UnknownError = "Something happened, and i'm not sure what!" 34 | case BadResponse = "Telegram responded with \"NOT OKAY\" so we're going to trust that it means business." 35 | case ResponseNotExtracted = "The request could not be extracted." 36 | } 37 | 38 | /** 39 | Errors related to update processing. Might merge the two? 40 | */ 41 | public enum TGUpdateError: String, Error { 42 | case BadUpdate = "The message received from Telegram was malformed or unable to be processed by this bot." 43 | } 44 | 45 | /** 46 | Motherfucking Vapor. 47 | */ 48 | public enum TGVaporError: String, Error { 49 | case EngineSucks = "Engine is unable to keep an SSL connection going, please use \"foundation\" instead, under your droplet configuration file." 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultVenue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultVenue.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a venue. By default, the venue will be sent by the user. 12 | 13 | Alternatively, you can use the `content` property to send a message with the specified content instead of the venue. 14 | */ 15 | public struct InlineResultVenue: InlineResult { 16 | 17 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 18 | public var metatype: InlineResultType = .venue 19 | 20 | /// Type of the result being given. 21 | public var type: String = "venue" 22 | 23 | /// Unique Identifier for the result, 1-64 bytes. 24 | public var id: String 25 | 26 | /// Content of the message to be sent. 27 | public var content: InputMessageContent? 28 | 29 | /// Inline keyboard attached to the message 30 | public var markup: MarkupInline? 31 | 32 | 33 | 34 | /// Location title. 35 | public var title: String 36 | 37 | /// Address of the venue. 38 | public var address: String 39 | 40 | /// Location latitude in degrees. 41 | public var latitude: Float 42 | 43 | /// Location longitude in degrees. 44 | public var longitude: Float 45 | 46 | /// Foursquare identifier of the venue if known. 47 | public var foursquareID: String? 48 | 49 | 50 | /// URL of the thumbnail to use for the result. 51 | public var thumbURL: String? 52 | 53 | /// Thumbnail width. 54 | public var thumbWidth: Int? 55 | 56 | /// Thumbnail height. 57 | public var thumbHeight: Int? 58 | 59 | 60 | /// Coding keys to map values when Encoding and Decoding. 61 | enum CodingKeys: String, CodingKey { 62 | case type 63 | case id 64 | case content = "input_message_content" 65 | case markup = "reply_markup" 66 | 67 | case title 68 | case address 69 | case latitude 70 | case longitude 71 | case foursquareID = "foursquare_id" 72 | 73 | case thumbURL = "thumb_url" 74 | case thumbWidth = "thumb_width" 75 | case thumbHeight = "thumb_height" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Document.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Document.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a generic file type, typically not covered by other Telegram file types (like Audio, Voice or Photo). 14 | */ 15 | public struct Document: TelegramType, MessageFile { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "document" 19 | public var method: String = "sendDocument" 20 | 21 | // FILE SOURCE 22 | public var fileID: String? 23 | public var url: String? 24 | 25 | // PARAMETERS 26 | /// Document thumbnail. 27 | public var thumb: Photo? 28 | 29 | /// Original filename. 30 | public var fileName: String? 31 | 32 | /// MIME type of the file. 33 | public var mimeType: String? 34 | 35 | /// File size. 36 | public var fileSize: Int? 37 | 38 | /// Coding keys to map values when Encoding and Decoding. 39 | enum CodingKeys: String, CodingKey { 40 | case fileID = "file_id" 41 | case url 42 | 43 | case thumb = "thumb" 44 | case fileName = "file_name" 45 | case mimeType = "mime_type" 46 | case fileSize = "file_size" 47 | } 48 | 49 | 50 | public init(fileID: String, 51 | thumb: Photo? = nil, 52 | fileName: String? = nil, 53 | mimeType: String? = nil, 54 | fileSize: Int? = nil) { 55 | 56 | self.fileID = fileID 57 | self.thumb = thumb 58 | self.fileName = fileName 59 | self.mimeType = mimeType 60 | self.fileSize = fileSize 61 | } 62 | 63 | public init?(url: String, 64 | thumb: Photo? = nil, 65 | fileName: String? = nil, 66 | mimeType: String? = nil, 67 | fileSize: Int? = nil) { 68 | 69 | if url.checkURLValidity(acceptedExtensions: []) == false { return nil } 70 | 71 | self.url = url 72 | self.thumb = thumb 73 | self.fileName = fileName 74 | self.mimeType = mimeType 75 | self.fileSize = fileSize 76 | } 77 | 78 | // SendType conforming methods 79 | public func getQuery() -> [String: Codable] { 80 | let keys: [String: Codable] = [ 81 | "document": fileID] 82 | 83 | return keys 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Log/Log+Prints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug+Prints.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 23/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PLog { 11 | 12 | /** 13 | Attempts to print a log at the verbose level, with the included type and text. 14 | Also automatically includes the file, function and line the function is called from. 15 | */ 16 | static func verbose(_ text: String, file: String = #file, function: String = #function, line: Int = #line) { 17 | PLog.addPrint(level: .verbose, text: text, file: file, function: function, line: line) 18 | } 19 | 20 | /** 21 | Attempts to print a log at the info level, with the included type and text. 22 | Also automatically includes the file, function and line the function is called from. 23 | */ 24 | static func info(_ text: String, file: String = #file, function: String = #function, line: Int = #line) { 25 | PLog.addPrint(level: .info, text: text, file: file, function: function, line: line) 26 | } 27 | 28 | /** 29 | Attempts to print a log at the warning level, with the included type and text. 30 | Also automatically includes the file, function and line the function is called from. 31 | */ 32 | static func warning(_ text: String, file: String = #file, function: String = #function, line: Int = #line) { 33 | PLog.addPrint(level: .warning, text: text, file: file, function: function, line: line) 34 | } 35 | 36 | /** 37 | Attempts to print a log at the error level, with the included type and text. 38 | Also automatically includes the file, function and line the function is called from. 39 | */ 40 | static func error(_ text: String, file: String = #file, function: String = #function, line: Int = #line) { 41 | PLog.addPrint(level: .error, text: text, file: file, function: function, line: line) 42 | } 43 | 44 | /** 45 | Attempts to print a log at the severe level, with the included type and text. 46 | Also automatically includes the file, function and line the function is called from. 47 | */ 48 | static func fatal(_ text: String, file: String = #file, function: String = #function, line: Int = #line) { 49 | PLog.addPrint(level: .fatal, text: text, file: file, function: function, line: line) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultContact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultContact.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a contact with a phone number. 12 | 13 | By default, this contact will be sent by the user. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 14 | */ 15 | public struct InlineResultContact: InlineResult { 16 | 17 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 18 | public var metatype: InlineResultType = .contact 19 | 20 | /// Type of the result being given. 21 | public var type: String = "contact" 22 | 23 | /// Unique identifier for the result, 1-64 bytes. 24 | public var id: String 25 | 26 | /// Content of the message to be sent. 27 | public var content: InputMessageContent? 28 | 29 | /// Inline keyboard attached to the message 30 | public var markup: MarkupInline? 31 | 32 | 33 | /// Contact's phone number. 34 | public var phoneNumber: String 35 | 36 | /// Contact's first name. 37 | public var firstName: String 38 | 39 | /// Contact's last name. 40 | public var lastName: String? 41 | 42 | 43 | /// URL of the thumbnail to use for the result. 44 | public var thumbURL: String? 45 | 46 | /// Thumbnail width. 47 | public var thumbWidth: Int? 48 | 49 | /// Thumbnail height. 50 | public var thumbHeight: Int? 51 | 52 | /// Coding keys to map values when Encoding and Decoding. 53 | enum CodingKeys: String, CodingKey { 54 | case type 55 | case id 56 | case content = "input_message_content" 57 | case markup = "reply_markup" 58 | 59 | case phoneNumber = "phone_number" 60 | case firstName = "first_name" 61 | case lastName = "last_name" 62 | 63 | case thumbURL = "thumb_url" 64 | case thumbWidth = "thumb_width" 65 | case thumbHeight = "thumb_height" 66 | } 67 | 68 | init(id: String, phoneNumber: String, firstName: String, lastName: String?, content: InputMessageContent?) { 69 | self.id = id 70 | self.phoneNumber = phoneNumber 71 | self.firstName = firstName 72 | self.lastName = lastName 73 | self.content = content 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/VideoNote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoNote.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a VideoNote type, introduced in Telegram 4.0. 12 | */ 13 | public struct VideoNote: TelegramType, MessageContent, MessageFile { 14 | 15 | // STORAGE AND IDENTIFIERS 16 | public var contentType: String = "video_note" 17 | public var method: String = "sendVideoNote" 18 | 19 | // FILE SOURCE 20 | public var fileID: String? 21 | public var url: String? 22 | 23 | // PARAMETERS 24 | /// Width and height of the video in pixels. 25 | public var length: Int? 26 | 27 | /// Duration of the video in seconds. 28 | public var duration: Int? 29 | 30 | /// A thumbnail displayed for the video before it plays. 31 | public var thumb: Photo? 32 | 33 | /// The file size of the video 34 | public var fileSize: Int? 35 | 36 | /// Coding keys to map values when Encoding and Decoding. 37 | enum CodingKeys: String, CodingKey { 38 | case fileID = "file_id" 39 | case url 40 | 41 | case length 42 | case duration 43 | case thumb 44 | case fileSize = "file_size" 45 | } 46 | 47 | 48 | public init(fileID: String, 49 | length: Int? = nil, 50 | duration: Int? = nil, 51 | thumb: Photo? = nil, 52 | fileSize: Int? = nil) { 53 | 54 | self.fileID = fileID 55 | self.length = length 56 | self.duration = duration 57 | self.thumb = thumb 58 | self.fileSize = fileSize 59 | } 60 | 61 | 62 | public init?(url: String, 63 | length: Int? = nil, 64 | duration: Int? = nil, 65 | thumb: Photo? = nil, 66 | fileSize: Int? = nil) { 67 | 68 | if url.checkURLValidity(acceptedExtensions: ["mp4"]) == false { return nil } 69 | 70 | self.url = url 71 | self.length = length 72 | self.duration = duration 73 | self.thumb = thumb 74 | self.fileSize = fileSize 75 | } 76 | 77 | 78 | // SendType conforming methods to send itself to Telegram under the provided method. 79 | public func getQuery() -> [String: Codable] { 80 | var keys: [String: Codable] = [ 81 | "chat_id": fileID] 82 | 83 | if duration != 0 { keys["duration"] = duration } 84 | 85 | return keys 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Callback/CallbackQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallbackQuery.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | This object represents an incoming callback query from a callback button in an inline keyboard. 14 | 15 | If the button that originated the query was attached to a message sent by the bot, the field message will be present. If the button was attached to a message sent via the bot (in inline mode), the field inline_message_id will be present. Exactly one of the fields data or game_short_name will be present. 16 | */ 17 | final public class CallbackQuery: UpdateModel { 18 | 19 | /// Unique identifier for the query. 20 | public var id: String 21 | 22 | /// The sender of the query. 23 | public var from: User 24 | 25 | /// Message with the callback button that originated from the query. This will not be available if it's too old. 26 | public var message: Message? 27 | 28 | /// Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. Useful for high scores in games. 29 | public var chatInstance: String 30 | 31 | /// Identifier of the message sent via the bot in inline mode that originated the query. 32 | public var inlineMessageID: String? 33 | 34 | /// Data associated with the callback button. Be aware that a bad client can send arbitrary data here. 35 | public var data: String? 36 | 37 | /// Short name of a Game to be returned, serves as the unique identifier for the game. 38 | public var gameShortName: String? 39 | 40 | 41 | /// Coding keys to map values when Encoding and Decoding. 42 | enum CodingKeys: String, CodingKey { 43 | case id 44 | case from 45 | case message 46 | case inlineMessageID = "inline_message_id" 47 | case chatInstance = "chat_instance" 48 | case data 49 | case gameShortName = "game_short_name" 50 | } 51 | 52 | 53 | public init(id: String, 54 | from: User, 55 | messahe: Message? = nil, 56 | chatInstance: String, 57 | inlineMessageID: String? = nil, 58 | data: String? = nil, 59 | gameShortName: String? = nil) { 60 | 61 | self.id = id 62 | self.from = from 63 | self.chatInstance = chatInstance 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultGif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultGif.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents either a link to an animated GIF stored on the Telegram servers, or an external URL link to one. 12 | 13 | By default, this GIF will be sent by the user with an optional caption. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 14 | */ 15 | public struct InlineResultGIF: InlineResult { 16 | 17 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 18 | public var metatype: InlineResultType = .gif 19 | 20 | // Type of the result being given. 21 | public var type: String = "gif" 22 | 23 | // Unique Identifier for the result, 1-64 bytes. 24 | var id: String 25 | 26 | // Content of the message to be sent. 27 | var content: InputMessageContent? 28 | 29 | // Inline keyboard attached to the message 30 | var markup: MarkupInline? 31 | 32 | /// The title of the inline result. 33 | var title: String? 34 | 35 | 36 | 37 | /// A valid URL for the GIF file. File size must not exceed 1MB. 38 | var url: String? 39 | 40 | /// A file id for the inline result. Won't be used if the inline result is represented by a URL instead. 41 | var fileID: String? 42 | 43 | // A caption for the GIF to be sent, 200 characters maximum. 44 | var caption: String? 45 | 46 | // Width of the GIF. 47 | var width: Int? 48 | 49 | // Height of the GIF. 50 | var height: Int? 51 | 52 | /// Duration of the GIF. 53 | var duration: Int? 54 | 55 | // URL of the static thumbnail for the result (JPEG or GIF). This is not optional on non-cached types. 56 | var thumbURL: String? 57 | 58 | 59 | 60 | /// Coding keys to map values when Encoding and Decoding. 61 | enum CodingKeys: String, CodingKey { 62 | case type 63 | case id 64 | case content = "input_message_content" 65 | case markup = "reply_markup" 66 | case title 67 | 68 | case url = "mpeg4_url" 69 | case fileID = "gif_file_id" 70 | case caption 71 | 72 | case width = "gif_width" 73 | case height = "gif_height" 74 | case duration = "gif_duration" 75 | case thumbURL = "thumb_url" 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/User/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a Telegram user or bot. 14 | */ 15 | public struct User: Codable { 16 | public let messageTypeName = "user" 17 | 18 | /// Unique identifier for the user or bot. 19 | public var tgID: String 20 | 21 | /// If true, this user is a bot. 22 | public var isBot: Bool 23 | 24 | /// User's or bot's first name. 25 | public var firstName: String 26 | 27 | /// User's or bot's last name. 28 | public var lastName: String? 29 | 30 | /// User's or bot's username. 31 | public var username: String? 32 | 33 | /// IETF language tag of the user's language. 34 | public var languageCode: String? 35 | 36 | 37 | enum CodingKeys: String, CodingKey { 38 | case tgID = "id" 39 | case isBot = "is_bot" 40 | case firstName = "first_name" 41 | case lastName = "last_name" 42 | case username = "username" 43 | case languageCode = "language_code" 44 | } 45 | 46 | public init(id: String, isBot: Bool, firstName: String) { 47 | self.tgID = id 48 | self.isBot = isBot 49 | self.firstName = firstName 50 | } 51 | 52 | public init(from decoder: Decoder) throws { 53 | let values = try decoder.container(keyedBy: CodingKeys.self) 54 | 55 | tgID = try String(values.decode(Int.self, forKey: .tgID)) 56 | isBot = try values.decodeIfPresent(Bool.self, forKey: .isBot) ?? false 57 | 58 | firstName = try values.decode(String.self, forKey: .firstName) 59 | lastName = try values.decodeIfPresent(String.self, forKey: .lastName) 60 | username = try values.decodeIfPresent(String.self, forKey: .username) 61 | languageCode = try values.decodeIfPresent(String.self, forKey: .languageCode) 62 | } 63 | 64 | public func encode(to encoder: Encoder) throws { 65 | var container = encoder.container(keyedBy: CodingKeys.self) 66 | 67 | let intID = Int(tgID) 68 | try container.encode(intID, forKey: .tgID) 69 | try container.encodeIfPresent(isBot, forKey: .isBot) 70 | try container.encode(firstName, forKey: .firstName) 71 | try container.encodeIfPresent(lastName, forKey: .lastName) 72 | try container.encodeIfPresent(username, forKey: .username) 73 | try container.encodeIfPresent(languageCode, forKey: .languageCode) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultPhoto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultPhoto.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents either a link to a photo that's stored on the Telegram servers, or an external URL link to one. 13 | 14 | By default, this photo will be sent by the user with an optional caption. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 15 | */ 16 | public struct InlineResultPhoto: InlineResult { 17 | 18 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 19 | public var metatype: InlineResultType = .photo 20 | 21 | /// Type of the result being given. 22 | public var type: String = "photo" 23 | 24 | /// Unique Identifier for the result, 1-64 bytes. 25 | public var id: String 26 | 27 | /// Content of the message to be sent. 28 | public var content: InputMessageContent? 29 | 30 | /// Inline keyboard attached to the message 31 | public var markup: MarkupInline? 32 | 33 | 34 | /// A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB 35 | public var url: String? 36 | 37 | /// A valid file identifier of the photo. 38 | public var fileID: String? 39 | 40 | /// The title of the inline result. 41 | public var title: String? 42 | 43 | /// A short description of the inline result. 44 | public var description: String? 45 | 46 | /// A caption for the photo to be sent, 200 characters maximum. 47 | public var caption: String? 48 | 49 | 50 | /// URL of the thumbnail to use for the result. 51 | public var thumbURL: String? 52 | 53 | /// Thumbnail width. 54 | public var thumbWidth: Int? 55 | 56 | /// Thumbnail height. 57 | public var thumbHeight: Int? 58 | 59 | 60 | /// Coding keys to map values when Encoding and Decoding. 61 | enum CodingKeys: String, CodingKey { 62 | case type 63 | case id 64 | case content = "input_message_content" 65 | case markup = "reply_markup" 66 | 67 | case url = "photo_url" 68 | case fileID = "photo_file_id" 69 | case caption 70 | 71 | case title 72 | case description 73 | 74 | case thumbURL = "thumb_url" 75 | case thumbWidth = "thumb_width" 76 | case thumbHeight = "thumb_height" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultMpeg4Gif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultMpeg4Gif.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents either a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers, or an external URL link to one. 12 | 13 | By default, this MP4 GIF will be sent by the user with an optional caption. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 14 | */ 15 | public struct InlineResultMpeg4GIF: InlineResult { 16 | 17 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 18 | public var metatype: InlineResultType = .mpeg4Gif 19 | 20 | /// Type of the result being given. 21 | public var type: String = "mpeg4_gif" 22 | 23 | /// Unique Identifier for the result, 1-64 bytes. 24 | public var id: String 25 | 26 | /// Content of the message to be sent. 27 | public var content: InputMessageContent? 28 | 29 | /// Inline keyboard attached to the message 30 | public var markup: MarkupInline? 31 | 32 | 33 | 34 | /// A valid URL for the MP4 file. File size must not exceed 1MB. 35 | public var url: String? 36 | 37 | /// A valid file identifier for the MP4 file 38 | public var fileID: String? 39 | 40 | /// The title of the inline result. 41 | public var title: String? 42 | 43 | /// A caption for the MP4 GIF to be sent, 200 characters maximum. 44 | public var caption: String? 45 | 46 | /// Video width. 47 | public var width: Int? 48 | 49 | /// Video height. 50 | public var height: Int? 51 | 52 | /// Video duration. 53 | public var duration: Int? 54 | 55 | /// URL of the static thumbnail (jpeg or gif) for the result. This is not optional on non-cached types. 56 | public var thumbURL: String? 57 | 58 | 59 | 60 | /// Coding keys to map values when Encoding and Decoding. 61 | enum CodingKeys: String, CodingKey { 62 | case type 63 | case id 64 | case content = "input_message_content" 65 | case markup = "reply_markup" 66 | 67 | case url = "mpeg4_url" 68 | case fileID = "mpeg4_file_id" 69 | case title 70 | case caption 71 | 72 | case width = "mpeg4_width" 73 | case height = "mpeg4_height" 74 | case duration = "mpeg4_duration" 75 | case thumbURL = "thumb_url" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/PelicanTests/TestTemplates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PelicanTest.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 17/08/2017. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import Vapor 11 | import Pelican 12 | 13 | enum TestTemplateError: String, Error { 14 | case NoToken = "The token could not be found." 15 | } 16 | 17 | class GhostPelican { 18 | var drop: Droplet 19 | var pelican: Pelican 20 | var config: Config 21 | var token: String 22 | 23 | init() throws { 24 | 25 | // Make sure you set up Pelican manually so you can assign it variables. 26 | config = try Config() 27 | pelican = try Pelican(config: config) 28 | 29 | token = (config["pelican", "token"]?.string!)! 30 | 31 | pelican.setPoll(interval: 1) 32 | 33 | try config.addProvider(pelican) 34 | drop = try Droplet(config) 35 | } 36 | } 37 | 38 | // Used to provide common functions between test cases. 39 | class TestCase: XCTestCase { 40 | 41 | /** 42 | Repeats a given task multiple times, to see if it fails to complete it or if other errors are asserted. 43 | The function will also report back assertions with the number of times the operation was successfully executed beforehand. 44 | */ 45 | func testLoop(loop: Int, timeout: Double, description: String, task: @escaping () -> Void) { 46 | 47 | var i = 0 48 | 49 | for _ in 0.. Bool { 21 | 22 | // Check that the file has a valid file-extension. 23 | if acceptedExtensions.count > 0 { 24 | if self.components(separatedBy: ".").last == nil { return false } 25 | 26 | 27 | let ext = self.components(separatedBy: ".").last! 28 | if acceptedExtensions.contains(ext) == false { return false } 29 | 30 | return true 31 | } 32 | 33 | // Add checks for remote/internal paths and path consistency. 34 | return true 35 | } 36 | 37 | /** 38 | Converts a string that has snake case formatting to a camel case format. 39 | */ 40 | var snakeToCamelCase: String { 41 | 42 | let items = self.components(separatedBy: "_") 43 | var camelCase = "" 44 | 45 | items.enumerated().forEach { 46 | camelCase += 0 == $0 ? $1 : $1.capitalized 47 | } 48 | 49 | return camelCase 50 | } 51 | 52 | /** 53 | Converts a string that has camel case formatting to a snake case format. 54 | */ 55 | var camelToSnakeCase: String { 56 | 57 | // Check that we have characters... 58 | guard self.characters.count > 0 else { return self } 59 | 60 | var newString: String = "" 61 | 62 | // Break up the string into only "Unicode Scalars", which boils it down to a UTF-32 code. 63 | // This allows us to check if the identifier belongs in the "uppercase letter" set. 64 | let first = self.unicodeScalars.first! 65 | newString.append(Character(first)) 66 | 67 | for scalar in self.unicodeScalars.dropFirst() { 68 | 69 | // If the unicode scalar contains an upper case letter, add an underscore and lowercase the letter. 70 | if CharacterSet.uppercaseLetters.contains(scalar) { 71 | let character = Character(scalar) 72 | newString.append("_") 73 | newString += String(character).lowercased() 74 | } 75 | 76 | // Otherwise append it to the new string. 77 | else { 78 | let character = Character(scalar) 79 | newString.append(character) 80 | } 81 | } 82 | 83 | return newString 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/MethodRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 16/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A delegate for creating and sending TelegramRequest types. 12 | 13 | Use this in your sessions to make requests to Telegram like sending messages or getting chat information. 14 | The delegate will attempt to process the response received from Telegram into a useful and context-sensitive type. 15 | 16 | - To make synchronous calls where your code execution will wait until a response from Telegram is received, use the `sync` property. 17 | - To make asynchronous calls where your code execution will continue immediately after the request is made and sent, use the `async` property. 18 | 19 | - note: Due to the unpredictability of networking, server up-time and maintenance there is always a chance that a request 20 | may not succeed. Make sure to account for this when writing your own bots. 21 | */ 22 | public struct MethodRequest { 23 | 24 | /// The tag of the session that this request instance belongs to. 25 | var tag: SessionTag 26 | 27 | /// Contains all API methods you can use to make a synchronous API request, where your code execution will wait until a response from Telegram is received. 28 | public var sync: MethodRequestSync 29 | 30 | /// Contains all API methods you can use to make an asynchronous API request, where your code execution will continue immediately after the request is made and sent. 31 | public var async: MethodRequestAsync 32 | 33 | public init(tag: SessionTag) { 34 | self.tag = tag 35 | self.sync = MethodRequestSync(tag: tag) 36 | self.async = MethodRequestAsync(tag: tag) 37 | } 38 | 39 | /** 40 | A convenience function for decoding TelegramResponse data to any given type. 41 | */ 42 | static public func decodeResponse(_ response: TelegramResponse?) -> T? { 43 | 44 | if response == nil { return nil } 45 | if response!.success == false { return nil } 46 | 47 | if let result = response!.result { 48 | do { 49 | let data = try result.rawData() 50 | let decoder = JSONDecoder() 51 | return try decoder.decode(T.self, from: data) 52 | 53 | } catch { 54 | PLog.error("Pelican Response Decode Error: - \(error) - \(error.localizedDescription)") 55 | return nil 56 | } 57 | } else { 58 | PLog.error("Pelican Response Decode Error: Unable to decode result, no response.") 59 | return nil 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Stickers/Sticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sticker.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a Telegram sticker. 14 | */ 15 | final public class Sticker: TelegramType, MessageFile { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "sticker" 19 | public var method: String = "sendSticker" 20 | 21 | // FILE SOURCE 22 | public var fileID: String? 23 | public var url: String? 24 | 25 | // PARAMETERS 26 | /// The width of the sticker in pixels. 27 | public var width: Int? 28 | 29 | /// The height of the sticker in pixels. 30 | public var height: Int? 31 | 32 | /// Sticker thumbnail in .webp or .jpg format. 33 | public var thumb: Photo? 34 | 35 | /// A series of emoji associated with the sticker. 36 | public var emoji: String? 37 | 38 | /// The file size of the sticker. 39 | public var fileSize: Int? 40 | 41 | /// The name of the sticker set that this sticker belongs to. 42 | public var stickerSetName: String? 43 | 44 | /// For mask stickers, the position where the mask should be placed. 45 | public var maskPosition: MaskPosition? 46 | 47 | /// Coding keys to map values when Encoding and Decoding. 48 | enum CodingKeys: String, CodingKey { 49 | case fileID = "file_id" 50 | case url 51 | 52 | case width 53 | case height 54 | case thumb 55 | case emoji 56 | case fileSize = "file_size" 57 | case stickerSetName = "set_name" 58 | case maskPosition = "mask_position" 59 | } 60 | 61 | public init(fileID: String, width: Int? = nil, height: Int? = nil, thumb: Photo? = nil, emoji: String? = nil, fileSize: Int? = nil) { 62 | self.fileID = fileID 63 | self.width = width 64 | self.height = height 65 | self.thumb = thumb 66 | self.emoji = emoji 67 | self.fileSize = fileSize 68 | } 69 | 70 | public init?(url: String, width: Int? = nil, height: Int? = nil, thumb: Photo? = nil, emoji: String? = nil, fileSize: Int? = nil) { 71 | 72 | if url.checkURLValidity(acceptedExtensions: ["webp", "jpg", "png"]) == false { return nil } 73 | 74 | self.url = url 75 | self.width = width 76 | self.height = height 77 | self.thumb = thumb 78 | self.emoji = emoji 79 | self.fileSize = fileSize 80 | } 81 | 82 | // SendType conforming methods to send itself to Telegram under the provided method. 83 | public func getQuery() -> [String: Codable] { 84 | let keys: [String: Codable] = [ 85 | "file_id": fileID] 86 | 87 | return keys 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Type+Standard.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | 6 | 7 | /*** 8 | Represents a file ready to be downloaded. The file can be downloaded via the link https://api.telegram.org/file/bot/. It is guaranteed that the link will be valid for at least one hour. When the link expires, a new one can be requested by calling getFile. 9 | */ 10 | /* 11 | class File: Model { 12 | var id: Node? 13 | var fileID: String 14 | var fileSize: Int? // File size, if known 15 | var filePath: String? // File path, use https://api.telegram.org/file/bot/ to get the file. 16 | 17 | init(fileID: String) { 18 | self.fileID = fileID 19 | } 20 | 21 | // NodeRepresentable conforming methods 22 | required init(node: Node, in context: Context) throws { 23 | fileID = try row.get("file_id") 24 | fileSize = try row.get("file_size") 25 | filePath = try row.get("file_path") 26 | } 27 | 28 | func makeNode() throws -> Node { 29 | var keys: [String:NodeRepresentable?] = [ 30 | "file_id": fileID, 31 | ] 32 | 33 | if fileSize != nil { keys["file_size"] = fileSize } 34 | if filePath != nil { keys["file_path"] = filePath } 35 | return try Node(node: keys) 36 | } 37 | 38 | func makeNode(context: Context) throws -> Node { 39 | return try self.makeNode() 40 | } 41 | 42 | // Preparation conforming methods, for creating and deleting a database. 43 | static func prepare(_ database: Database) throws { 44 | try database.create("users") { users in 45 | users.id() 46 | } 47 | } 48 | 49 | static func revert(_ database: Database) throws { 50 | try database.delete("users") 51 | } 52 | } 53 | */ 54 | 55 | // Currently out of date and has no place in the current API type collection 56 | 57 | /** 58 | // Contains information about why a request was unsuccessful. 59 | final public class ResponseParameters { 60 | public var storage = Storage() 61 | public var migrateToChatID: Int? // ??? 62 | public var retryAfter: Int? // ??? 63 | 64 | public init(retryAfter: Int) { 65 | self.retryAfter = retryAfter 66 | } 67 | 68 | // NodeRepresentable conforming methods 69 | public required init(row: Row) throws { 70 | migrateToChatID = try row.get("migrate_to_chat_id") 71 | retryAfter = try row.get("retry_after") 72 | } 73 | 74 | public func makeRow() throws -> Row { 75 | var row = Row() 76 | try row.set("migrate_to_chat_id", migrateToChatID) 77 | try row.set("retry_after", retryAfter) 78 | 79 | return row 80 | } 81 | } 82 | */ 83 | 84 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultDocument.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents either a link to a file stored on the Telegram servers, or an external URL link to one. 13 | 14 | By default, this file will be sent by the user with an optional caption. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 15 | 16 | - note: Currently, only .PDF and .ZIP files can be sent using this method. 17 | */ 18 | public struct InlineResultDocument: InlineResult { 19 | 20 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 21 | public var metatype: InlineResultType = .document 22 | 23 | /// Type of the result being given. 24 | public var type: String = "document" 25 | 26 | /// Unique Identifier for the result, 1-64 bytes. 27 | public var id: String 28 | 29 | /// Content of the message to be sent. 30 | public var content: InputMessageContent? 31 | 32 | /// Inline keyboard attached to the message 33 | public var markup: MarkupInline? 34 | 35 | 36 | /// A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB 37 | public var url: String? 38 | 39 | /// A valid file identifier for the file. 40 | public var fileID: String? 41 | 42 | /// A caption for the document to be sent, 200 characters maximum. 43 | public var caption: String? 44 | 45 | /// The title of the inline result. 46 | public var title: String? 47 | 48 | /// A short description of the inline result. 49 | public var description: String? 50 | 51 | /// Mime type of the content of the file, either “application/pdf” or “application/zip”. Not optional for un-cached results. 52 | var mimeType: String? 53 | 54 | 55 | /// URL of the thumbnail to use for the result. 56 | public var thumbURL: String? 57 | 58 | /// Thumbnail width. 59 | public var thumbWidth: Int? 60 | 61 | /// Thumbnail height. 62 | public var thumbHeight: Int? 63 | 64 | 65 | /// Coding keys to map values when Encoding and Decoding. 66 | enum CodingKeys: String, CodingKey { 67 | case type 68 | case id 69 | case content = "input_message_content" 70 | case markup = "reply_markup" 71 | 72 | case url = "document_url" 73 | case fileID = "document_file_id" 74 | case caption 75 | 76 | case title 77 | case description 78 | case mimeType = "mime_type" 79 | 80 | case thumbURL = "thumb_url" 81 | case thumbWidth = "thumb_width" 82 | case thumbHeight = "thumb_height" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Response/TelegramResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramResponse.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 11/07/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | /** 13 | Represents a response from Telegram servers once a request has been made. 14 | */ 15 | public class TelegramResponse: CustomStringConvertible { 16 | 17 | // HTTP CORE 18 | /// The HTTP Version. 19 | //public var version: Version 20 | 21 | /// The status of the request. 22 | public private(set) var status: Status 23 | 24 | /// HTTP response headers. 25 | public private(set) var headers: [String: String] 26 | 27 | /// The date the response was received. 28 | public private(set) var date = Date() 29 | 30 | /// The body of the response. 31 | public private(set) var body: JSON 32 | 33 | 34 | // TELEGRAM INFO 35 | /// If true, the request was a success. 36 | public private(set) var success: Bool 37 | 38 | /// The Telegram code sent back. 39 | public private(set) var responseCode: String? 40 | 41 | /// The error description, if the request was unsuccessful. 42 | public private(set) var responseStatus: String? 43 | 44 | /// The result of the request, if successful. 45 | public private(set) var result: JSON? 46 | 47 | public var description: String { 48 | 49 | if success == true { 50 | return """ 51 | Telegram Response: Successful 52 | Date: \(date) 53 | """ 54 | 55 | } else { 56 | return """ 57 | Telegram Response: Error (\(responseCode ?? "No Error Code")) 58 | Date: \(date) 59 | Status: \(responseStatus ?? "No Status Received") 60 | """ 61 | } 62 | } 63 | 64 | 65 | /** 66 | Converts a response received from a Telegram Request to a response type. 67 | - parameter response: The Vapor.Response type returned from a Telegram API request. Failing 68 | to provide the correct response will result in a runtime error. 69 | */ 70 | init(data: Data, urlResponse: HTTPURLResponse) throws { 71 | 72 | //self.version = urlResponse.something 73 | self.status = Status(statusCode: urlResponse.statusCode) 74 | var newHeaders: [String: String] = [:] 75 | urlResponse.allHeaderFields.forEach { tuple in 76 | newHeaders["\(tuple.key)"] = "\(tuple.value)" 77 | } 78 | 79 | self.headers = newHeaders 80 | self.body = JSON(data: data) 81 | 82 | self.success = body["ok"].bool ?? false 83 | self.responseCode = body["error_code"].string 84 | self.responseStatus = body["description"].string 85 | self.result = body["result"] 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Router/Types/RouteManual.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteManual.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Lets you define your own routing function for incoming updates. 13 | */ 14 | public class RouteManual: Route { 15 | 16 | /// The commands to listen for in a message. Only one has to be found in the set for the action to execute. 17 | public var handler: (Update) -> (Bool) 18 | 19 | /** 20 | Initialises a route with a manually defined handler function to check whether a given update can trigger another function. 21 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 22 | you create unique names for routes you want to be considered separate entities. 23 | - parameter handler: The custom handler to use to define whether an Update can trigger a Route action. Return true if the update should be used by the route, false if not. 24 | - parameter action: The function to be executed if the Route is able to handle an incoming update, verified by the handler function. 25 | */ 26 | public init(name: String = "", handler: @escaping (Update) -> (Bool), action: @escaping (Update) -> (Bool)) { 27 | 28 | self.handler = handler 29 | super.init(name: name, action: action) 30 | } 31 | 32 | /** 33 | Initialises a route with a manually defined handler function to check whether a given update can trigger another function. 34 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 35 | you create unique names for routes you want to be considered separate entities. 36 | - parameter handler: The custom handler to use to define whether an Update can trigger a Route action. Return true if the update should be used by the route, false if not. 37 | - parameter routes: The routes that an update will be propagated to if the handler function returns true. 38 | */ 39 | public init(name: String = "", handler: @escaping (Update) -> (Bool), routes: Route...) { 40 | 41 | self.handler = handler 42 | super.init(name: name, routes: routes) 43 | } 44 | 45 | override public func handle(_ update: Update) -> Bool { 46 | 47 | let result = handler(update) 48 | 49 | if result == true { 50 | return passUpdate(update) 51 | } else { 52 | return false 53 | } 54 | 55 | } 56 | 57 | override public func compare(_ route: Route) -> Bool { 58 | 59 | // Check the types match 60 | if route is RouteManual { 61 | let otherRoute = route as! RouteManual 62 | return super.compare(otherRoute) 63 | } 64 | 65 | return false 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Type+Custom.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | 5 | 6 | /* 7 | 8 | // Defines what the thumbnail is being used for. 9 | enum InlineThumbnailType: String { 10 | case contact 11 | case document 12 | case photo 13 | case GIF // JPEG or GIF. 14 | case mpeg4GIF // JPEG or GIF. 15 | case video // JPEG only. 16 | case location 17 | } 18 | 19 | // A thumbnail to represent a preview of an inline file. NOT FOR NORMAL FILE USE. 20 | struct InlineThumbnail { 21 | //var type: ThumbnailType 22 | var url: String = "" 23 | var width: Int = 0 24 | var height: Int = 0 25 | 26 | init () { 27 | 28 | } 29 | 30 | func getQuerySet() -> [String : CustomStringConvertible] { 31 | var keys: [String:CustomStringConvertible] = [ 32 | "thumb_url": url] 33 | 34 | if width != 0 { keys["thumb_width"] = width } 35 | if height != 0 { keys["thumb_height"] = height } 36 | return keys 37 | } 38 | 39 | // NodeRepresentable conforming methods 40 | init(node: Node, in context: Context) throws { 41 | //type = try node.extract("type") 42 | url = try node.extract("thumb_url") 43 | width = try node.extract("thumb_width") 44 | height = try node.extract("thumb_height") 45 | } 46 | 47 | func makeNode() throws -> Node { 48 | var keys: [String:NodeRepresentable] = [ 49 | "url": url 50 | ] 51 | 52 | if width != 0 { keys["width"] = width } 53 | if height != 0 { keys["height"] = height } 54 | return try Node(node: keys) 55 | } 56 | 57 | func makeNode(context: Context) throws -> Node { 58 | return try self.makeNode() 59 | } 60 | } 61 | 62 | 63 | enum InputFileType { 64 | case fileID(String) 65 | case url(String) 66 | } 67 | 68 | // A type for handling files for sending. 69 | class InputFile: NodeConvertible, JSONConvertible { 70 | var type: InputFileType 71 | 72 | // NodeRepresentable conforming methods 73 | required init(node: Node, in context: Context) throws { 74 | let fileID: String = try node.extract("file_id") 75 | 76 | if fileID != "" { 77 | type = .fileID(fileID) 78 | } 79 | else { 80 | type = .url(try node.extract("url")) 81 | } 82 | } 83 | 84 | func makeNode() throws -> Node { 85 | var keys: [String:NodeRepresentable] = [:] 86 | 87 | switch type{ 88 | case .fileID(let fileID): 89 | keys["file_id"] = fileID 90 | case .url(let url): 91 | keys["url"] = url 92 | } 93 | 94 | return try Node(node: keys) 95 | } 96 | 97 | func makeNode(context: Context) throws -> Node { 98 | return try self.makeNode() 99 | } 100 | } 101 | */ 102 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultVideo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultVideo.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Represents either a link to a video file that's stored on the Telegram servers, or an external URL link to one. Can also be a link to a page containing an embedded video player. 13 | 14 | By default, this video will be sent by the user with an optional caption. Alternatively, you can use the `content` property to send a message with the specified content instead of the file. 15 | 16 | - note: If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must replace its content using the `content` property. 17 | */ 18 | public struct InlineResultVideo: InlineResult { 19 | 20 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 21 | public var metatype: InlineResultType = .video 22 | 23 | /// Type of the result being given. 24 | public var type: String = "video" 25 | 26 | /// Unique Identifier for the result, 1-64 bytes. 27 | public var id: String 28 | 29 | /// Content of the message to be sent. 30 | public var content: InputMessageContent? 31 | 32 | /// Inline keyboard attached to the message 33 | public var markup: MarkupInline? 34 | 35 | 36 | 37 | /// A valid URL for the embedded video player or video file. 38 | public var url: String? 39 | 40 | /// A valid file identifier for the video file. 41 | public var fileID: String? 42 | 43 | /// The title of the inline result. 44 | public var title: String? 45 | 46 | /// A short description of the inline result. 47 | public var description: String? 48 | 49 | /// A caption for the photo to be sent, 200 characters maximum. 50 | public var caption: String? 51 | 52 | /// Video width. 53 | public var width: Int? 54 | 55 | /// Video height. 56 | public var height: Int? 57 | 58 | /// Video duration. 59 | public var duration: Int? 60 | 61 | /// Mime type of the content of the file, either “text/html" or "video/mp4". 62 | var mimeType: String 63 | 64 | /// URL of the thumbbail for the result. 65 | var thumbURL: String? 66 | 67 | /// Coding keys to map values when Encoding and Decoding. 68 | enum CodingKeys: String, CodingKey { 69 | case type 70 | case id 71 | case content = "input_message_content" 72 | case markup = "reply_markup" 73 | 74 | case url = "video_url" 75 | case fileID = "video_file_id" 76 | case title 77 | case description 78 | case caption 79 | 80 | case width = "video_width" 81 | case height = "video_height" 82 | case duration = "video_duration" 83 | case mimeType = "mime_type" 84 | case thumbURL = "thumb_url" 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Type+Protocols.swift: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | Defines anything relating to types, but isn't a real type component, such as protocols and extensions. 4 | */ 5 | 6 | import Foundation 7 | 8 | 9 | /** 10 | For any model that replicates the Telegram API, it must inherit fron this to be designed to build queries and be converted from responses 11 | in a way that Telegram understands. 12 | */ 13 | protocol TelegramType { 14 | 15 | } 16 | 17 | 18 | // Defines a type that can send unique content types inside a message. 19 | public protocol MessageContent: Codable { 20 | 21 | /// What API type the type that uses this protocol is imitating. 22 | var contentType: String { get } 23 | 24 | /// The method used when the Telegram API call is made. 25 | var method: String { get } 26 | 27 | 28 | /** Not yet implemented due to type overlap with content that doesn't need a file to be sent */ 29 | 30 | /// An optional type to specify the MIME type of the file being sent. 31 | //var mimeType: String? { get set } 32 | 33 | 34 | /// Used to enable any method sending this message content to access the information it needs to deliver it, omitting any custom properties. 35 | func getQuery() -> [String: Codable] 36 | 37 | 38 | } 39 | 40 | 41 | 42 | /** 43 | Defines a type of Message content that is represented by a fileID (in which case the content has been uploaded before), 44 | and/or a URL (the location of the resource to be uploaded). 45 | 46 | All types that conform to this protocol should also provide initialisers that accept a URL instead of a fileID 47 | */ 48 | public protocol MessageFile: MessageContent { 49 | 50 | /// The content type it represents, used by Telegram to interpret what is being sent. 51 | var contentType: String { get } 52 | 53 | /// The method that will be used to send this content type. 54 | var method: String { get } 55 | 56 | /// The Telegram File ID, obtained when a file is uploaded to the bot either by the bot itself, or by a user interacting with it. 57 | var fileID: String? { get set } 58 | 59 | /** 60 | The path to the resource either as a local source relative to the Public/ folder 61 | of your app (eg. `karaoke/jack-1.png`) or as a remote source using an HTTP link. 62 | */ 63 | var url: String? { get set } 64 | } 65 | 66 | extension MessageFile { 67 | 68 | /// Returns whether or not the object has the necessary requirements to be sent. 69 | var isSendable: Bool { 70 | if fileID == nil && url == nil { 71 | return false 72 | } 73 | return true 74 | } 75 | } 76 | 77 | 78 | 79 | /** 80 | Defines a type that encapsulates a request from a user, through a standard Telegram API type (eg. Message, InlineQuery, CallbackQuery). 81 | */ 82 | public protocol UpdateModel: Codable { 83 | 84 | 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Schedule/Schedule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pelican+Schedule.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 18/07/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | 13 | /** 14 | Organises a list of scheduled events, submitted either by Pelican or a Session to perform at a later date. 15 | 16 | Updates by the scheduler are dispatched to the DispatchQueue of the session it belongs to, in order to ensure thread-safe operations. 17 | */ 18 | public class Schedule { 19 | 20 | /// A sorted list of events, sorted from the shortest delay to the longest. 21 | var queue = SynchronizedArray() 22 | 23 | /// The time the run loop was last called at (used for popExpiredEvent) 24 | var runTime: Date = Date() 25 | 26 | /// A callback that allows the Schedule to request that Pelican have the event work be performed on the originating Session's DispatchQueue. 27 | var requestExecution: (SessionTag, @escaping () -> ()) -> () 28 | 29 | /** 30 | The range that the Schedule will look outside the current time to find events to execute on in seconds. 31 | Useful to account for any loop time fluctuations, defaults to 0.1 seconds. 32 | */ 33 | var fluctuationRange: Double = 0.1 34 | 35 | 36 | init(workCallback: @escaping (SessionTag, @escaping () -> ()) -> ()) { 37 | self.requestExecution = workCallback 38 | } 39 | 40 | 41 | /** 42 | Adds an event to the queue. 43 | */ 44 | func add(_ event: ScheduleEvent) { 45 | 46 | if let index = queue.index(where: { $0.executeTime.timeIntervalSince1970 > event.executeTime.timeIntervalSince1970 } ) { 47 | queue.insert(event, at: index) 48 | } 49 | 50 | else { 51 | queue.append(event) 52 | } 53 | 54 | } 55 | 56 | /** 57 | Removes an event from the queue if it matches the event used as an argument. 58 | */ 59 | func remove(_ event: ScheduleEvent) { 60 | queue.remove(event) 61 | } 62 | 63 | /** 64 | Checks to see if an event's time has expired, so it can be removed from the stack. 65 | */ 66 | private func popExpiredEvent() -> ScheduleEvent? { 67 | 68 | if queue.count == 0 { return nil } 69 | if queue[0] == nil { return nil } 70 | 71 | if queue[0]!.executeTime.timeIntervalSince1970 < runTime.timeIntervalSince1970 + fluctuationRange { 72 | let event = queue[0]! 73 | queue.remove(at: 0) 74 | return event 75 | } 76 | 77 | return nil 78 | } 79 | 80 | 81 | /** 82 | Executes all available, expired events in the schedule queue. 83 | */ 84 | func run() { 85 | 86 | runTime = Date() 87 | 88 | while let event = popExpiredEvent() { 89 | requestExecution(event.tag, event.action) 90 | } 91 | } 92 | 93 | } 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Sources/Pelican/Utilities/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // kabuki 4 | // 5 | // Created by Takanu Kyriako on 24/03/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | import Dispatch // Must be specified for Linux support 12 | 13 | // This needs re-factoring ASAP. 14 | 15 | internal func iterateEnum(_: T.Type) -> AnyIterator { 16 | var i = 0 17 | return AnyIterator { 18 | let next = withUnsafeBytes(of: &i) { $0.load(as: T.self) } 19 | if next.hashValue != i { return nil } 20 | i += 1 21 | return next 22 | } 23 | } 24 | 25 | internal class DispatchTimer { 26 | private let queue = DispatchQueue(label: "timer") 27 | private let interval: TimeInterval 28 | private let execute: () -> Void 29 | private var operation: DispatchWorkItem? 30 | 31 | init(interval: TimeInterval, execute: @escaping () -> Void) { 32 | self.interval = interval 33 | self.execute = execute 34 | } 35 | 36 | func start() { 37 | let operation = DispatchWorkItem { [weak self] in 38 | 39 | defer { self?.start() } 40 | self?.execute() 41 | 42 | } 43 | self.operation = operation 44 | queue.asyncAfter(deadline: .now() + interval, execute: operation) 45 | } 46 | func stop() { 47 | operation?.cancel() 48 | } 49 | } 50 | 51 | /// With Vapor 2 these are currently no longer necessary. 52 | /* 53 | // Extensions to manipulate node entries more seamlessly 54 | extension Node { 55 | mutating func addNodeEntry(name: String, value: NodeConvertible) throws { 56 | var object = self.nodeObject 57 | if object != nil { 58 | object![name] = try value.makeNode() 59 | self = try object!.makeNode() 60 | } 61 | } 62 | 63 | mutating func removeNodeEntry(name: String) throws -> Bool { 64 | var object = self.nodeObject 65 | if object != nil { 66 | _ = object!.removeValue(forKey: name) 67 | self = try object!.makeNode() 68 | return true 69 | } 70 | 71 | else { return false } 72 | } 73 | 74 | mutating func renameNodeEntry(from: String, to: String) throws -> Bool { 75 | var object = self.nodeObject 76 | if object != nil { 77 | let value = object!.removeValue(forKey: from) 78 | object![to] = value 79 | self = try object!.makeNode() 80 | return true 81 | } 82 | 83 | else { return false } 84 | } 85 | 86 | mutating func removeNilValues() throws { 87 | var object = self.nodeObject 88 | if object != nil { 89 | for value in object! { 90 | 91 | if value.value.isNull == true { 92 | object!.removeValue(forKey: value.key) 93 | } 94 | } 95 | 96 | self = try object!.makeNode() 97 | } 98 | } 99 | } 100 | */ 101 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Queues/LoopQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pelican+Queues.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 04/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Dispatch // Required on Linux platforms. 10 | 11 | 12 | // The Dispatch queue for getting updates and serving them to sessions. 13 | class LoopQueue { 14 | 15 | /// The actual dispatch queue, that uses the highest level of QoS. 16 | private let queue: DispatchQueue 17 | 18 | /// The minimum length of time that must be between the start of the code block and the end of it, before it is called again. 19 | private let interval: TimeInterval 20 | 21 | /// The time the current code block was executed. 22 | private var startTime: Date 23 | 24 | /// The length of time it took for the code block to fully execute. 25 | private var lastExecuteLength: TimeInterval 26 | 27 | /// The code block to be executed by the queue. 28 | private let execute: () -> Void 29 | 30 | /// The work item that encapsulates the `execute` closure property, which is the item pushed to the queue 31 | private var operation: DispatchWorkItem? 32 | 33 | init(queueLabel: String, 34 | qos: DispatchQoS, 35 | interval: TimeInterval, 36 | execute: @escaping () -> Void) { 37 | 38 | self.queue = DispatchQueue(label: queueLabel, 39 | qos: qos, 40 | target: nil) 41 | 42 | self.interval = interval 43 | self.startTime = Date() 44 | self.lastExecuteLength = TimeInterval.init(0) 45 | self.execute = execute 46 | self.operation = DispatchWorkItem(qos: qos, flags: .enforceQoS) { [weak self] in 47 | 48 | // Record the starting time and execute the loop 49 | self?.startTime = Date() 50 | self?.execute() 51 | self?.queueNext() 52 | } 53 | } 54 | 55 | /** 56 | The function that should be called when the code block finishes executing on the queue, to determine when to 57 | schedule the next code block execution. 58 | */ 59 | func queueNext() { 60 | lastExecuteLength = abs(startTime.timeIntervalSinceNow) 61 | 62 | // Account for loop time in the asynchronous delay. 63 | let delayTime = interval - lastExecuteLength 64 | 65 | // If the delay time left is below 0, execute the loop immediately. 66 | if delayTime <= 0 { 67 | //PLog.verbose("Update loop executing immediately.") 68 | queue.async(execute: self.operation!) 69 | } 70 | 71 | // Otherwise use the built delay time. 72 | else { 73 | //PLog.verbose("Update loop executing at \(DispatchWallTime.now() + delayTime)") 74 | queue.asyncAfter(wallDeadline: .now() + delayTime, execute: self.operation!) 75 | } 76 | } 77 | 78 | /** 79 | Cancels the execution of the `operation` work item, if currently being executed by the queue. 80 | */ 81 | func stop() { 82 | operation?.cancel() 83 | PLog.warning("Update cycle cancelled") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Router/Types/RoutePass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutePass.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Allows any update handled by it to execute the action so long as it meets a specified update type. 13 | 14 | Useful for when you wish to accept any kind of content input from the user and handle it inside the 15 | given action. 16 | */ 17 | public class RoutePass: Route { 18 | 19 | /// The update types the update can potentially be in order for the action to be executed. 20 | public var updateTypes: [UpdateType] = [] 21 | 22 | /** 23 | Initialises a Route that will allow any Update matching specific Update types to trigger an action. 24 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 25 | you create unique names for routes you want to be considered separate entities. 26 | - parameter updateTypes: The types of updates the route can consider for triggering an action. 27 | - parameter action: The function to be executed if the Route is able to handle an incoming update. 28 | */ 29 | public init(name: String = "", updateTypes: [UpdateType], action: @escaping (Update) -> (Bool)) { 30 | 31 | super.init(name: name, action: action) 32 | self.updateTypes = updateTypes 33 | } 34 | 35 | /** 36 | Initialises a Route that will allow any Update matching specific Update types to trigger an action. 37 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 38 | you create unique names for routes you want to be considered separate entities. 39 | - parameter updateTypes: The types of updates the route can consider for triggering an action. 40 | - parameter action: The function to be executed if the Route is able to handle an incoming update. 41 | */ 42 | public init(name: String = "", updateTypes: [UpdateType], routes: Route...) { 43 | 44 | super.init(name: name, routes: routes) 45 | self.updateTypes = updateTypes 46 | } 47 | 48 | override public func handle(_ update: Update) -> Bool { 49 | 50 | if updateTypes.contains(update.type) == true { 51 | return passUpdate(update) 52 | } 53 | 54 | return false 55 | } 56 | 57 | override public func compare(_ route: Route) -> Bool { 58 | 59 | // Check the types match 60 | if route is RoutePass { 61 | let otherRoute = route as! RoutePass 62 | 63 | // Check the type contents 64 | if self.updateTypes.count == otherRoute.updateTypes.count { 65 | 66 | for (i, type) in self.updateTypes.enumerated() { 67 | if type != otherRoute.updateTypes[i] { return false } 68 | } 69 | 70 | 71 | } 72 | 73 | // Check the base Route class 74 | return super.compare(otherRoute) 75 | } 76 | 77 | return false 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Video.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents a video file. Go figure. 14 | */ 15 | public struct Video: TelegramType, MessageFile { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | public var contentType: String = "video" 19 | public var method: String = "sendVideo" 20 | 21 | // FILE SOURCE 22 | public var fileID: String? 23 | public var url: String? 24 | 25 | // PARAMETERS 26 | /// Width of the video in pixels. 27 | public var width: Int? 28 | 29 | /// Height of the video in pixels. 30 | public var height: Int? 31 | 32 | /// Duration of the video in seconds. 33 | public var duration: Int? 34 | 35 | /// A thumbnail displayed for the video before it plays. 36 | public var thumb: Photo? 37 | 38 | /// The mime type of the video. 39 | public var mimeType: String? 40 | 41 | /// The file size of the video 42 | public var fileSize: Int? 43 | 44 | /// Coding keys to map values when Encoding and Decoding. 45 | enum CodingKeys: String, CodingKey { 46 | case fileID = "file_id" 47 | case url 48 | 49 | case width 50 | case height 51 | case duration 52 | case thumb 53 | case mimeType = "mime_type" 54 | case fileSize = "file_size" 55 | } 56 | 57 | 58 | public init(fileID: String, 59 | width: Int? = nil, 60 | height: Int? = nil, 61 | duration: Int? = nil, 62 | thumb: Photo? = nil, 63 | mimeType: String? = nil, 64 | fileSize: Int? = nil) { 65 | 66 | self.fileID = fileID 67 | self.width = width 68 | self.height = height 69 | self.duration = duration 70 | self.thumb = thumb 71 | self.mimeType = mimeType 72 | self.fileSize = fileSize 73 | } 74 | 75 | 76 | public init?(url: String, 77 | width: Int? = nil, 78 | height: Int? = nil, 79 | duration: Int? = nil, 80 | thumb: Photo? = nil, 81 | mimeType: String? = nil, 82 | fileSize: Int? = nil) { 83 | 84 | if url.checkURLValidity(acceptedExtensions: ["mp4"]) == false { return nil } 85 | 86 | self.url = url 87 | self.width = width 88 | self.height = height 89 | self.duration = duration 90 | self.thumb = thumb 91 | self.mimeType = mimeType 92 | self.fileSize = fileSize 93 | } 94 | 95 | 96 | // SendType conforming methods to send itself to Telegram under the provided method. 97 | public func getQuery() -> [String: Codable] { 98 | var keys: [String: Codable] = [ 99 | "video": fileID] 100 | 101 | if duration != 0 { keys["duration"] = duration } 102 | if width != 0 { keys["width"] = width } 103 | if height != 0 { keys["height"] = height } 104 | 105 | return keys 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Request/Request+Payments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Payments.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 18/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension TelegramRequest { 11 | 12 | /** 13 | NOT YET FINISHED ! ! ! 14 | Use this method to send invoices. 15 | - parameter prices: An array of costs involved in the transaction (eg. product price, taxes, discounts). 16 | */ 17 | static public func sendInvoice(title: String, 18 | description: String, 19 | payload: String, 20 | providerToken: String, 21 | startParameter: String, 22 | currency: String, 23 | prices: [String: Int], 24 | chatID: String) -> TelegramRequest { 25 | 26 | let request = TelegramRequest() 27 | 28 | request.query = [ 29 | "title": title, 30 | "description": description, 31 | "payload": payload, 32 | "provider_token": providerToken, 33 | "start_parameter": startParameter, 34 | "currency": currency, 35 | "prices": prices, 36 | "chat_id": Int(chatID) 37 | ] 38 | 39 | // Set the query 40 | request.method = "sendInvoice" 41 | 42 | return request 43 | } 44 | 45 | /** 46 | NOT YET FINISHED ! ! ! 47 | If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. 48 | Use this method to reply to shipping queries. On success, True is returned. 49 | */ 50 | static public func answerShippingQuery(shippingQueryID: String, 51 | acceptShippingAddress: Bool, 52 | shippingOptionsFIXME: [String]?, 53 | errorMessage: String?) -> TelegramRequest { 54 | 55 | let request = TelegramRequest() 56 | 57 | request.query = [ 58 | "shipping_query_id": shippingQueryID, 59 | "ok": acceptShippingAddress, 60 | "shipping_options": shippingOptionsFIXME, 61 | ] 62 | 63 | if errorMessage != nil { request.query["error_message"] = errorMessage } 64 | 65 | // Set the query 66 | request.method = "answerShippingQuery" 67 | 68 | return request 69 | } 70 | 71 | /** 72 | Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to respond to such pre-checkout queries. On success, True is returned. 73 | 74 | - note: The Bot API must receive an answer within 10 seconds after the pre-checkout query was sent. 75 | */ 76 | static public func answerPreCheckoutQuery(preCheckoutQueryID: String, 77 | acceptPaymentQuery: Bool, 78 | errorMessage: String?) -> TelegramRequest { 79 | let request = TelegramRequest() 80 | 81 | request.query = [ 82 | "pre_checkout_query_id": preCheckoutQueryID, 83 | "ok": acceptPaymentQuery, 84 | ] 85 | 86 | if errorMessage != nil { request.query["error_message"] = errorMessage } 87 | 88 | // Set the query 89 | request.method = "answerPreCheckoutQuery" 90 | 91 | return request 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message/MessageEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageEntity.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /// Represents one special entity in a text message, such as a hashtag, username or URL. 13 | public enum MessageEntityType: String, Codable { 14 | 15 | case mention 16 | case hashtag 17 | case botCommand 18 | case url 19 | case email 20 | case textLink 21 | case textMention 22 | 23 | case bold 24 | case italic 25 | case code 26 | case pre 27 | 28 | case unknown 29 | } 30 | 31 | public struct MessageEntity: Codable { 32 | 33 | /// Type of the entity. Can be a mention, hashtag, bot command, URL, email, special text formatting or a text mention. 34 | public var type: MessageEntityType 35 | 36 | /// Offset in UTF-16 code units to the start of the entity. 37 | public var offset: Int 38 | 39 | /// Length of the entity in UTF-16 code units. 40 | public var length: Int 41 | 42 | // For text links only, will be opened when the user taps on it. 43 | public var url: String? 44 | 45 | // For text mentions only, the mentioned user. 46 | public var user: User? 47 | 48 | enum CodingKeys: String, CodingKey { 49 | case type 50 | case offset 51 | case length 52 | case url 53 | case user 54 | } 55 | 56 | 57 | public init(type: MessageEntityType, offset: Int, length: Int) { 58 | self.type = type 59 | self.offset = offset 60 | self.length = length 61 | } 62 | 63 | public init(from decoder: Decoder) throws { 64 | let values = try decoder.container(keyedBy: CodingKeys.self) 65 | 66 | let typeString = try values.decode(String.self, forKey: .type) 67 | type = MessageEntityType(rawValue: typeString.snakeToCamelCase) ?? .unknown 68 | 69 | offset = try values.decode(Int.self, forKey: .offset) 70 | length = try values.decode(Int.self, forKey: .length) 71 | url = try values.decodeIfPresent(String.self, forKey: .url) 72 | user = try values.decodeIfPresent(User.self, forKey: .user) 73 | } 74 | 75 | public func encode(to encoder: Encoder) throws { 76 | var container = encoder.container(keyedBy: CodingKeys.self) 77 | 78 | try container.encode(type.rawValue, forKey: .type) 79 | try container.encode(offset, forKey: .offset) 80 | try container.encode(length, forKey: .length) 81 | try container.encodeIfPresent(url, forKey: .url) 82 | try container.encodeIfPresent(user, forKey: .user) 83 | } 84 | 85 | /** 86 | Extracts the piece of text it represents from the message body. 87 | - returns: The string of the entity if successful, and nil if not. 88 | */ 89 | public func extract(fromMessage message: Message) -> String? { 90 | 91 | if message.text == nil { return nil } 92 | let text = message.text! 93 | 94 | let encoded = text.utf16 95 | let encStart = encoded.index(encoded.startIndex, offsetBy: offset) 96 | let encEnd = encoded.index(encStart, offsetBy: length) 97 | 98 | let stringBody = encoded.prefix(upTo: encEnd) 99 | let finalString = stringBody.suffix(from: encStart) 100 | 101 | return String(finalString) 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Pelican/HTTP/Version.swift: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2015 Intrepid Pursuits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | */ 11 | 12 | public struct Version { 13 | public private(set) var major: Int = 0 14 | public private(set) var minor: Int = 0 15 | public private(set) var patch: Int = 0 16 | 17 | public init(major: Int, minor: Int = 0, patch: Int = 0) { 18 | self.major = major 19 | self.minor = minor 20 | self.patch = patch 21 | } 22 | 23 | public init(_ string: String) { 24 | let components = string.characters.split(separator: ".").map { String($0) } 25 | 26 | if components.count > 0, let major = Int(components[0]) { 27 | self.major = major 28 | } 29 | 30 | if components.count > 1, let minor = Int(components[1]) { 31 | self.minor = minor 32 | } 33 | 34 | if components.count > 2, let patch = Int(components[2]) { 35 | self.patch = patch 36 | } 37 | } 38 | } 39 | 40 | extension Version : CustomStringConvertible { 41 | public var description: String { 42 | return "\(major).\(minor).\(patch)" 43 | } 44 | } 45 | 46 | extension Version : Equatable {} 47 | 48 | public func ==(lhs: Version, rhs: Version) -> Bool { 49 | guard lhs.major == rhs.major else { return false } 50 | guard lhs.minor == rhs.minor else { return false } 51 | guard lhs.patch == rhs.patch else { return false } 52 | return true 53 | } 54 | 55 | public func !=(lhs: Version, rhs: Version) -> Bool { 56 | return !(lhs == rhs) 57 | } 58 | 59 | extension Version : Comparable {} 60 | 61 | public func < (lhs: Version, rhs: Version) -> Bool { 62 | if lhs.major != rhs.major { 63 | return lhs.major < rhs.major 64 | } else if lhs.minor != rhs.minor { 65 | return lhs.minor < rhs.minor 66 | } else { 67 | return lhs.patch < rhs.patch 68 | } 69 | } 70 | 71 | public func <= (lhs: Version, rhs: Version) -> Bool { 72 | return lhs < rhs || lhs == rhs 73 | } 74 | 75 | public func >= (lhs: Version, rhs: Version) -> Bool { 76 | return lhs > rhs || lhs == rhs 77 | } 78 | 79 | public func > (lhs: Version, rhs: Version) -> Bool { 80 | if lhs.major != rhs.major { 81 | return lhs.major > rhs.major 82 | } else if lhs.minor != rhs.minor { 83 | return lhs.minor > rhs.minor 84 | } else { 85 | return lhs.patch > rhs.patch 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /Sources/Pelican/Pelican/Client/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 21/02/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Allows Pelican to make Telegram API methods either synchronosly or asynchronously, using URLSession. 13 | 14 | It also keeps track of all requests, automatically times out any requests that don't receive a response in a 15 | fast enough time and attempts to resolve any errors where possible. 16 | */ 17 | class Client { 18 | 19 | /// The API token of the bot this current instance of Pelican is responsible for. 20 | var token: String 21 | 22 | /// The URLSession to be used for data tasks 23 | var session: URLSession! 24 | 25 | // WILL BE RE-IMPLEMENTED LATER 26 | /// The length of time any client connection is allowed to be active before it's cancelled and the client makes another attempt. 27 | //var connectionTimeout: Duration = 3.sec 28 | 29 | /// The data task currently being performed 30 | var dataTask: URLSessionDataTask? 31 | 32 | /// The time taken to perform the data task 33 | public var requestTime: TimeInterval? 34 | 35 | /// A reference to the CacheManager required to store and update FileIDs of files and optimise file sending. 36 | var cache: CacheManager 37 | 38 | /// The current list of ongoing connections to Telegram sorted by a unique identifier. 39 | var activeRequests = SynchronizedArray() 40 | 41 | 42 | init(token: String, cache: CacheManager) { 43 | 44 | self.token = token 45 | self.cache = cache 46 | 47 | let config = URLSessionConfiguration.default 48 | config.httpCookieStorage = nil 49 | config.httpMaximumConnectionsPerHost = 30 50 | //config.isDiscretionary = false // Not Linux-compatible currently. 51 | config.networkServiceType = .default 52 | //config.timeoutIntervalForRequest = 20 53 | //config.shouldUseExtendedBackgroundIdleMode = false 54 | //config.waitsForConnectivity = true 55 | 56 | /// Assigning the delegate queue is a bad idea. Don't do it unless you know what you're doing. Which we don't. 57 | session = URLSession(configuration: config) 58 | } 59 | 60 | 61 | /** 62 | Makes a synchronous client request. This will block thread execution until a response is received, 63 | but you will be able to directly receive a TelegramResponse and handle it on the same thread. 64 | */ 65 | func syncRequest(request: TelegramRequest) -> TelegramResponse? { 66 | 67 | do { 68 | let urlRequest = try request.makeURLRequest(token, cache: cache) 69 | let portal = ClientConnection(urlRequest) 70 | activeRequests.append(portal) 71 | 72 | return try portal.openSync(session: session) 73 | 74 | } catch { 75 | return nil 76 | } 77 | } 78 | 79 | /** 80 | Makes an asynchronous client request which will not block thread code execution. 81 | An optional closure can be provided to handle the result once a response is received. 82 | */ 83 | func asyncRequest(request: TelegramRequest, callback: ((TelegramResponse?) -> ())? ) { 84 | 85 | do { 86 | let urlRequest = try request.makeURLRequest(token, cache: cache) 87 | let portal = ClientConnection(urlRequest) 88 | activeRequests.append(portal) 89 | 90 | try portal.openAsync(session: session, callback: callback) 91 | 92 | } catch { 93 | 94 | } 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Router/Types/RouteListen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteFilter.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Used to specifically route specific update content like "Yes" or "$200" to actions. 14 | 15 | - warning: Currently just supports string matching, Regex support will come later. 16 | */ 17 | public class RouteListen: Route { 18 | 19 | /** 20 | The pattern that the update content is compared against, to decide whether or not to use it. 21 | Leave it blank if the route is dynamic and wishes to receive any already unclaimed responses. 22 | */ 23 | public var pattern: String = "" 24 | 25 | /// The type of user request the route targets. 26 | private var type: UpdateType 27 | 28 | /// Retrieves the route type, that determines what kind of user responses it is targeting. 29 | public var getType: UpdateType { return type } 30 | 31 | /** 32 | Initialises a route that listens out for specific pieces of text in update content. 33 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 34 | you create unique names for routes you want to be considered separate entities. 35 | - parameter pattern: The text or RegEx request to search for in the contents of an update. 36 | - parameter type: The types of updates the route can consider for triggering an action. 37 | - parameter action: The function to be executed if the Route is able to handle an incoming update. 38 | */ 39 | public init(name: String = "", pattern: String, type: UpdateType, action: @escaping (Update) -> (Bool)) { 40 | 41 | self.type = type 42 | self.pattern = pattern 43 | super.init(name: name, action: action) 44 | } 45 | 46 | /** 47 | Initialises a route that listens out for specific pieces of text in update content, and forwards the update to other routes if successful. 48 | - parameter name: The name of the route. This is used to check for equatibility with other routes, so ensure 49 | you create unique names for routes you want to be considered separate entities. 50 | - parameter pattern: The text or RegEx request to search for in the contents of an update. 51 | - parameter type: The types of updates the route can consider for triggering an action. 52 | - parameter action: The function to be executed if the Route is able to handle an incoming update. 53 | */ 54 | public init(name: String = "", pattern: String, type: UpdateType, routes: Route...) { 55 | 56 | self.type = type 57 | self.pattern = pattern 58 | super.init(name: name, routes: routes) 59 | } 60 | 61 | override public func handle(_ update: Update) -> Bool { 62 | 63 | // If the types match, check the filter 64 | if update.matches(pattern, types: [type.string()]) == true { 65 | 66 | // If we made it, execute the action 67 | return passUpdate(update) 68 | 69 | } 70 | 71 | return false 72 | } 73 | 74 | override public func compare(_ route: Route) -> Bool { 75 | 76 | // Check the types match 77 | if route is RouteListen { 78 | let otherRoute = route as! RouteListen 79 | 80 | // Check the properties 81 | if self.pattern != otherRoute.pattern { return false } 82 | if self.type != otherRoute.type { return false } 83 | 84 | return super.compare(route) 85 | } 86 | 87 | return false 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/PelicanTests/ConnectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionTest.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 17/08/2017. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import Vapor 11 | import Pelican 12 | 13 | class TechnicalConnectionTypes: TestCase { 14 | 15 | /// The length of time a task is allowed to take before a timeout is triggered in the loop tests. 16 | var loopTimeout: Double = 5 17 | /// The number of times the defined operation will be repeated in the loop tests. 18 | var loopCount = 200 19 | /// The template to be operated with. 20 | var ghost = try! GhostPelican() 21 | /// A test client based on the Vapor API 22 | var client: ClientProtocol? 23 | 24 | override func setUp() { 25 | 26 | // Build a new template 27 | ghost = try! GhostPelican() 28 | client = try! ghost.drop.client.makeClient(hostname: "api.telegram.org", port: 443, securityLayer: .tls(EngineClient.defaultTLSContext()), proxy: nil) 29 | 30 | } 31 | 32 | func testVaporRequestSingle() throws { 33 | XCTAssertNotNil(try ghost.drop.client.post(ghost.pelican.getAPIURL + "/getUpdates")) 34 | 35 | } 36 | 37 | func testVaporRequestLoop() throws { 38 | 39 | testLoop(loop: loopCount, timeout: loopTimeout, description: "Connect to the Telegram Bot API using Vapor.") { 40 | XCTAssertNotNil(try! self.ghost.drop.client.post(self.ghost.pelican.getAPIURL + "/getUpdates")) 41 | } 42 | } 43 | 44 | func testURLSessionRequestSingle() throws { 45 | 46 | // Build the session and request 47 | let expectation = self.expectation(description: "Build a JSON object from a URLSession Data Task.") 48 | var request = URLRequest(url: URL(string: self.ghost.pelican.getAPIURL + "/getUpdates")!) 49 | request.httpMethod = "GET" 50 | let session = URLSession.shared 51 | 52 | // Build the task 53 | session.dataTask(with: request) { data, response, error in 54 | 55 | if error != nil { 56 | XCTFail("The URL Session request returned with an error - \(error!)") 57 | } 58 | 59 | else { 60 | let json = try! JSON.init(bytes: data!.makeBytes()) 61 | print(json) 62 | expectation.fulfill() 63 | } 64 | }.resume() 65 | 66 | wait(for: [expectation], timeout: 5.0) 67 | } 68 | 69 | func testURLSessionRequestLoop() throws { 70 | 71 | testLoop(loop: loopCount, timeout: loopTimeout, description: "Connect to the Telegram Bot API using URLSession.") { 72 | 73 | // Build the session and request 74 | 75 | var request = URLRequest(url: URL(string: self.ghost.pelican.getAPIURL + "/getUpdates")!) 76 | request.httpMethod = "POST" 77 | let session = URLSession.shared 78 | 79 | // Build the task 80 | session.dataTask(with: request) { data, response, error in 81 | 82 | if error == nil { 83 | XCTFail("The URL Session request returned with an error - \(error!)") 84 | } 85 | 86 | else { 87 | let json = try! JSON.init(bytes: data!.makeBytes()) 88 | } 89 | }.resume() 90 | } 91 | } 92 | 93 | func testVaporCustomClientRequestLoop() { 94 | 95 | testLoop(loop: loopCount, timeout: loopTimeout, description: "Connect to the Telegram Bot API using a custom client.") { 96 | XCTAssertNotNil(try! self.client!.post(self.ghost.pelican.getAPIURL + "/getUpdates")) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Monitor/TimeoutMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timeout.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Used to monitor timeout conditions for a session. 13 | */ 14 | public class TimeoutMonitor { 15 | 16 | // CALLBACKS 17 | var tag: SessionTag 18 | var schedule: Schedule 19 | 20 | // CONTROLS 21 | /// The types that will allow a timeout to get bumped. 22 | var types: [UpdateType] = [] 23 | 24 | /// The length of time it takes for a timeout to occur if the timeout controller is not successfully bumped. If 0 seconds, a timeout will not be triggered. 25 | var duration: Duration = 0.seconds 26 | 27 | /** 28 | An optional action to be executed if the timeout occurs. 29 | - note: The session will be removed from Pelican as soon as the action has finished executing, do not queue Schedule events using the session. 30 | */ 31 | var action: (() -> ())? 32 | 33 | // CURRENT STATE 34 | /// The last time the timeout time was bumped forwards. 35 | var lastBump = Date() 36 | 37 | /// The last event that was scheduled to trigger a timeout. 38 | var lastEvent: ScheduleEvent? 39 | 40 | 41 | init(tag: SessionTag, schedule: Schedule) { 42 | 43 | self.tag = tag 44 | self.schedule = schedule 45 | } 46 | 47 | 48 | /** 49 | Enable timeouts for the session, which removes the session from the update pool if no updates are received by it within a given timeframe. 50 | */ 51 | public func set(updateTypes: [UpdateType], duration: Duration, action: (() -> ())?) { 52 | 53 | // Set the properties 54 | self.types = updateTypes 55 | self.duration = duration 56 | self.action = action 57 | 58 | // Setup the new Schedule event. 59 | pushEvent() 60 | } 61 | 62 | 63 | /** 64 | Attempts to bump the timeout event further if an update matching the types of updates it considers as 65 | valid timeout checks are received. 66 | */ 67 | public func bump(_ update: Update) { 68 | 69 | if types.contains(update.type) == false { return } 70 | 71 | // Remove the old event and add the new one. 72 | pushEvent() 73 | } 74 | 75 | /** 76 | An internal event for moving the scheduled event for a Timeout to occur further in time. 77 | */ 78 | private func pushEvent() { 79 | 80 | // Check if a last event exists, and pull it from the schedule. 81 | if lastEvent != nil { 82 | schedule.remove(lastEvent!) 83 | } 84 | 85 | // Set the new bump date and event 86 | lastBump = Date() 87 | lastEvent = ScheduleEvent(tag: tag, delay: [duration]) { 88 | 89 | if self.action != nil { 90 | self.action!() 91 | } 92 | 93 | self.tag.sendEvent(type: .timeout, action: .remove) 94 | } 95 | 96 | _ = schedule.add(lastEvent!) 97 | } 98 | 99 | /** 100 | Should be used by the owning Session to clean up requested events before the Session itself closes. 101 | */ 102 | public func close() { 103 | 104 | if lastEvent != nil { 105 | schedule.remove(lastEvent!) 106 | } 107 | 108 | reset() 109 | } 110 | 111 | /** 112 | Resets all Timeout properties to default values. 113 | */ 114 | public func reset() { 115 | self.types = [] 116 | self.duration = 0.sec 117 | self.action = nil 118 | 119 | self.lastBump = Date() 120 | self.lastEvent = nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Message Content/Audio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Audio.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 31/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /** 13 | Represents an audio file to be treated as music by the Telegram clients. 14 | */ 15 | public struct Audio: TelegramType, MessageFile { 16 | 17 | // STORAGE AND IDENTIFIERS 18 | /// The content type it represents, used by Telegram to interpret what is being sent. 19 | public var contentType: String = "audio" 20 | 21 | /// The method that will be used to send this content type. 22 | public var method: String = "sendAudio" 23 | 24 | // FILE SOURCE 25 | public var fileID: String? 26 | public var url: String? 27 | 28 | // PARAMETERS 29 | /// Duration of the audio in seconds as defined by the sender. 30 | public var duration: Int? 31 | 32 | /// Performer of the audio as defined by the sender or by audio tags 33 | public var performer: String? 34 | 35 | /// Title of the audio as defined by the sender or by audio tags. 36 | public var title: String? 37 | 38 | /// MIME type of the file as defined by the sender. 39 | public var mimeType: String? 40 | 41 | /// File size of the audio file. Only specify if you reeeeally have to. 42 | public var fileSize: Int? 43 | 44 | /// Coding keys to map values when Encoding and Decoding. 45 | enum CodingKeys: String, CodingKey { 46 | case fileID = "file_id" 47 | case url 48 | 49 | case duration 50 | case performer 51 | case title 52 | case mimeType = "mime_type" 53 | case fileSize = "file_size" 54 | } 55 | 56 | /** 57 | Initialises an Audio object based on a Telegram File ID and any optional parameters. 58 | */ 59 | public init(fileID: String, 60 | duration: Int = 0, 61 | performer: String? = nil, 62 | title: String? = nil, 63 | mimeType: String? = nil, 64 | fileSize: Int? = 0) { 65 | 66 | self.fileID = fileID 67 | self.duration = duration 68 | self.performer = performer 69 | self.title = title 70 | self.mimeType = mimeType 71 | self.fileSize = fileSize 72 | } 73 | 74 | /** 75 | Initialises an Audio object based on a URL and any optional parameters. The URL can be defined either as a local source relative to the Public/ folder 76 | of your app (eg. `karaoke/jack-1.png`) or as a remote source using an HTTP link. 77 | 78 | - warning: The URL must link to a MP3 formatted file, this is the only file type that can be sent using this type. 79 | */ 80 | public init?(url: String, 81 | duration: Int = 0, 82 | performer: String? = nil, 83 | title: String? = nil, 84 | mimeType: String? = nil, 85 | fileSize: Int? = 0) { 86 | 87 | if url.checkURLValidity(acceptedExtensions: ["mp3"]) == false { return nil } 88 | 89 | self.url = url 90 | self.duration = duration 91 | self.performer = performer 92 | self.title = title 93 | self.mimeType = mimeType 94 | self.fileSize = fileSize 95 | } 96 | 97 | // SendType conforming methods 98 | public func getQuery() -> [String: Codable] { 99 | var keys: [String: Codable] = [ 100 | "audio": fileID] 101 | 102 | if duration != 0 { keys["duration"] = duration } 103 | if performer != nil { keys["performer"] = performer } 104 | if title != nil { keys["title"] = title } 105 | 106 | return keys 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Request/Request+Chat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Get.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 10/07/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /** 13 | Adds an extension that deals in fetching information from Telegram. 14 | */ 15 | extension TelegramRequest { 16 | 17 | /* Use this method for your bot to leave a group, supergroup or channel. Returns True on success. */ 18 | public func leaveChat(chatID: String, userID: String) { 19 | 20 | query = [ 21 | "chat_id": Int(chatID), 22 | "user_id": Int(userID), 23 | ] 24 | 25 | // Set the Request, Method and Content 26 | method = "leaveChat" 27 | 28 | } 29 | 30 | 31 | /** 32 | Builds and returns a TelegramRequest for the API method with the given arguments. 33 | 34 | ## API Description 35 | Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, 36 | current username of a user, group or channel, etc.). Returns a Chat object on success. 37 | */ 38 | public static func getChat(chatID: String) -> TelegramRequest { 39 | 40 | let request = TelegramRequest() 41 | 42 | request.query = [ 43 | "chat_id": Int(chatID), 44 | ] 45 | 46 | // Set the Request, Method and Content 47 | request.method = "getChat" 48 | 49 | return request 50 | 51 | } 52 | 53 | 54 | /** 55 | Builds and returns a TelegramRequest for the API method with the given arguments. 56 | 57 | ## API Description 58 | Use this method to get a list of administrators in a chat. On success, returns an Array of ChatMember objects that 59 | contains information about all chat administrators except other bots. If the chat is a group or a supergroup and 60 | no administrators were appointed, only the creator will be returned. 61 | */ 62 | public static func getChatAdministrators(chatID: String) -> TelegramRequest { 63 | 64 | let request = TelegramRequest() 65 | 66 | request.query = [ 67 | "chat_id": Int(chatID), 68 | ] 69 | 70 | // Set the Request, Method and Content 71 | request.method = "getChatAdministrators" 72 | 73 | return request 74 | 75 | } 76 | 77 | 78 | /** 79 | Builds and returns a TelegramRequest for the API method with the given arguments. 80 | 81 | ## API Description 82 | Use this method to get the number of members in a chat. Returns Int on success. 83 | */ 84 | public static func getChatMemberCount(chatID: String) -> TelegramRequest { 85 | 86 | let request = TelegramRequest() 87 | 88 | request.query = [ 89 | "chat_id": Int(chatID), 90 | ] 91 | 92 | // Set the Request, Method and Content 93 | request.method = "getChatMembersCount" 94 | 95 | return request 96 | 97 | } 98 | 99 | 100 | /** 101 | Builds and returns a TelegramRequest for the API method with the given arguments. 102 | 103 | ## API Description 104 | Use this method to get information about a member of a chat. Returns a ChatMember object on success. 105 | */ 106 | public static func getChatMember(chatID: String, userID: String) -> TelegramRequest { 107 | 108 | let request = TelegramRequest() 109 | 110 | request.query = [ 111 | "chat_id": Int(chatID), 112 | "user_id": Int(userID), 113 | ] 114 | 115 | // Set the Request, Method and Content 116 | request.method = "getChatMember" 117 | 118 | return request 119 | 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Request/Request+Answer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Answer.swift 3 | // PelicanTests 4 | // 5 | // Created by Takanu Kyriako on 12/07/2017. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Adds an extension that deals in answering queries sent by a user in various contexts. 13 | */ 14 | extension TelegramRequest { 15 | 16 | /** 17 | Builds and returns a TelegramRequest for the API method with the given arguments. 18 | 19 | ## API Description 20 | Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. 21 | 22 | Alternatively, the user can be redirected to the specified Game URL. For this option to work, you must first create a game for your bot via BotFather and accept the terms. Otherwise, you may use links like t.me/your_bot start=XXXX that open your bot with a parameter. 23 | */ 24 | public static func answerCallbackQuery(queryID: String, 25 | text: String?, 26 | showAlert: Bool, 27 | url: String?, 28 | cacheTime: Int = 0) -> TelegramRequest { 29 | 30 | let request = TelegramRequest() 31 | 32 | request.query = [ 33 | "callback_query_id": queryID 34 | ] 35 | 36 | if text != nil { request.query["text"] = text! } 37 | if showAlert == true { request.query["show_alert"] = showAlert } 38 | if url != nil { request.query["url"] = url! } 39 | if cacheTime != 0 { request.query["cache_time"] = cacheTime } 40 | 41 | 42 | // Set the query 43 | request.method = "answerCallbackQuery" 44 | request.content = text as Any 45 | 46 | return request 47 | } 48 | 49 | /** 50 | Builds and returns a TelegramRequest for the API method with the given arguments. 51 | 52 | ## API Description 53 | Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. 54 | 55 | Use this method to send answers to an inline query. On success, True is returned. 56 | No more than 50 results per query are allowed. 57 | */ 58 | public static func answerInlineQuery(queryID: String, 59 | results: [InlineResult], 60 | cacheTime: Int = 0, 61 | isPersonal: Bool = false, 62 | nextOffset: String?, 63 | switchPM: String?, 64 | switchPMParam: String?) -> TelegramRequest? { 65 | 66 | let request = TelegramRequest() 67 | 68 | request.query = [ 69 | "inline_query_id": queryID 70 | ] 71 | 72 | let resultAny: [InlineResultAny] = results.map {T in return InlineResultAny(T) } 73 | request.query["results"] = TelegramRequest.encodeDataToUTF8(resultAny) 74 | 75 | // Check whether any other query needs to be added 76 | if cacheTime != 300 { request.query["cache_time"] = cacheTime } 77 | if isPersonal != false { request.query["is_personal"] = isPersonal } 78 | if nextOffset != nil { request.query["next_offset"] = nextOffset } 79 | if switchPM != "" { request.query["switch_pm_text"] = switchPM } 80 | if switchPMParam != "" { request.query["switch_pm_parameter"] = switchPMParam } 81 | 82 | 83 | // Set the query 84 | request.method = "answerInlineQuery" 85 | request.content = results as Any 86 | 87 | return request 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sources/Pelican/API/Types/Inline/InlineQueryResult/InlineResultArticle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineResultArticle.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 19/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Represents a link to an article or web page. This type is also ideal for setting the contents to a simple message by using the `content` property, for sending messages to a chat. 12 | */ 13 | public struct InlineResultArticle: InlineResult { 14 | 15 | /// A metatype, used to Encode and Decode itself as part of the InlineResult protocol. 16 | public var metatype: InlineResultType = .article 17 | 18 | /// Type of the result being given. 19 | public var type: String = "article" 20 | 21 | /// Unique Identifier for the result, 1-64 bytes. 22 | public var tgID: String 23 | 24 | /// Content of the message to be sent. 25 | public var content: InputMessageContent? 26 | 27 | /// Inline keyboard attached to the message 28 | public var markup: MarkupInline? 29 | 30 | 31 | /// URL of the result. 32 | public var url: String? 33 | 34 | /// The title of the inline result. 35 | public var title: String 36 | 37 | /// The description of the inline result. 38 | public var description: String? 39 | 40 | /// Set as true if you don't want the URL to be shown in the message. 41 | public var hideURL: Bool? 42 | 43 | 44 | /// URL of the thumbnailnail to use for the result. 45 | public var thumbnailURL: String? 46 | 47 | /// Thumbnail width. 48 | public var thumbnailWidth: Int? 49 | 50 | /// Thumbnail height. 51 | public var thumbnailHeight: Int? 52 | 53 | 54 | /// Coding keys to map values when Encoding and Decoding. 55 | enum CodingKeys: String, CodingKey { 56 | case type 57 | case tgID = "id" 58 | case content = "input_message_content" 59 | case markup = "reply_markup" 60 | 61 | case url 62 | case title 63 | case description 64 | case hideURL = "hide_url" 65 | 66 | case thumbnailURL = "thumb_url" 67 | case thumbnailWidth = "thumb_width" 68 | case thumbnailHeight = "thumb_height" 69 | } 70 | 71 | /** 72 | Initialises a InlineResultArticle type for the explicit purpose of sending a text message, rather than a link. 73 | */ 74 | public init(id: String, title: String, description: String, contents: String, markup: MarkupInline?) { 75 | self.tgID = id 76 | self.title = title 77 | self.content = InputMessageContent(content: InputMessageContent_Text(text: contents, 78 | parseMode: "", 79 | disableWebPreview: nil 80 | )) 81 | 82 | self.markup = markup 83 | self.description = description 84 | } 85 | 86 | /** 87 | Initialises an InlineResultArticle type to provide a link to an article or web page. 88 | */ 89 | public init(id: String, 90 | title: String, 91 | description: String, 92 | url: String, 93 | markup: MarkupInline?, 94 | hideURL: Bool?, 95 | thumbnailURL: String?, 96 | thumbnailWidth: Int?, 97 | thumbnailHeight: Int?) { 98 | 99 | self.tgID = id 100 | self.content = nil 101 | self.title = title 102 | self.description = description 103 | self.markup = markup 104 | self.url = url 105 | 106 | self.hideURL = hideURL 107 | self.thumbnailURL = thumbnailURL 108 | self.thumbnailWidth = thumbnailWidth 109 | self.thumbnailHeight = thumbnailHeight 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Sync/Sync+Edit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequest+Edit.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 16/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MethodRequestSync { 11 | 12 | /** 13 | Edits a text based message. 14 | */ 15 | @discardableResult 16 | public func editMessage(_ message: String, 17 | messageID: Int?, 18 | inlineMessageID: Int?, 19 | markup: MarkupType? = nil, 20 | chatID: String, 21 | parseMode: MessageParseMode = .markdown, 22 | disableWebPreview: Bool = false) -> Bool { 23 | 24 | let request = TelegramRequest.editMessageText(chatID: chatID, 25 | messageID: messageID, 26 | inlineMessageID: inlineMessageID, 27 | text: message, 28 | markup: markup, 29 | parseMode: parseMode, 30 | disableWebPreview: disableWebPreview) 31 | 32 | let response = tag.sendSyncRequest(request) 33 | if response == nil { return false } 34 | 35 | if response!.result?["chat"] != nil { return true } 36 | else { return MethodRequest.decodeResponse(response) ?? false } 37 | } 38 | 39 | /** 40 | Edits the caption on a media/file based message. 41 | */ 42 | @discardableResult 43 | public func editCaption(messageID: Int = 0, 44 | caption: String, 45 | markup: MarkupType? = nil, 46 | chatID: String) -> Bool { 47 | 48 | let request = TelegramRequest.editMessageCaption(chatID: chatID, 49 | messageID: messageID, 50 | caption: caption, 51 | markup: markup) 52 | let response = tag.sendSyncRequest(request) 53 | if response == nil { return false } 54 | 55 | if response!.result?["chat"] != nil { return true } 56 | else { return MethodRequest.decodeResponse(response) ?? false } 57 | } 58 | 59 | /** 60 | Edits the inline markup options assigned to any type of message. 61 | */ 62 | @discardableResult 63 | public func editReplyMarkup(_ markup: MarkupType, 64 | messageID: Int = 0, 65 | inlineMessageID: Int = 0, 66 | chatID: String) -> Bool { 67 | 68 | let request = TelegramRequest.editMessageReplyMarkup(chatID: chatID, messageID: messageID, inlineMessageID: inlineMessageID, markup: markup) 69 | let response = tag.sendSyncRequest(request) 70 | if response == nil { return false } 71 | 72 | if response!.result?["chat"] != nil { return true } 73 | else { return MethodRequest.decodeResponse(response) ?? false } 74 | } 75 | 76 | /** 77 | Deletes a message the bot has made using it's message ID. This method has the following limitations: 78 | - A message can only be deleted if it was sent less than 48 hours ago. 79 | - Bots can delete outgoing messages in groups and supergroups. 80 | - Bots granted can_post_messages permissions can delete outgoing messages in channels. 81 | - If the bot is an administrator of a group, it can delete any message there. 82 | - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. 83 | */ 84 | @discardableResult 85 | public func deleteMessage(_ messageID: Int, chatID: String) -> Bool { 86 | 87 | let request = TelegramRequest.deleteMessage(chatID: chatID, messageID: messageID) 88 | let response = tag.sendSyncRequest(request) 89 | return MethodRequest.decodeResponse(response) ?? false 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/Monitor/FloodMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flood.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 20/08/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /** 12 | Used for monitoring and controlling updates sent to a session by a user. 13 | */ 14 | public class FloodMonitor { 15 | 16 | /// The currently active set of limits. 17 | var limits: [FloodLimit] = [] 18 | 19 | init() { } 20 | 21 | /** 22 | Adds a new flood monitor to the class. If the criteria for the flood monitor you're attempting to add match a 23 | monitor thats already being used, a new monitor will not be created. 24 | */ 25 | public func add(type: [UpdateType], hits: Int, duration: Duration, action: @escaping () -> ()) { 26 | 27 | let limit = FloodLimit(type: type, hits: hits, duration: duration, action: action) 28 | if limits.contains(limit) { return } 29 | 30 | limits.append(limit) 31 | 32 | } 33 | 34 | /** 35 | Passes the update to all available monitors. 36 | */ 37 | public func handle(_ update: Update) { 38 | 39 | limits.forEach( { $0.bump(update) } ) 40 | } 41 | 42 | /** 43 | Clears all flood monitors currently being stored. 44 | */ 45 | public func clearAll() { 46 | limits.removeAll() 47 | } 48 | } 49 | 50 | /** 51 | Defines a set of rules that a FloodLimit class can check for. 52 | */ 53 | public class FloodLimit: Equatable { 54 | 55 | // CRITERIA 56 | /// The type of update to look for when incrementing the hit counter. 57 | var type: [UpdateType] = [] 58 | 59 | /// The number of hits currently received within the flood monitor's `duration`. 60 | var hits: Int 61 | 62 | /// The length of time the flood monitor range lasts for. 63 | var duration: Duration 64 | 65 | /// The action to be triggered should the monitor 66 | var action: () -> () 67 | 68 | // CURRENT STATE 69 | var currentTime: Date = Date() 70 | var currentHits: Int = 0 71 | var actionExecuted: Bool = false 72 | 73 | 74 | /** 75 | Creates a new FloodMonitor type. 76 | */ 77 | init(type: [UpdateType], hits: Int, duration: Duration, action: @escaping () -> ()) { 78 | 79 | self.type = type 80 | self.hits = hits 81 | self.duration = duration 82 | self.action = action 83 | } 84 | 85 | /** 86 | Uses an update to attempt bumping the hits that have occurred on the monitor, in an attempt to trigger 87 | the action belonging to it. 88 | */ 89 | public func bump(_ update: Update) { 90 | 91 | // Check that we can actually bump the monitor 92 | if self.type.contains(update.type) == false { return } 93 | 94 | // If so, check the times and hits 95 | if Date().timeIntervalSince1970 > (currentTime.timeIntervalSince1970 + duration.rawValue) { 96 | 97 | resetTime() 98 | return 99 | } 100 | 101 | // Otherwise bump the hits and check if we've hit the requirement for the action to be triggered. 102 | currentHits += 1 103 | 104 | if currentHits >= hits && actionExecuted == false { 105 | action() 106 | actionExecuted = true 107 | } 108 | } 109 | 110 | /** 111 | Resets all properties related to measuring hits and action triggering. 112 | */ 113 | public func resetTime() { 114 | 115 | currentTime = Date() 116 | currentHits = 0 117 | actionExecuted = false 118 | } 119 | 120 | 121 | public static func ==(lhs: FloodLimit, rhs: FloodLimit) -> Bool { 122 | 123 | if lhs.type == rhs.type && 124 | lhs.hits == rhs.hits && 125 | lhs.duration == rhs.duration { return true } 126 | 127 | return false 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Sync/Sync+Stickers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodRequest+Stickers.swift 3 | // Pelican 4 | // 5 | // Created by Takanu Kyriako on 17/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | This extension handles any kinds of operations involving stickers (including setting group sticker packs). 12 | */ 13 | extension MethodRequestSync { 14 | 15 | /** 16 | Returns a StickerSet type for the name of the sticker set given, if successful. 17 | */ 18 | public func getStickerSet(_ name: String) -> StickerSet? { 19 | 20 | let request = TelegramRequest.getStickerSet(name: name) 21 | let response = tag.sendSyncRequest(request) 22 | return MethodRequest.decodeResponse(response) 23 | } 24 | 25 | /** 26 | Use this method to upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet 27 | methods (can be used multiple times). 28 | */ 29 | public func uploadStickerFile(_ sticker: Sticker, userID: String) -> FileDownload? { 30 | 31 | guard let request = TelegramRequest.uploadStickerFile(userID: userID, sticker: sticker) else { 32 | PLog.error("Can't create uploadStickerFile request.") 33 | return nil 34 | } 35 | 36 | let response = tag.sendSyncRequest(request) 37 | return MethodRequest.decodeResponse(response) 38 | } 39 | 40 | /** 41 | Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. 42 | */ 43 | @discardableResult 44 | public func createNewStickerSet(userID: String, 45 | name: String, 46 | title: String, 47 | sticker: Sticker, 48 | emojis: String, 49 | containsMasks: Bool? = nil, 50 | maskPosition: MaskPosition? = nil) -> Bool { 51 | 52 | let request = TelegramRequest.createNewStickerSet(userID: userID, 53 | name: name, 54 | title: title, 55 | sticker: sticker, 56 | emojis: emojis, 57 | containsMasks: containsMasks, 58 | maskPosition: maskPosition) 59 | let response = tag.sendSyncRequest(request) 60 | return MethodRequest.decodeResponse(response) ?? false 61 | } 62 | 63 | /** 64 | Adds a sticker to a sticker set created by the bot. 65 | */ 66 | @discardableResult 67 | public func addStickerToSet(userID: String, 68 | name: String, 69 | pngSticker: Sticker, 70 | emojis: String, 71 | maskPosition: MaskPosition? = nil) -> Bool { 72 | 73 | let request = TelegramRequest.addStickerToSet(userID: userID, name: name, pngSticker: pngSticker, emojis: emojis, maskPosition: maskPosition) 74 | let response = tag.sendSyncRequest(request) 75 | return MethodRequest.decodeResponse(response) ?? false 76 | } 77 | 78 | /** 79 | Use this method to move a sticker in a set created by the bot to a specific position. 80 | */ 81 | @discardableResult 82 | public func setStickerPositionInSet(stickerID: String, newPosition: Int) -> Bool { 83 | 84 | let request = TelegramRequest.setStickerPositionInSet(stickerID: stickerID, position: newPosition) 85 | let response = tag.sendSyncRequest(request) 86 | return MethodRequest.decodeResponse(response) ?? false 87 | } 88 | 89 | /** 90 | Use this method to delete a sticker from a set created by the bot. 91 | */ 92 | @discardableResult 93 | public func deleteStickerFromSet(stickerID: String) -> Bool { 94 | 95 | let request = TelegramRequest.deleteStickerFromSet(stickerID: stickerID) 96 | let response = tag.sendSyncRequest(request) 97 | return MethodRequest.decodeResponse(response) ?? false 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Pelican/Session/Modules/API Request/Async/Async+Edit.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | 5 | extension MethodRequestAsync { 6 | 7 | /** 8 | Edits a text based message. 9 | */ 10 | public func editMessage(_ message: String, 11 | messageID: Int?, 12 | inlineMessageID: Int?, 13 | markup: MarkupType? = nil, 14 | chatID: String, 15 | parseMode: MessageParseMode = .markdown, 16 | disableWebPreview: Bool = false, 17 | callback: CallbackBoolean) { 18 | 19 | let request = TelegramRequest.editMessageText(chatID: chatID, 20 | messageID: messageID, 21 | inlineMessageID: inlineMessageID, 22 | text: message, 23 | markup: markup, 24 | parseMode: parseMode, 25 | disableWebPreview: disableWebPreview) 26 | 27 | tag.sendAsyncRequest(request) { response in 28 | 29 | if callback != nil { 30 | var result = false 31 | if response!.result?["chat"] != nil { result = true } 32 | else { result = MethodRequest.decodeResponse(response) ?? false } 33 | 34 | callback!(result) 35 | } 36 | } 37 | } 38 | 39 | /** 40 | Edits the caption on a media/file based message. 41 | */ 42 | public func editCaption(messageID: Int = 0, 43 | caption: String, 44 | markup: MarkupType? = nil, 45 | chatID: String, 46 | callback: CallbackBoolean) { 47 | 48 | let request = TelegramRequest.editMessageCaption(chatID: chatID, 49 | messageID: messageID, 50 | caption: caption, 51 | markup: markup) 52 | 53 | tag.sendAsyncRequest(request) { response in 54 | 55 | if callback != nil { 56 | var result = false 57 | if response!.result?["chat"] != nil { result = true } 58 | else { result = MethodRequest.decodeResponse(response) ?? false } 59 | 60 | callback!(result) 61 | } 62 | } 63 | } 64 | 65 | /** 66 | Edits the inline markup options assigned to any type of message. 67 | */ 68 | public func editReplyMarkup(_ markup: MarkupType?, 69 | messageID: Int = 0, 70 | inlineMessageID: Int = 0, 71 | chatID: String, 72 | callback: CallbackBoolean) { 73 | 74 | let request = TelegramRequest.editMessageReplyMarkup(chatID: chatID, messageID: messageID, inlineMessageID: inlineMessageID, markup: markup) 75 | tag.sendAsyncRequest(request) { response in 76 | 77 | if callback != nil { 78 | var result = false 79 | if response!.result?["chat"] != nil { result = true } 80 | else { result = MethodRequest.decodeResponse(response) ?? false } 81 | 82 | callback!(result) 83 | } 84 | } 85 | } 86 | 87 | /** 88 | Deletes a message the bot has made using it's message ID. This method has the following limitations: 89 | - A message can only be deleted if it was sent less than 48 hours ago. 90 | - Bots can delete outgoing messages in groups and supergroups. 91 | - Bots granted can_post_messages permissions can delete outgoing messages in channels. 92 | - If the bot is an administrator of a group, it can delete any message there. 93 | - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. 94 | */ 95 | public func deleteMessage(_ messageID: Int, chatID: String, callback: CallbackBoolean) { 96 | 97 | let request = TelegramRequest.deleteMessage(chatID: chatID, messageID: messageID) 98 | tag.sendAsyncRequest(request) { response in 99 | 100 | if callback != nil { 101 | callback!(MethodRequest.decodeResponse(response) ?? false) 102 | } 103 | } 104 | } 105 | 106 | } 107 | --------------------------------------------------------------------------------