diff --git a/.travis.yml b/.travis.yml index 6c833a8..edb7420 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,9 @@ services: - docker script: - - sudo sh ./scripts/build.sh + - sudo bash ./docker/test.sh + +notifications: + email: false + webhooks: + secure: qBLY9VOBi/W5Mf0pgLd7h79d0g+/4qRu5ClEd/C7zSLSx9XsPbC7vYZ8CALZNnR8xCrfZj1osTWcY4O/g9Wso1iWt7k9ekdQi+ze36LgxYk4ouW2p3BbxUlIgSJ38AvRCxXrb457/LK/ddi/E3/433APE78VSQU6ZMawDmdYe8L20kIhodXgJxsxMjVt+nhTjktMCRCgrXaOUz4UtfNSwiIP1Woo4L7xAnuFdhy1XmhOV1ARP8Z+smgRia00JsOBD6P+S5DLguOQ8Ri1glSer7bAJzD9eYA4pc+C/5ZvgJ8sqH8Le/2LR8XFhH6Pg+d3r+6VPfOXc20sXmRXgFT8SUQCCnzQ4pqsC4vRijsD1ZLkHMCYw3lbF4u4JTwYquDmwb9CRvPNA9bFepbMbVR65g8ox9RzAQu3Z9oXrtI/IDRMEjNP3mgHopt6IzqdHvcmHxnhYiu5GH9fw1ZQLC/wSsecH6hI6dKJ6zQwnjtyJIFkfMOetknmvAFlMo/HN3YH8NBCeGsvFKPJ698fG1gOWuBsIdMz/LqavmSSBKD05J47OnxMlI41Pf1uxdo5Q0jAzMlJaC1IFc4bawN23N1wtw/XClA32XPxsirZWKfrOKo1jPYw5R5DP7BcwCP+Y8pEEQqqbeML5gqhXKmsUV9ilHqD5s9Lju3E+Ig7k/RUKws= diff --git a/Example/main.swift b/Example/main.swift new file mode 100644 index 0000000..578435e --- /dev/null +++ b/Example/main.swift @@ -0,0 +1,20 @@ +// +// main.swift +// ZEGBotExample +// +// Created by Shane Qi on 9/28/17. +// + +import ZEGBot +import Foundation + +let bot = ZEGBot(token: "TYPE YOUR TOKEN HERE") + +bot.run { updateResult, bot in + switch updateResult { + case .success(let update): + dump(update) + case .failure(let error): + dump(error) + } +} diff --git a/Package.pins b/Package.pins deleted file mode 100644 index 74ebfee..0000000 --- a/Package.pins +++ /dev/null @@ -1,12 +0,0 @@ -{ - "autoPin": true, - "pins": [ - { - "package": "SwiftyJSON", - "reason": null, - "repositoryURL": "https://github.com/IBM-Swift/SwiftyJSON.git", - "version": "15.0.6" - } - ], - "version": 1 -} \ No newline at end of file diff --git a/Package.swift b/Package.swift index a560733..e679256 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,29 @@ +// swift-tools-version:4.0 + import PackageDescription let package = Package( name: "ZEGBot", - dependencies: [ - .Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", majorVersion: 15) - ] + products: [ + .library( + name: "ZEGBot", + targets: ["ZEGBot"]), + .executable( + name: "ZEGBotExample", + targets: ["ZEGBotExample"]) + ], + dependencies: [], + targets: [ + .target( + name: "ZEGBot", + dependencies: [], + path: "./Sources"), + .target( + name: "ZEGBotExample", + dependencies: ["ZEGBot"], + path: "./Example"), + .testTarget( + name: "ZEGBotTests", + dependencies: ["ZEGBot"]), + ] ) diff --git a/README.md b/README.md index 0cceafe..6a88069 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ZEGBot -[![Build Status](https://travis-ci.org/ShaneQi/ZEGBot.svg?branch=master)](https://travis-ci.org/ShaneQi/ZEGBot) ![Swift Version](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat) ![Platforms](https://img.shields.io/badge/Platforms-OS%20X%20%7C%20Linux%20-lightgray.svg?style=flat) ![License](https://img.shields.io/badge/License-Apache-lightgrey.svg?style=flat) +[![Build Status](https://travis-ci.org/ShaneQi/ZEGBot.svg?branch=master)](https://travis-ci.org/ShaneQi/ZEGBot) [![Swift Version](https://img.shields.io/badge/Swift-4.0-orange.svg?style=flat)](https://swift.org) ![Platforms](https://img.shields.io/badge/Platforms-OS%20X%20%7C%20Linux%20-blue.svg?style=flat) ![License](https://img.shields.io/badge/License-Apache-red.svg?style=flat) This library wraps the JSON decoding processing, making it easy to decode incoming JSON String to manipulatable objects. @@ -11,11 +11,11 @@ This library wraps the processing of converting objects to Telegram Bot API requ Add this project as a dependency in your Package.swift file. ```swift -.Package(url: "https://github.com/shaneqi/ZEGBot.git", majorVersion: 2) +.package(url: "https://github.com/shaneqi/ZEGBot.git", from: Version(4, 0, 0)) ``` ## Quick Start -[ZEGBot-Template](https://github.com/ShaneQi/ZEGBot-Template) - an empty starter project. +Checkout the example here: [./Example](https://github.com/ShaneQi/ZEGBot/tree/master/Example). Or you can just put the following code into `main.swift` of your project. diff --git a/Sources/Converting.swift b/Sources/Converting.swift deleted file mode 100644 index d22bca1..0000000 --- a/Sources/Converting.swift +++ /dev/null @@ -1,391 +0,0 @@ -// -// Converting.swift -// ZEGBot -// -// Created by Shane Qi on 5/30/16. -// Copyright © 2016 com.github.shaneqi. All rights reserved. -// -// Licensed under Apache License v2.0 -// - -import SwiftyJSON - -protocol JSONConvertible { - - init?(from json: JSON) - -} - -protocol ArrayConvertible { - - static func array(from json: JSON) -> [Self]? - -} - -extension ArrayConvertible where Self: JSONConvertible { - - internal static func array(from json: JSON) -> [Self]? { - - guard !json.isEmpty else { return nil } - - guard let jsonArray = json.array else { - Log.warning(on: json) - return nil - } - - return jsonArray.map({ Self(from: $0) }).flatMap({ $0 }) - - } - -} - -extension Update: JSONConvertible, ArrayConvertible { - - internal init?(from json: JSON) { - guard let updateId = json[PARAM.UPDATE_ID].int else { - Log.warning(on: json) - return nil - } - self.updateId = updateId - self.message = Message(from: json[PARAM.MESSAGE]) - self.editedMessage = Message(from: json[PARAM.EDITED_MESSAGE]) - self.channelPost = Message(from: json[PARAM.CHANNEL_POST]) - } - -} - -extension Message { - - internal convenience init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - self.init() - - guard let messageId = json[PARAM.MESSAGE_ID].int, - let date = json[PARAM.DATE].int, - let chat = Chat(from: json[PARAM.CHAT]) else { - Log.warning(on: json) - return nil - } - - self.messageId = messageId - self.date = date - self.chat = chat - self.from = User(from: json[PARAM.FROM]) - self.forwardFrom = User(from: json[PARAM.FORWARD_FROM]) - self.forwardFromChat = Chat(from: json[PARAM.FORWARD_FROM_CHAT]) - self.forwardDate = json[PARAM.FORWARD_DATE].int - self.replyToMessage = Message(from: json[PARAM.REPLY_TO_MESSAGE]) - self.editDate = json[PARAM.EDIT_DATE].int - self.text = json[PARAM.TEXT].string - self.entities = MessageEntity.array(from: json[PARAM.ENTITIES]) - self.audio = Audio(from: json[PARAM.AUDIO]) - self.document = Document(from: json[PARAM.DOCUMENT]) - self.photo = PhotoSize.array(from: json[PARAM.PHOTO]) - self.sticker = Sticker(from: json[PARAM.STICKER]) - self.video = Video(from: json[PARAM.VIDEO]) - self.voice = Voice(from: json[PARAM.VOICE]) - self.caption = json[PARAM.CAPTION].string - self.contact = Contact(from: json[PARAM.CONTACT]) - self.location = Location(from: json[PARAM.LOCATION]) - self.venue = Venue(from: json[PARAM.VENUE]) - self.newChatMember = User(from: json[PARAM.NEW_CHAT_MEMBER]) - self.leftChatMember = User(from: json[PARAM.LEFT_CHAT_MEMBER]) - self.newChatTitle = json[PARAM.NEW_CHAT_TITLE].string - self.newChatPhoto = PhotoSize.array(from: json[PARAM.NEW_CHAT_PHOTO]) - self.deleteChatPhoto = json[PARAM.DELETE_CHAT_PHOTO].bool - self.groupChatCreated = json[PARAM.GROUP_CHAT_CREATED].bool - self.supergroupChatCreated = json[PARAM.SUPER_GROUP_CHAT_CREATED].bool - self.channelChatCreated = json[PARAM.CHANNEL_CHAT_CREATED].bool - self.migrateToChatId = json[PARAM.MIGRATE_TO_CHAT_ID].int - self.migrateFromChatId = json[PARAM.MIGRATE_FROM_CHAT_ID].int - self.pinnedMessage = Message(from: json[PARAM.PINNED_MESSAGE]) - - } - -} - -extension Chat: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let id = json[PARAM.ID].int, - let type = Chat.StructType(from: json[PARAM.TYPE].string) else { - Log.warning(on: json) - return nil - } - - self.id = id - self.type = type - self.title = json[PARAM.TITLE].string - self.username = json[PARAM.USERNAME].string - self.firstName = json[PARAM.FIRST_NAME].string - self.lastName = json[PARAM.LAST_NAME].string - - } - -} - -extension User: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let id = json[PARAM.ID].int, - let firstName = json[PARAM.FIRST_NAME].string else { - Log.warning(on: json) - return nil - } - - self.id = id - self.firstName = firstName - self.lastName = json[PARAM.LAST_NAME].string - self.username = json[PARAM.USERNAME].string - - } - -} - -extension MessageEntity: JSONConvertible, ArrayConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let type = MessageEntity.StructType(from: json[PARAM.TYPE].string), - let offset = json[PARAM.OFFSET].int, - let length = json[PARAM.LENGTH].int else { - Log.warning(on: json) - return nil - } - - self.type = type - self.offset = offset - self.length = length - self.url = json[PARAM.URL].string - self.user = User(from: json[PARAM.USER]) - } - -} - -extension Audio: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let duration = json[PARAM.DURATION].int else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.duration = duration - self.performer = json[PARAM.PERFORMER].string - self.title = json[PARAM.TITLE].string - self.mimeType = json[PARAM.MIME_SIZE].string - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension Document: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.thumb = PhotoSize(from: json[PARAM.THUMB]) - self.fileName = json[PARAM.FILE_NAME].string - self.mimeType = json[PARAM.MIME_TYPE].string - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension PhotoSize: JSONConvertible, ArrayConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let width = json[PARAM.WIDTH].int, - let height = json[PARAM.HEIGHT].int else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.width = width - self.height = height - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension Sticker: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let width = json[PARAM.WIDTH].int, - let height = json[PARAM.HEIGHT].int else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.width = width - self.height = height - self.thumb = PhotoSize(from: json[PARAM.THUMB]) - self.emoji = json[PARAM.EMOJI].string - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension Video: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let width = json[PARAM.WIDTH].int, - let height = json[PARAM.HEIGHT].int, - let duration = json[PARAM.DURATION].int else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.width = width - self.height = height - self.duration = duration - self.thumb = PhotoSize(from: json[PARAM.THUMB]) - self.mimeType = json[PARAM.MIME_TYPE].string - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension Voice: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let duration = json[PARAM.DURATION].int else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.duration = duration - self.mimeType = json[PARAM.MIME_TYPE].string - self.fileSize = json[PARAM.FILE_SIZE].int - - } - -} - -extension Contact: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let phoneNumber = json[PARAM.PHONE_NUMBER].string, - let firstName = json[PARAM.FIRST_NAME].string else { - Log.warning(on: json) - return nil - } - - self.phoneNumber = phoneNumber - self.firstName = firstName - self.lastName = json[PARAM.LAST_NAME].string - self.userId = json[PARAM.USER_ID].int - - } - -} - -extension Location: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let longitude = json[PARAM.LONGITUDE].double, - let latitude = json[PARAM.LATITUDE].double else { - Log.warning(on: json) - return nil - } - - self.longitude = longitude - self.latitude = latitude - - } - -} - -extension Venue: JSONConvertible { - - internal init?(from json: JSON) { - - guard !json.isEmpty else { return nil } - - guard let location = Location(from: json[PARAM.LOCATION]), - let title = json[PARAM.TITLE].string, - let address = json[PARAM.ADDRESS].string else { - Log.warning(on: json) - return nil - } - - self.location = location - self.title = title - self.address = address - self.foursquareId = json[PARAM.FOURSQUARE_ID].string - - } - -} - -extension File: JSONConvertible { - - internal init?(from json: JSON) { - guard !json.isEmpty else { return nil } - - guard let fileId = json[PARAM.FILE_ID].string, - let fileSize = json[PARAM.FILE_SIZE].int, - let filePath = json[PARAM.FILE_PATH].string else { - Log.warning(on: json) - return nil - } - - self.fileId = fileId - self.fileSize = fileSize - self.filePath = filePath - } - -} diff --git a/Sources/Methods.swift b/Sources/Methods.swift index ab8598f..50cad5d 100644 --- a/Sources/Methods.swift +++ b/Sources/Methods.swift @@ -8,7 +8,6 @@ // Licensed under Apache License v2.0 // -import SwiftyJSON import Foundation import Dispatch @@ -18,268 +17,158 @@ extension ZEGBot { @discardableResult public func send(message text: String, to receiver: Sendable, parseMode: ParseMode? = nil, - disableWebPagePreview: Bool = false, - disableNotification: Bool = false) -> Message? { - - var payload: [String: Any] = [ - PARAM.TEXT: text - ] - - if let parseMode = parseMode { payload[PARAM.PARSE_MODE] = parseMode.rawValue } - if disableWebPagePreview { payload[PARAM.DISABLE_WEB_PAGE_PREVIEW] = true } - - if disableNotification { payload[PARAM.DISABLE_NOTIFICATION] = true } - payload.append(contentOf: receiver.receiverIdentifier) - - guard let responseJSON = perform(method: PARAM.SEND_MESSAGE, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) - + disableWebPagePreview: Bool? = nil, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .message(text: text, parseMode: parseMode, disableWebPagePreview: disableWebPagePreview), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendMessage", payload: payload) } @discardableResult public func forward(message: Message, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var payload: [String: Any] = [ - PARAM.MESSAGE_ID: message.messageId, - PARAM.FROM_CHAT_ID: message.chat.id - ] - - if disableNotification { payload[PARAM.DISABLE_NOTIFICATION] = true } - payload.append(contentOf: receiver.receiverIdentifier) - - guard let responseJSON = perform(method: PARAM.FORWARD_MESSAGE, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .forwardMessage(chatId: message.chatId, messageId: message.messageId), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "forwardMessage", payload: payload) } @discardableResult - public func send(photo: PhotoSize, to receiver: Sendable, - disableNotification: Bool = false, - caption: String? = nil) -> Message? { - - var options = [String: Any]() - - options[PARAM.CAPTION] = caption - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: photo, to: receiver, options: options) - + public func send(sticker fileId: String, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .sticker(fileId: fileId), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendSticker", payload: payload) } @discardableResult - public func send(audio: Audio, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var options = [String: Any]() - - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: audio, to: receiver, options: options) - + public func send(photo fileId: String, caption: String? = nil, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .photo(fileId: fileId, caption: caption), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendPhoto", payload: payload) } @discardableResult - public func send(document: Document, to receiver: Sendable, - disableNotification: Bool = false, - caption: String? = nil) -> Message? { - - var options = [String: Any]() - - options[PARAM.CAPTION] = caption - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: document, to: receiver, options: options) - + public func send(audio fileId: String, caption: String? = nil, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .audio(fileId: fileId, caption: caption), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendAudio", payload: payload) } @discardableResult - public func send(sticker: Sticker, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var options = [String: Any]() - - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: sticker, to: receiver, options: options) - + public func send(document fileId: String, caption: String? = nil, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .document(fileId: fileId, caption: caption), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendDocument", payload: payload) } - @discardableResult - public func send(video: Video, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var options = [String: Any]() - - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: video, to: receiver, options: options) + @discardableResult + public func send(video fileId: String, caption: String? = nil, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .video(fileId: fileId, caption: caption), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendVideo", payload: payload) } @discardableResult - public func send(voice: Voice, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var options = [String: Any]() - - if disableNotification { options[PARAM.DISABLE_NOTIFICATION] = true } - - return send(contentOnServer: voice, to: receiver, options: options) - + public func send(voice fileId: String, caption: String? = nil, to receiver: Sendable, + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .voice(fileId: fileId, caption: caption), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendVoice", payload: payload) } @discardableResult public func sendLocation(latitude: Double, longitude: Double, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var payload: [String: Any] = [ - PARAM.LATITUDE: latitude, - PARAM.LONGITUDE: longitude - ] - - if disableNotification { payload[PARAM.DISABLE_NOTIFICATION] = true } - payload.append(contentOf: receiver.receiverIdentifier) - - guard let responseJSON = perform(method: PARAM.SEND_LOCATION, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .location(latitude: latitude, longitude: longitude), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendLocation", payload: payload) } @discardableResult public func sendVenue(latitude: Double, longitude: Double, title: String, address: String, foursquareId: String? = nil, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var payload: [String: Any] = [ - PARAM.LATITUDE: latitude, - PARAM.LONGITUDE: longitude, - PARAM.TITLE: title, - PARAM.ADDRESS: address - ] - let optionalPayload: [String: Any?] = [ - PARAM.FOURSQUARE_ID: foursquareId - ] - payload.append(contentOf: optionalPayload) - - if disableNotification { payload[PARAM.DISABLE_NOTIFICATION] = true } - payload.append(contentOf: receiver.receiverIdentifier) - - guard let responseJSON = perform(method: PARAM.SEND_VENUE, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) - + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .venue(latitude: latitude, longitude: longitude, title: title, address: address, foursquareId: foursquareId), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendVenue", payload: payload) } @discardableResult - public func sendContact(phoneNumber: String, lastName: String, firstName: String? = nil, + public func sendContact(phoneNumber: String, firstName: String, lastName: String? = nil, to receiver: Sendable, - disableNotification: Bool = false) -> Message? { - - var payload: [String: Any] = [ - PARAM.PHONE_NUMBER: phoneNumber, - PARAM.LAST_NAME: lastName - ] - let optionalPayload: [String: Any?] = [ - PARAM.FIRST_NAME: firstName - ] - payload.append(contentOf: optionalPayload) - - if disableNotification { payload[PARAM.DISABLE_NOTIFICATION] = true } - payload.append(contentOf: receiver.receiverIdentifier) - - guard let responseJSON = perform(method: PARAM.SEND_CONTACT, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) - + disableNotification: Bool? = nil) -> Result { + let payload = SendingPayload(content: .contact(phoneNumber: phoneNumber, firstName: firstName, lastName: lastName), + chatId: receiver.chatId, + replyToMessageId: receiver.replyToMessageId, + disableNotification: disableNotification) + return performRequest(ofMethod: "sendContact", payload: payload) } - public func send(chatAction: ChatAction, to receiver: Sendable) { - - var payload: [String: Any] = [ - PARAM.ACTION: chatAction.rawValue - ] - - payload.append(contentOf: receiver.receiverIdentifier) - - let _ = perform(method: PARAM.SEND_CHAT_ACTION, payload: payload) - + @discardableResult + public func send(chatAction: ChatAction, toChat chatId: Int) -> Result { + let payload = SendingPayload(content: .chatAction(chatAction: chatAction), + chatId: chatId) + return performRequest(ofMethod: "sendChatAction", payload: payload) } - public func getFile(ofId fileId: String) -> File? { - - let payload: [String: Any] = [ - PARAM.FILE_ID: fileId - ] - - guard let responseJSON = perform(method: PARAM.GET_FILE, payload: payload) else { - return nil - } - - return File(from: responseJSON[PARAM.RESULT]) - + public func getFile(ofId fileId: String) -> Result { + return performRequest(ofMethod: "getFile", payload: ["file_id": fileId]) } } extension ZEGBot { - internal func send(contentOnServer content: Identifiable, - to receiver: Sendable, - options: [String: Any]) -> Message? { - - var payload = [String: Any]() - - payload.append(contentOf: content.identifier) - payload.append(contentOf: receiver.receiverIdentifier) - payload.append(contentOf: options) - - guard let responseJSON = perform(method: content.sendingMethod, payload: payload) else { - return nil - } - - return Message(from: responseJSON[PARAM.RESULT]) - - } - - internal func perform(method: String, payload: [String: Any]) -> JSON? { - if let data: Data = performRequest(ofMethod: method, payload: payload) { - return JSON(data: data) - } - return nil - } - - private func performRequest(ofMethod method: String, payload: [String: Any]) -> Data? { - guard let bodyData = try? JSON(payload).rawData() else { - Log.warning(onMethod: method) - return nil - } - var resultData: Data? - let semaphore = DispatchSemaphore(value: 0) - var request = URLRequest(url: URL(string: urlPrefix + method)!) - request.httpMethod = "POST" - request.httpBody = bodyData - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - let task = URLSession(configuration: .default).dataTask(with: request) { data, _, _ in - resultData = data - semaphore.signal() - } - task.resume() - semaphore.wait() - return resultData + private func performRequest(ofMethod method: String, payload: Input) -> Result + where Input: Encodable, Output: Decodable { + // Preparing the request. + let bodyData = (try? JSONEncoder().encode(payload))! + let semaphore = DispatchSemaphore(value: 0) + var request = URLRequest(url: URL(string: urlPrefix + method)!) + request.httpMethod = "POST" + request.httpBody = bodyData + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + // Perform the request. + var result: Result? + let task = URLSession(configuration: .default).dataTask(with: request) { data, _, error in + if let data = data { + result = Result.decode(from: data) + } else { + result = .failure(error!) + } + semaphore.signal() + } + task.resume() + semaphore.wait() + return result! } } diff --git a/Sources/Parameters.swift b/Sources/Parameters.swift deleted file mode 100644 index 02b6b0d..0000000 --- a/Sources/Parameters.swift +++ /dev/null @@ -1,272 +0,0 @@ -// -// Parameters.swift -// ZEGBot -// -// Created by Shane Qi on 8/25/16. -// -// - -extension ZEGBot { - - - internal struct PARAM { - - /* Master param strings. */ - static let DISABLE_NOTIFICATION = "disable_notification" - static let POST_JSON_HEADER_CONTENT_TYPE = "Content-Type: application/json" - static let RESULT = "result" - - /* Shared param strings. */ - static let CAPTION = "caption" - static let MESSAGE_ID = "message_id" - static let FROM_CHAT_ID = "from_chat_id" - static let LATITUDE = "latitude" - static let LONGITUDE = "longitude" - - static let SEND_MESSAGE = "sendMessage" - static let TEXT = "text" - static let PARSE_MODE = "parse_mode" - static let DISABLE_WEB_PAGE_PREVIEW = "disable_web_page_preview" - - static let FORWARD_MESSAGE = "forwardMessage" - - static let SEND_PHOTO = "sendPhoto" - static let PHOTO = "photo" - - static let SEND_AUDIO = "sendAudio" - static let AUDIO = "audio" - - static let SEND_DOCUMENT = "sendDocument" - static let DOCUMENT = "document" - - static let SEND_STICKER = "sendSticker" - static let STICKER = "sticker" - - static let SEND_VIDEO = "sendVideo" - static let VIDEO = "video" - - static let SEND_VOICE = "sendVoice" - static let VOICE = "voice" - - static let SEND_LOCATION = "sendLocation" - - static let SEND_VENUE = "sendVenue" - static let TITLE = "title" - static let ADDRESS = "address" - static let FOURSQUARE_ID = "foursquare_id" - - static let SEND_CONTACT = "sendContact" - static let PHONE_NUMBER = "phone_number" - static let FIRST_NAME = "first_name" - static let LAST_NAME = "last_name" - - static let SEND_CHAT_ACTION = "sendChatAction" - static let ACTION = "action" - - static let GET_FILE = "getFile" - static let FILE_ID = "file_id" - - } - -} - -extension Update { - - internal struct PARAM { - static let UPDATE_ID = "update_id" - static let MESSAGE = "message" - static let EDITED_MESSAGE = "edited_message" - static let CHANNEL_POST = "channel_post" - } - -} - -extension Message { - - internal struct PARAM { - static let MESSAGE_ID = "message_id" - static let DATE = "date" - static let CHAT = "chat" - static let FROM = "from" - static let FORWARD_FROM = "forward_from" - static let FORWARD_FROM_CHAT = "forward_from_chat" - static let FORWARD_DATE = "forward_date" - static let REPLY_TO_MESSAGE = "reply_to_message" - static let EDIT_DATE = "edit_date" - static let TEXT = "text" - static let ENTITIES = "entities" - static let AUDIO = "audio" - static let DOCUMENT = "document" - static let PHOTO = "photo" - static let STICKER = "sticker" - static let VIDEO = "video" - static let VOICE = "voice" - static let CAPTION = "caption" - static let CONTACT = "contact" - static let LOCATION = "location" - static let VENUE = "venue" - static let NEW_CHAT_MEMBER = "new_chat_member" - static let LEFT_CHAT_MEMBER = "left_chat_member" - static let NEW_CHAT_TITLE = "new_chat_title" - static let NEW_CHAT_PHOTO = "new_chat_photo" - static let DELETE_CHAT_PHOTO = "delete_chat_photo" - static let GROUP_CHAT_CREATED = "group_chat_created" - static let SUPER_GROUP_CHAT_CREATED="super_group_chat_created" - static let CHANNEL_CHAT_CREATED = "channel_chat_created" - static let MIGRATE_TO_CHAT_ID = "migrate_to_chat_id" - static let MIGRATE_FROM_CHAT_ID = "migrate_from_chat_id" - static let PINNED_MESSAGE = "pinned_message" - } - -} - -extension Chat { - - internal struct PARAM { - static let ID = "id" - static let TYPE = "type" - static let TITLE = "title" - static let USERNAME = "username" - static let FIRST_NAME = "first_name" - static let LAST_NAME = "last_name" - } - -} - -extension User { - - internal struct PARAM { - static let ID = "id" - static let FIRST_NAME = "first_name" - static let LAST_NAME = "last_name" - static let USERNAME = "username" - } - -} - -extension MessageEntity { - - internal struct PARAM { - static let TYPE = "type" - static let OFFSET = "offset" - static let LENGTH = "length" - static let URL = "url" - static let USER = "user" - } - -} - -extension Audio { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let DURATION = "duration" - static let PERFORMER = "performer" - static let TITLE = "title" - static let MIME_SIZE = "mime_size" - static let FILE_SIZE = "file_size" - } - -} - -extension Document { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let THUMB = "thumb" - static let FILE_NAME = "file_name" - static let MIME_TYPE = "mime_type" - static let FILE_SIZE = "file_size" - } - -} - -extension PhotoSize { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let WIDTH = "width" - static let HEIGHT = "height" - static let FILE_SIZE = "file_size" - } - -} - -extension Sticker { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let WIDTH = "width" - static let HEIGHT = "height" - static let THUMB = "thumb" - static let EMOJI = "emoji" - static let FILE_SIZE = "file_size" - } - -} - -extension Video { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let WIDTH = "width" - static let HEIGHT = "height" - static let DURATION = "duration" - static let THUMB = "thumb" - static let MIME_TYPE = "mime_type" - static let FILE_SIZE = "file_size" - } - -} - -extension Voice { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let DURATION = "duration" - static let MIME_TYPE = "mime_type" - static let FILE_SIZE = "file_size" - } - -} - -extension Contact { - - internal struct PARAM { - static let PHONE_NUMBER = "phone_number" - static let FIRST_NAME = "first_name" - static let LAST_NAME = "last_name" - static let USER_ID = "user_id" - } - -} - -extension Location { - - internal struct PARAM { - static let LONGITUDE = "longitude" - static let LATITUDE = "latitude" - } - -} - -extension Venue { - - internal struct PARAM { - static let LOCATION = "location" - static let TITLE = "title" - static let ADDRESS = "address" - static let FOURSQUARE_ID = "foursquare_id" - } - -} - -extension File { - - internal struct PARAM { - static let FILE_ID = "file_id" - static let FILE_SIZE = "file_size" - static let FILE_PATH = "file_path" - } - -} diff --git a/Sources/Sending.swift b/Sources/Sending.swift index 18c05ed..4d47c52 100644 --- a/Sources/Sending.swift +++ b/Sources/Sending.swift @@ -6,91 +6,151 @@ // // -public protocol Sendable { - - var receiverIdentifier: [String: Any] { get } - -} - -extension Chat: Sendable { - - public var receiverIdentifier: [String: Any] { - return ["chat_id": self.id] +struct SendingPayload: Encodable { + + let content: Content + let chatId: Int + let replyToMessageId: Int? + let disableNotification: Bool? + + init(content: Content, + chatId: Int, + replyToMessageId: Int? = nil, + disableNotification: Bool? = nil) { + self.content = content + self.chatId = chatId + self.replyToMessageId = replyToMessageId + self.disableNotification = disableNotification } - -} - -extension Message: Sendable { - - public var receiverIdentifier: [String: Any] { - return ["chat_id": self.chat.id, "reply_to_message_id": self.messageId] + + enum Content { + case message(text: String, parseMode: ParseMode?, disableWebPagePreview: Bool?) + case forwardMessage(chatId: Int, messageId: Int) + case sticker(fileId: String) + case photo(fileId: String, caption: String?) + case audio(fileId: String, caption: String?) + case document(fileId: String, caption: String?) + case video(fileId: String, caption: String?) + case voice(fileId: String, caption: String?) + case videoNote(fileId: String) + case location(latitude: Double, longitude: Double) + case venue(latitude: Double, longitude: Double, title: String, address: String, foursquareId: String?) + case contact(phoneNumber: String, firstName: String, lastName: String?) + case chatAction(chatAction: ChatAction) } -} + enum CodingKeys: String, CodingKey { -public protocol Identifiable { + case chatId = "chat_id" + case replyToMessageId = "reply_to_message_id" + case disableNotification = "disable_notification" - var identifier: [String: Any] { get } - var sendingMethod: String { get } + // sendMessage + case text + case parseMode = "parse_mode" + case disableWebPagePreview = "disable_web_page_preview" -} + // forwardMessage + case fromChatId = "from_chat_id" + case messageId = "message_id" -extension PhotoSize: Identifiable { + case sticker + case caption, photo, audio, document, video, voice, location, venue, contact + case videoNote = "video_note" - public var identifier: [String: Any] { - return [ZEGBot.PARAM.PHOTO: self.fileId] - } + // sendLocation + case latitude, longitude - public var sendingMethod: String { return ZEGBot.PARAM.SEND_PHOTO } + // sendVenue + case address, title + case foursquareId = "foursquare_id" -} + // sendContact + case phoneNumber = "phone_number" + case firstName = "firstName" + case lastName = "lastName" -extension Audio: Identifiable { + // sendChatAction + case chatAction = "action" - public var identifier: [String: Any] { - return [ZEGBot.PARAM.AUDIO: self.fileId] } - public var sendingMethod: String { return ZEGBot.PARAM.SEND_AUDIO } - -} - -extension Document: Identifiable { - - public var identifier: [String: Any] { - return [ZEGBot.PARAM.DOCUMENT: self.fileId] + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(chatId, forKey: .chatId) + if let replyToMessageId = replyToMessageId { + try container.encode(replyToMessageId, forKey: .replyToMessageId) + } + if let disableNotification = disableNotification { + try container.encode(disableNotification, forKey: .disableNotification) + } + + switch content { + case .message(text: let text, parseMode: let parseMode, disableWebPagePreview: let disableWebPagePreview): + try container.encode(text, forKey: .text) + if let parseMode = parseMode { try container.encode(parseMode, forKey: .parseMode) } + if let disableWebPagePreview = disableWebPagePreview { + try container.encode(disableWebPagePreview, forKey: .disableWebPagePreview) + } + case .forwardMessage(chatId: let chatId, messageId: let messageId): + try container.encode(chatId, forKey: .fromChatId) + try container.encode(messageId, forKey: .messageId) + case .sticker(fileId: let fileId): + try container.encode(fileId, forKey: .sticker) + case .photo(fileId: let fileId, caption: let caption): + try container.encode(fileId, forKey: .photo) + if let caption = caption { try container.encode(caption, forKey: .caption) } + case .audio(fileId: let fileId, caption: let caption): + try container.encode(fileId, forKey: .audio) + if let caption = caption { try container.encode(caption, forKey: .caption) } + case .document(fileId: let fileId, caption: let caption): + try container.encode(fileId, forKey: .document) + if let caption = caption { try container.encode(caption, forKey: .caption) } + case .video(fileId: let fileId, caption: let caption): + try container.encode(fileId, forKey: .video) + if let caption = caption { try container.encode(caption, forKey: .caption) } + case .voice(fileId: let fileId, caption: let caption): + try container.encode(fileId, forKey: .voice) + if let caption = caption { try container.encode(caption, forKey: .caption) } + case .videoNote(fileId: let fileId): + try container.encode(fileId, forKey: .videoNote) + case .location(latitude: let latitude, longitude: let longitude): + try container.encode(latitude, forKey: .latitude) + try container.encode(longitude, forKey: .longitude) + case .venue(latitude: let latitude, longitude: let longitude, title: let title, address: let address, foursquareId: let foursquareId): + try container.encode(latitude, forKey: .latitude) + try container.encode(longitude, forKey: .longitude) + try container.encode(title, forKey: .title) + try container.encode(address, forKey: .address) + if let foursquareId = foursquareId { try container.encode(foursquareId, forKey: .foursquareId) } + case .contact(phoneNumber: let phoneNumber, firstName: let firstName, lastName: let lastName): + try container.encode(phoneNumber, forKey: .phoneNumber) + try container.encode(firstName, forKey: .firstName) + if let lastName = lastName { try container.encode(lastName, forKey: .lastName) } + case .chatAction(chatAction: let chatAction): + try container.encode(chatAction, forKey: .chatAction) + } } - public var sendingMethod: String { return ZEGBot.PARAM.SEND_DOCUMENT } - } -extension Sticker: Identifiable { - - public var identifier: [String: Any] { - return [ZEGBot.PARAM.STICKER: self.fileId] - } +public protocol Sendable { - public var sendingMethod: String { return ZEGBot.PARAM.SEND_STICKER } + var chatId: Int { get } + var replyToMessageId: Int? { get } } -extension Video: Identifiable { - - public var identifier: [String: Any] { - return [ZEGBot.PARAM.VIDEO: self.fileId] - } +extension Chat: Sendable { - public var sendingMethod: String { return ZEGBot.PARAM.SEND_VIDEO } + public var chatId: Int { return id } + public var replyToMessageId: Int? { return nil } } -extension Voice: Identifiable { - - public var identifier: [String: Any] { - return [ZEGBot.PARAM.VOICE: self.fileId] - } +extension Message: Sendable { - public var sendingMethod: String { return ZEGBot.PARAM.SEND_VOICE } + public var chatId: Int { return self.chat.id } + public var replyToMessageId: Int? { return messageId } } diff --git a/Sources/Types.swift b/Sources/Types.swift index 8926a57..65aa52f 100644 --- a/Sources/Types.swift +++ b/Sources/Types.swift @@ -8,246 +8,321 @@ // Licensed under Apache License v2.0 // -public struct Update { +public struct Update: Codable { - public var updateId: Int + public let updateId: Int /* Optional. */ - public var message: Message? - public var editedMessage: Message? - public var channelPost: Message? - // public var inlineQuery: InlineQuery? - // public var chosenInlineResult: ChosenInlineResult? - // public var callbackQuery: CallbackQuery? + public let message: Message? + public let editedMessage: Message? + public let channelPost: Message? + + enum CodingKeys: String, CodingKey { + case message + case updateId = "update_id" + case editedMessage = "edited_message" + case channelPost = "channel_post" + } } -public class Message { +public class Message: Codable { - public var messageId: Int - public var date: Int - public var chat: Chat + public let messageId: Int + public let date: Int + public let chat: Chat /* Optional. */ - public var from: User? - public var forwardFrom: User? - public var forwardFromChat: Chat? - public var forwardDate: Int? - public var replyToMessage: Message? - public var editDate: Int? - public var text: String? - public var entities: [MessageEntity]? - public var audio: Audio? - public var document: Document? - public var photo: [PhotoSize]? - public var sticker: Sticker? - public var video: Video? - public var voice: Voice? - public var caption: String? - public var contact: Contact? - public var location: Location? - public var venue: Venue? - public var newChatMember: User? - public var leftChatMember: User? - public var newChatTitle: String? - public var newChatPhoto: [PhotoSize]? - public var deleteChatPhoto: Bool? - public var groupChatCreated: Bool? - public var supergroupChatCreated: Bool? - public var channelChatCreated: Bool? - public var migrateToChatId: Int? - public var migrateFromChatId: Int? - public var pinnedMessage: Message? - - public init() { - self.messageId = 0 - self.date = 0 - self.chat = Chat(id: 9, type: .private, title: nil, username: nil, firstName: nil, lastName: nil) + public let from: User? + public let forwardFrom: User? + public let forwardFromChat: Chat? + public let forwardDate: Int? + public let replyToMessage: Message? + public let editDate: Int? + public let text: String? + public let entities: [MessageEntity]? + public let audio: Audio? + public let document: Document? + public let photo: [PhotoSize]? + public let sticker: Sticker? + public let video: Video? + public let voice: Voice? + public let caption: String? + public let contact: Contact? + public let location: Location? + public let venue: Venue? + public let newChatMember: User? + public let leftChatMember: User? + public let newChatTitle: String? + public let newChatPhoto: [PhotoSize]? + public let deleteChatPhoto: Bool? + public let groupChatCreated: Bool? + public let supergroupChatCreated: Bool? + public let channelChatCreated: Bool? + public let migrateToChatId: Int? + public let migrateFromChatId: Int? + public let pinnedMessage: Message? + + enum CodingKeys: String, CodingKey { + case date, chat, from, text, entities, audio, document, photo, sticker, video, voice, caption, contact, location, venue + case messageId = "message_id" + case forwardFrom = "forward_from" + case forwardFromChat = "forward_from_chat" + case forwardDate = "forward_date" + case replyToMessage = "reply_to_message" + case editDate = "edit_date" + case newChatMember = "new_chat_member" + case leftChatMember = "left_chat_member" + case newChatTitle = "new_chat_title" + case newChatPhoto = "new_chat_photo" + case deleteChatPhoto = "delete_chat_photo" + case groupChatCreated = "group_chat_created" + case supergroupChatCreated = "supergroup_chat_created" + case channelChatCreated = "channel_chat_created" + case migrateToChatId = "migrate_to_chat_id" + case migrateFromChatId = "migrate_from_chat_id" + case pinnedMessage = "pinned_message" } } -public struct Chat { +public struct Chat: Codable { - public var id: Int - public var type: StructType + public let id: Int + public let type: StructType /* Optional. */ - public var title: String? - public var username: String? - public var firstName: String? - public var lastName: String? - - public enum StructType: String { - - public init?(from string: String?) { - guard let typeString = string else { return nil } - guard let instance = StructType(rawValue: typeString.lowercased()) else { return nil } - self = instance - } + public let title: String? + public let username: String? + public let firstName: String? + public let lastName: String? + public enum StructType: String, Codable { case `private`, group, supergroup, channel } + enum CodingKeys: String, CodingKey { + case id, type, title, username + case firstName = "first_name" + case lastName = "last_name" + } + } -public struct User { +public struct User: Codable { - public var id: Int - public var firstName: String + public let id: Int + public let firstName: String /* OPTIONAL. */ - public var lastName: String? - public var username: String? + public let lastName: String? + public let username: String? + + enum CodingKeys: String, CodingKey { + case id, username + case firstName = "first_name" + case lastName = "last_name" + } } -public struct MessageEntity { +public struct MessageEntity: Codable { - public var type: StructType - public var offset: Int - public var length: Int + public let type: StructType + public let offset: Int + public let length: Int /* OPTIONAl. */ - public var url: String? - public var user: User? - - public enum StructType: String { - public init?(from string: String?) { - guard let typeString = string else { return nil } - guard let instance = StructType(rawValue: typeString.lowercased()) else { return nil } - self = instance - } + public let url: String? + public let user: User? - case mention, hashtag + public enum StructType: String, Codable { + case mention, hashtag, url, email, bold, italic, code, pre case botCommand = "bot_command" - case url, email, bold, italic, code, pre case textLink = "text_link" case textMention = "text_mention" - } } -public struct Audio { +public struct Audio: Codable { - public var fileId: String - public var duration: Int + public let fileId: String + public let duration: Int /* OPTIONAL. */ - public var performer: String? - public var title: String? - public var mimeType: String? - public var fileSize: Int? + public let performer: String? + public let title: String? + public let mimeType: String? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case duration, performer, title + case fileId = "file_id" + case mimeType = "mime_type" + case fileSize = "file_size" + } } -public struct Document { +public struct Document: Codable { - public var fileId: String + public let fileId: String /* OPTIONAL. */ - public var thumb: PhotoSize? - public var fileName: String? - public var mimeType: String? - public var fileSize: Int? + public let thumb: PhotoSize? + public let fileName: String? + public let mimeType: String? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case thumb + case fileId = "file_id" + case fileName = "file_name" + case mimeType = "mime_type" + case fileSize = "file_size" + } } -public struct PhotoSize { +public struct PhotoSize: Codable { - public var fileId: String - public var width: Int - public var height: Int + public let fileId: String + public let width: Int + public let height: Int /* Optional. */ - public var fileSize: Int? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case width, height + case fileId = "file_id" + case fileSize = "file_size" + } } -public struct Sticker { +public struct Sticker: Codable { - public var fileId: String - public var width: Int - public var height: Int + public let fileId: String + public let width: Int + public let height: Int /* Optional. */ - public var thumb: PhotoSize? - public var emoji: String? - public var fileSize: Int? + public let thumb: PhotoSize? + public let emoji: String? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case width, height, thumb, emoji + case fileId = "file_id" + case fileSize = "file_size" + } } -public struct Video { +public struct Video: Codable { - public var fileId: String - public var width: Int - public var height: Int - public var duration: Int + public let fileId: String + public let width: Int + public let height: Int + public let duration: Int /* OPTIONAL. */ - public var thumb: PhotoSize? - public var mimeType: String? - public var fileSize: Int? + public let thumb: PhotoSize? + public let mimeType: String? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case width, height, duration, thumb + case fileId = "file_id" + case mimeType = "mime_type" + case fileSize = "file_size" + } } -public struct Voice { +public struct Voice: Codable { - public var fileId: String - public var duration: Int + public let fileId: String + public let duration: Int /* Optional. */ - public var mimeType: String? - public var fileSize: Int? + public let mimeType: String? + public let fileSize: Int? + + enum CodingKeys: String, CodingKey { + case duration + case fileId = "file_id" + case mimeType = "mime_type" + case fileSize = "file_size" + } } -public struct Contact { +public struct Contact: Codable { - public var phoneNumber: String - public var firstName: String + public let phoneNumber: String + public let firstName: String /* OPTIONAL. */ - public var lastName: String? - public var userId: Int? + public let lastName: String? + public let userId: Int? + + enum CodingKeys: String, CodingKey { + case phoneNumber = "phone_number" + case firstName = "first_name" + case lastName = "last_name" + case userId = "user_id" + } } -public struct Location { +public struct Location: Codable { - public var longitude: Double - public var latitude: Double + public let longitude: Double + public let latitude: Double } -public struct Venue { +public struct Venue: Codable { - public var location: Location - public var title: String - public var address: String + public let location: Location + public let title: String + public let address: String /* OPTIONAL. */ - public var foursquareId: String? + public let foursquareId: String? + + enum CodingKeys: String, CodingKey { + case location, title, address + case foursquareId = "foursquare_id" + } } -public struct File { +public struct File: Codable { - public var fileId: String + public let fileId: String /* OPTIONAL. */ - public var fileSize: Int? - public var filePath: String? + public let fileSize: Int? + public let filePath: String? + + enum CodingKeys: String, CodingKey { + case fileSize = "file_size" + case fileId = "file_id" + case filePath = "file_path" + } } -public enum ParseMode: String { +public enum ParseMode: String, Codable { case markdown case html } -public enum ChatAction: String { +public enum ChatAction: String, Codable { case typing case uploadPhoto = "upload_photo" case recordVideo = "record_video" diff --git a/Sources/Utilities.swift b/Sources/Utilities.swift index de4441d..e511ac9 100644 --- a/Sources/Utilities.swift +++ b/Sources/Utilities.swift @@ -6,57 +6,41 @@ // // -extension String { +import Foundation - func bytes() -> [UInt8] { - return [UInt8](utf8) - } - -} - -struct Log { +public enum Result: Decodable where T: Decodable { - static func warning(message: String) { - print("[⚠️WARN] \(message)") - } - - static func warning(on object: Any) { - warning(message: "====>>>====<<<====") - warning(message: "Failed to convert:") - warning(message: "\(object)") - } + case success(T) + case failure(Swift.Error) - static func warning(onMethod method: String) { - warning(message: "====>>>====<<<====") - warning(message: "Failed in method: \(method)") + enum CodingKeys: String, CodingKey { + case ok, result, description } -} - -extension Dictionary { - - - /// Append content of another dictionary to self. - /// - /// The value will override the original value if the key is duplicated. - /// - /// - Parameter dictionary: the another dictionary - mutating func append(contentOf dictionary: [Key: Value]) { - for (key, value) in dictionary { - self[key] = value + public init(from decoder: Decoder) { + do { + let container = try decoder.container(keyedBy: CodingKeys.self) + switch try container.decode(Bool.self, forKey: .ok) { + case true: + self = .success(try container.decode(T.self, forKey: .result)) + case false: + self = .failure(Error.telegram(try container.decode(String.self, forKey: .description))) + } + } catch(let error) { + self = .failure(error) } } - /// Append content of another dictionary to self. - /// - /// If the optional value has some value, it will override the original value if the key is duplicated. - /// But if the optional value is nil, it won't override the original value if the key is duplicated. - /// - /// - Parameter dictionary: the another dictionary - mutating func append(contentOf dictionary: [Key: Value?]) { - for (key, optionalValue) in dictionary { - if let value = optionalValue { self[key] = value } + static func decode(from data: Data) -> Result { + do { + return try JSONDecoder().decode(Result.self, from: data) + } catch(let error) { + return .failure(error) } } } + +public enum Error: Swift.Error { + case telegram(String?) +} diff --git a/Sources/ZEGBot.swift b/Sources/ZEGBot.swift index 322e8d1..f178b5c 100644 --- a/Sources/ZEGBot.swift +++ b/Sources/ZEGBot.swift @@ -8,10 +8,11 @@ // Licensed under Apache License v2.0 // -import SwiftyJSON import Foundation import Dispatch +public typealias UpdateHandler = (Result, ZEGBot) -> Void + public struct ZEGBot { internal let session = URLSession(configuration: .default) @@ -26,17 +27,21 @@ public struct ZEGBot { var offset = 0 let semaphore = DispatchSemaphore(value: 0) while true { - let task = session.dataTask(with: URL(string: urlPrefix + "getupdates?timeout=60&offset=\(offset)")!) { data, _, _ in - guard let updatesData = data, - let updates = ZEGBot.decodeUpdates(from: updatesData) else { - semaphore.signal() - return + let task = session.dataTask(with: URL(string: urlPrefix + "getupdates?timeout=60&offset=\(offset)")!) { data, _, error in + guard let data = data else { + handler(.failure(error!), self) + semaphore.signal() + return } - if let lastUpdate = updates.last { offset = lastUpdate.updateId + 1 } - semaphore.signal() - for update in updates { - handler(update, self) + switch Result<[Update]>.decode(from: data) { + case .success(let updates): + if let lastUpdate = updates.last { offset = lastUpdate.updateId + 1 } + for update in updates { + handler(.success(update), self) + } + case .failure(let error): handler(.failure(error), self) } + semaphore.signal() } task.resume() semaphore.wait() @@ -44,23 +49,3 @@ public struct ZEGBot { } } - -extension ZEGBot { - - /* For getUpdates. */ - static func decodeUpdates(from jsonData: Data) -> [Update]? { - - return Update.array(from: JSON(data: jsonData)[PARAM.RESULT]) - - } - - /* For webhook. */ - static func decodeUpdate(from jsonData: Data) -> Update? { - - return Update(from: JSON(data: jsonData)[PARAM.RESULT]) - - } - -} - -public typealias UpdateHandler = (Update, ZEGBot) -> Void diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..9a1a3f0 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import ZEGBotTests + +XCTMain([ + testCase(ZEGBotTests.allTests), + ]) diff --git a/Tests/ZEGBotTests/ZEGBotTests.swift b/Tests/ZEGBotTests/ZEGBotTests.swift index 1f39c57..8b29584 100644 --- a/Tests/ZEGBotTests/ZEGBotTests.swift +++ b/Tests/ZEGBotTests/ZEGBotTests.swift @@ -1,31 +1,187 @@ import XCTest +import Foundation @testable import ZEGBot class ZEGBotTests: XCTestCase { static var allTests : [(String, (ZEGBotTests) -> () throws -> Void)] { return [ - ("testStringToBytes", testStringToBytes), + ("testMessageEntities", testMessageEntities) ] } - func testStringToBytes() { - let bytes: [UInt8] = [104, 101, 108, 108, 111, 33] - XCTAssertEqual("hello!".bytes(), bytes) - } + func testMessageEntities() { + let message1 = try! JSONDecoder().decode(Message.self, from: """ + { + "message_id": 1502, + "from": { + "id": 80548625, + "is_bot": false, + "first_name": "Shane", + "last_name": "Qi", + "username": "ShaneQi", + "language_code": "en-US" + }, + "chat": { + "id": 80548625, + "first_name": "Shane", + "last_name": "Qi", + "username": "ShaneQi", + "type": "private" + }, + "date": 1506745637, + "text": "@ShaneQi\\n#hashtag\\nhttps://google.com\\nqizengtai@gmail.com\\nbold\\nitalic\\ncode\\nlet hello = \\"world\\"\\n\\ngoogle", + "entities": [{ + "offset": 0, + "length": 8, + "type": "mention" + }, { + "offset": 9, + "length": 8, + "type": "hashtag" + }, { + "offset": 18, + "length": 18, + "type": "url" + }, { + "offset": 37, + "length": 19, + "type": "email" + }, { + "offset": 57, + "length": 4, + "type": "bold" + }, { + "offset": 62, + "length": 6, + "type": "italic" + }, { + "offset": 69, + "length": 4, + "type": "code" + }, { + "offset": 74, + "length": 20, + "type": "pre" + }, { + "offset": 95, + "length": 6, + "type": "text_link", + "url": "https://google.com/" + }] + } + """.data(using: .utf8)!) + + XCTAssert(message1.entities!.count == 9) + + XCTAssert(message1.entities![0].offset == 0) + XCTAssert(message1.entities![0].length == 8) + XCTAssert(message1.entities![0].type == .mention) + XCTAssert(message1.entities![0].url == nil) + XCTAssert(message1.entities![0].user == nil) + + XCTAssert(message1.entities![1].offset == 9) + XCTAssert(message1.entities![1].length == 8) + XCTAssert(message1.entities![1].type == .hashtag) + XCTAssert(message1.entities![1].url == nil) + XCTAssert(message1.entities![1].user == nil) + + XCTAssert(message1.entities![2].offset == 18) + XCTAssert(message1.entities![2].length == 18) + XCTAssert(message1.entities![2].type == .url) + XCTAssert(message1.entities![2].url == nil) + XCTAssert(message1.entities![2].user == nil) + + XCTAssert(message1.entities![3].offset == 37) + XCTAssert(message1.entities![3].length == 19) + XCTAssert(message1.entities![3].type == .email) + XCTAssert(message1.entities![3].url == nil) + XCTAssert(message1.entities![3].user == nil) + + XCTAssert(message1.entities![4].offset == 57) + XCTAssert(message1.entities![4].length == 4) + XCTAssert(message1.entities![4].type == .bold) + XCTAssert(message1.entities![4].url == nil) + XCTAssert(message1.entities![4].user == nil) + + XCTAssert(message1.entities![5].offset == 62) + XCTAssert(message1.entities![5].length == 6) + XCTAssert(message1.entities![5].type == .italic) + XCTAssert(message1.entities![5].url == nil) + XCTAssert(message1.entities![5].user == nil) + + XCTAssert(message1.entities![6].offset == 69) + XCTAssert(message1.entities![6].length == 4) + XCTAssert(message1.entities![6].type == .code) + XCTAssert(message1.entities![6].url == nil) + XCTAssert(message1.entities![6].user == nil) + + XCTAssert(message1.entities![7].offset == 74) + XCTAssert(message1.entities![7].length == 20) + XCTAssert(message1.entities![7].type == .pre) + XCTAssert(message1.entities![7].url == nil) + XCTAssert(message1.entities![7].user == nil) + + XCTAssert(message1.entities![8].offset == 95) + XCTAssert(message1.entities![8].length == 6) + XCTAssert(message1.entities![8].type == .textLink) + XCTAssert(message1.entities![8].url == "https://google.com/") + XCTAssert(message1.entities![8].user == nil) + + let message2 = try! JSONDecoder().decode(Message.self, from: """ + { + "message_id": 1510, + "from": { + "id": 80548625, + "is_bot": false, + "first_name": "Shane", + "last_name": "Qi", + "username": "ShaneQi", + "language_code": "en-US" + }, + "chat": { + "id": 80548625, + "first_name": "Shane", + "last_name": "Qi", + "username": "ShaneQi", + "type": "private" + }, + "date": 1506747809, + "text": "Max /jake", + "entities": [{ + "offset": 0, + "length": 3, + "type": "text_mention", + "user": { + "id": 413748427, + "is_bot": false, + "first_name": "Max", + "last_name": "Lau" + } + }, { + "offset": 4, + "length": 5, + "type": "bot_command" + }] + } + """.data(using: .utf8)!) - func testDictionaryAppending() { - var dictionary: [String: Any] = ["key1": 1] - let additionalDictionary: [String: Any] = ["key1": "value1", "key2": "value2"] - let additionalOptionalDictionary: [String: Any?] = ["key1": nil, "key3": 3] + XCTAssert(message2.entities!.count == 2) - dictionary.append(contentOf: additionalDictionary) - XCTAssertEqual(dictionary.keys.count, 2) - XCTAssertEqual(dictionary["key1"] as! String, "value1") + XCTAssert(message2.entities![0].offset == 0) + XCTAssert(message2.entities![0].length == 3) + XCTAssert(message2.entities![0].type == .textMention) + XCTAssert(message2.entities![0].url == nil) + XCTAssert(message2.entities![0].user!.id == 413748427) + XCTAssert(message2.entities![0].user!.firstName == "Max") + XCTAssert(message2.entities![0].user!.lastName == "Lau") + XCTAssert(message2.entities![0].user!.username == nil) - dictionary.append(contentOf: additionalOptionalDictionary) - XCTAssertEqual(dictionary["key1"] as! String, "value1") - XCTAssertEqual(dictionary["key3"] as! Int, 3) + XCTAssert(message2.entities![1].offset == 4) + XCTAssert(message2.entities![1].length == 5) + XCTAssert(message2.entities![1].type == .botCommand) + XCTAssert(message2.entities![1].url == nil) + XCTAssert(message2.entities![1].user == nil) } } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ea27866 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,73 @@ +FROM ubuntu:16.04 +MAINTAINER Haris Amin + +# Install related packages and set LLVM 3.6 as the compiler +RUN apt-get -q update && \ + apt-get -q install -y \ + make \ + libc6-dev \ + clang-3.8 \ + curl \ + libedit-dev \ + python2.7 \ + python2.7-dev \ + libicu-dev \ + libssl-dev \ + libxml2 \ + git \ + libcurl4-openssl-dev \ + pkg-config \ + && update-alternatives --quiet --install /usr/bin/clang clang /usr/bin/clang-3.8 100 \ + && update-alternatives --quiet --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.8 100 \ + && rm -r /var/lib/apt/lists/* + +# Everything up to here should cache nicely between Swift versions, assuming dev dependencies change little +ARG SWIFT_PLATFORM=ubuntu16.04 +ARG SWIFT_BRANCH=swift-4.0-release +ARG SWIFT_VERSION=swift-4.0-RELEASE + +ENV SWIFT_PLATFORM=$SWIFT_PLATFORM \ + SWIFT_BRANCH=$SWIFT_BRANCH \ + SWIFT_VERSION=$SWIFT_VERSION + +# Download GPG keys, signature and Swift package, then unpack, cleanup and execute permissions for foundation libs +RUN SWIFT_URL=https://swift.org/builds/$SWIFT_BRANCH/$(echo "$SWIFT_PLATFORM" | tr -d .)/$SWIFT_VERSION/$SWIFT_VERSION-$SWIFT_PLATFORM.tar.gz \ + && curl -fSsL $SWIFT_URL -o swift.tar.gz \ + && curl -fSsL $SWIFT_URL.sig -o swift.tar.gz.sig \ + && export GNUPGHOME="$(mktemp -d)" \ + && set -e; \ + for key in \ + # pub 4096R/412B37AD 2015-11-19 [expires: 2017-11-18] + # Key fingerprint = 7463 A81A 4B2E EA1B 551F FBCF D441 C977 412B 37AD + # uid Swift Automatic Signing Key #1 + 7463A81A4B2EEA1B551FFBCFD441C977412B37AD \ + # pub 4096R/21A56D5F 2015-11-28 [expires: 2017-11-27] + # Key fingerprint = 1BE1 E29A 084C B305 F397 D62A 9F59 7F4D 21A5 6D5F + # uid Swift 2.2 Release Signing Key + 1BE1E29A084CB305F397D62A9F597F4D21A56D5F \ + # pub 4096R/91D306C6 2016-05-31 [expires: 2018-05-31] + # Key fingerprint = A3BA FD35 56A5 9079 C068 94BD 63BC 1CFE 91D3 06C6 + # uid Swift 3.x Release Signing Key + A3BAFD3556A59079C06894BD63BC1CFE91D306C6 \ + # pub 4096R/71E1B235 2016-05-31 [expires: 2019-06-14] + # Key fingerprint = 5E4D F843 FB06 5D7F 7E24 FBA2 EF54 30F0 71E1 B235 + # uid Swift 4.x Release Signing Key + 5E4DF843FB065D7F7E24FBA2EF5430F071E1B235 \ + ; do \ + gpg --quiet --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ + done \ + && gpg --batch --verify --quiet swift.tar.gz.sig swift.tar.gz \ + && tar -xzf swift.tar.gz --directory / --strip-components=1 \ + && rm -r "$GNUPGHOME" swift.tar.gz.sig swift.tar.gz \ + && chmod -R o+r /usr/lib/swift + +# Post cleanup for binaries orthogonal to swift runtime, but was used to download and install. +RUN apt-get -y remove --purge \ + python2.7 + +# Post cleanup for binaries orthogonal to swift runtime, but was used to download and install. +RUN apt-get -y remove --purge \ + python2.7 + +# Print Installed Swift Version +RUN swift --version diff --git a/docker/test.sh b/docker/test.sh new file mode 100644 index 0000000..c13acfa --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,7 @@ +sudo docker run \ +--rm \ +-v `pwd`/:/ZEGBot \ +-w /ZEGBot \ +swiftdocker/swift:latest \ +/bin/sh -c \ +"swift test" diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100644 index 6878147..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -sudo docker run \ --v `pwd`/:/ZEGBot \ -swift:latest \ -/bin/sh -c \ -"\ -apt-get update;\ -apt-get install uuid-dev -y;\ -cd ZEGBot;\ -swift build;\ -"