├── .gitignore ├── .swift-version ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── Builder.swift ├── Client.swift ├── Env.swift ├── Event.swift ├── EventParser.swift ├── ImagemapMessageBuilder.swift ├── Int.swift ├── JSON.swift ├── LINEBotAPI.swift ├── MessageBuilder.swift ├── Profile.swift ├── String.swift ├── TemplateMessageBuilder.swift └── URLHelper.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yoshiki Kurihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "LINEBotAPI", 5 | dependencies: [ 6 | .Package(url: "https://github.com/yoshiki/Curl.git", majorVersion: 0), 7 | .Package(url: "https://github.com/Zewo/JSON.git", majorVersion: 0, minor: 12), 8 | .Package(url: "https://github.com/Zewo/HTTPServer.git", majorVersion: 0, minor: 13), 9 | .Package(url: "https://github.com/ZewoGraveyard/Base64.git", majorVersion: 0, minor: 12), 10 | ], 11 | exclude: [ "EnvironmentTests" ] 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINEBotAPI 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![Platform][platform-badge]][platform-url] 5 | [![License][mit-badge]][mit-url] 6 | [![Bot][linebot-badge]][linebot-url] 7 | 8 | ## Overview 9 | 10 | This library is **Unofficial** 11 | 12 | **LINEBotAPI** is a SDK of the LINE Messaging API for Swift. 13 | 14 | - Swift 3 support 15 | - Using [Zewo](https://github.com/Zewo/) 16 | - Linux Ready 17 | 18 | ## Features 19 | 20 | - [x] Send text/image/video/audio/location/sticker message 21 | - [x] Handle follow/unfollow/join/leave/postback/Beacon events 22 | - [x] Send imagemap/template message 23 | 24 | ## A Work In progress 25 | 26 | LINEBotAPI is currently in development. 27 | 28 | ## Attention 29 | 30 | Currently LINEBotAPI works with `3.0`. 31 | 32 | ## Getting started 33 | 34 | ### Installation 35 | 36 | Before we start, we need to install some tools and dependencies. 37 | 38 | ### Swift 39 | 40 | Install Swift using swiftenv or use docker images([docker-swift](https://github.com/swiftdocker/docker-swift)). 41 | 42 | ### swiftenv 43 | 44 | [`swiftenv`](https://github.com/kylef/swiftenv) allows you to easily install multiple versions of swift. 45 | 46 | ``` 47 | % git clone https://github.com/kylef/swiftenv.git ~/.swiftenv 48 | ``` 49 | and add settings for your shell(For more information, please see [swiftenv's wiki](https://github.com/kylef/swiftenv). 50 | 51 | Then install Swift 3.0. This process does not need on Mac OS X installed Xcode 8, only Linux. 52 | 53 | ``` 54 | % swiftenv install 3.0 55 | ``` 56 | 57 | ## Install other libraries. 58 | 59 | - On OS X 60 | 61 | ``` 62 | % brew install openssl curl 63 | % brew link --force openssl 64 | ``` 65 | 66 | - On Linux 67 | 68 | ``` 69 | % sudo apt-get update 70 | % sudo apt-get install libcurl4-openssl-dev 71 | ``` 72 | 73 | # Create project 74 | 75 | We got prepared for a creating project. 76 | 77 | Let's create your LINE Bot! 78 | 79 | ## Make project directory. 80 | 81 | ``` 82 | % mkdir linebot && cd linebot 83 | ``` 84 | 85 | Set Swift version to `3.0` if you need. 86 | 87 | ``` 88 | % swiftenv local 3.0 89 | ``` 90 | 91 | Initialize project directory with Swift Package Manager(**SPM**). 92 | 93 | ``` 94 | % swift package init --type executable 95 | ``` 96 | 97 | Then this command will create the basic structure for executable command. 98 | 99 | ``` 100 | . 101 | ├── Package.swift 102 | ├── Sources 103 | │   └── main.swift 104 | └── Tests 105 | ``` 106 | 107 | ## Package.swift 108 | 109 | Open `Package.swift` and make it looks like this: 110 | 111 | ```swift 112 | import PackageDescription 113 | 114 | let package = Package( 115 | name: "linebot", 116 | dependencies: [ 117 | .Package(url: "https://github.com/yoshik/LINEBotAPI.git", majorVersion: 1, minor: 0), 118 | ] 119 | ) 120 | ``` 121 | 122 | ## main.swift 123 | 124 | Next, write main program in `main.swift`. 125 | 126 | ```swift 127 | import LINEBotAPI 128 | 129 | let bot = LINEBotAPI(accessToken: "YOUR_ACCESS_TOKEN") 130 | try bot.sendText(to: "USER_ID", text: "Hello! Hello!") 131 | ``` 132 | 133 | This code: 134 | - get target `userId`(A user id on LINE) 135 | - send text to `userId` specified. 136 | 137 | ## Build project 138 | 139 | Change lib and include paths to your environment. 140 | 141 | ``` 142 | % swift build 143 | ``` 144 | 145 | ## Run it 146 | 147 | After it compiles, run it. 148 | 149 | >Your must specify USER_ID` to yours. `USER_ID` is your user id. 150 | 151 | ``` 152 | % .build/debug/linebot 153 | ``` 154 | 155 | You will get a message from bot on LINE if you had setup setting on bot management page. 156 | 157 | # Start Server 158 | 159 | LINEBotAPI supports server mode using `Zewo`. 160 | 161 | ## main.swift 162 | 163 | Open `main.swift` and make it look like this: 164 | 165 | ```swift 166 | import LINEBotAPI 167 | import HTTPServer 168 | 169 | let bot = LINEBotAPI( 170 | accessToken: "YOUR_ACCESS_TOKEN", 171 | channelSecret: "YOUR_CHANNEL_SECRET" 172 | ) 173 | let log = LogMiddleware(debug: true) 174 | 175 | // Initializer a router. 176 | let router = BasicRouter { (routes) in 177 | // Waiting for POST request on /callback. 178 | routes.post("/callback") { (request) in 179 | // Parsing request and validate signature 180 | return try bot.parseRequest(request) { (event) in 181 | if let textMessage = event as? TextMessage, let text = textMessage.text { 182 | let builder = MessageBuilder() 183 | builder.addText(text: text) 184 | if let messages = try builder.build(), let replyToken = textMessage.replyToken { 185 | try bot.replyMessage(replyToken: replyToken, messages: messages) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | 192 | // start server 193 | let server = try Server(port: 8080, middleware: [log], responder: router) 194 | try server.start() 195 | ``` 196 | 197 | >This is echo bot. 198 | 199 | ## Build and Run it 200 | 201 | Then build and run it. 202 | 203 | ``` 204 | % swift build 205 | % .build/debug/linebot 206 | ``` 207 | 208 | The server will be started on port 8080. 209 | 210 | This server will be waiting a POST request from Bot Server at `/callback`. 211 | 212 | # Other examples 213 | 214 | >TODO 215 | 216 | # Tips 217 | 218 | ## Can I develop on Xcode? 219 | 220 | Yes, sure. You can generate a xcode project file with following command. 221 | 222 | ``` 223 | % swift package generate-xcodeproj 224 | ``` 225 | 226 | ## Can I use https server? 227 | 228 | Maybe. We are developing it using reverse proxy, but you must be able to support https because Zewo has `HTTPSServer`. 229 | 230 | # License 231 | 232 | LINEBotAPI is released under the MIT license. See LICENSE for details. 233 | 234 | [swift-badge]: https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat 235 | [swift-url]: https://swift.org 236 | [platform-badge]: https://img.shields.io/badge/Platform-Mac%20%26%20Linux-lightgray.svg?style=flat 237 | [platform-url]: https://swift.org 238 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 239 | [mit-url]: https://tldrlegal.com/license/mit-license 240 | [linebot-badge]:https://img.shields.io/badge/Bot-LINE-brightgreen.svg?style=flat 241 | [linebot-url]:https://developers.line.me/bot-api/overview 242 | -------------------------------------------------------------------------------- /Sources/Builder.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public enum BuilderError: Error { 4 | case messagesNotFound 5 | } 6 | 7 | public protocol Builder { 8 | func build() throws -> JSON? 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Client.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | import Curl 3 | 4 | public typealias Headers = [(String,String)] 5 | 6 | public enum ClientError: Error { 7 | case invalidURI, unknownError 8 | } 9 | 10 | public struct Client { 11 | var headers: Headers 12 | let curl = Curl() 13 | 14 | public init(headers: Headers) { 15 | self.headers = headers 16 | } 17 | 18 | public func get(uri: String) -> Data? { 19 | return curl.get(url: uri, headers: headers) 20 | } 21 | 22 | public func post(uri: String, json: JSON? = nil) -> Data? { 23 | if let json = json { 24 | return curl.post(url: uri, headers: headers, body: JSONSerializer().serialize(json: json)) 25 | } else { 26 | return curl.post(url: uri, headers: headers, body: "") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Env.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin 5 | #endif 6 | 7 | public class Env { 8 | public static func getVar(name: String) -> String? { 9 | if let out = getenv(name) { 10 | return String(validatingUTF8: out) 11 | } else { 12 | return nil 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Event.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public enum EventType: String { 4 | case message = "message" 5 | case follow = "follow" 6 | case unfollow = "unfollow" 7 | case join = "join" 8 | case leave = "leave" 9 | case postback = "postback" 10 | case beacon = "beacon" 11 | 12 | public var asJSON: JSON { 13 | return JSON.infer(rawValue) 14 | } 15 | } 16 | 17 | public enum MessageType: String { 18 | case text = "text" 19 | case image = "image" 20 | case video = "video" 21 | case audio = "audio" 22 | case location = "location" 23 | case sticker = "sticker" 24 | case imagemap = "imagemap" 25 | case template = "template" 26 | 27 | public var asJSON: JSON { 28 | return JSON.infer(rawValue) 29 | } 30 | } 31 | 32 | public enum SourceType: String { 33 | case user = "user" 34 | case group = "group" 35 | case room = "room" 36 | 37 | public var asJSON: JSON { 38 | return JSON.infer(rawValue) 39 | } 40 | } 41 | 42 | public struct Source { 43 | public let type: SourceType 44 | public let id: String 45 | } 46 | 47 | public protocol GetContentAPI {} 48 | 49 | public protocol Event { 50 | var json: JSON { get set } 51 | init(json: JSON) 52 | } 53 | 54 | extension Event { 55 | public subscript(path: String) -> JSON? { 56 | return json.get(path: path) 57 | } 58 | 59 | public var replyToken: String? { 60 | return json["replyToken"]?.stringValue 61 | } 62 | 63 | public var eventType: EventType? { 64 | return self["type"] 65 | .flatMap { $0.stringValue } 66 | .flatMap { EventType(rawValue: $0) } 67 | } 68 | 69 | public var timestamp: String? { 70 | return self["timestamp"] 71 | .flatMap { $0.stringValue } 72 | } 73 | 74 | public var source: Source? { 75 | return self["source"] 76 | .flatMap { $0.objectValue } 77 | .flatMap { (s) -> Source? in 78 | if let type = s["type"]?.stringValue, 79 | let sourceType = SourceType(rawValue: type) { 80 | switch sourceType { 81 | case .user: 82 | if let id = s["userId"]?.stringValue { 83 | return Source(type: sourceType, id: id) 84 | } 85 | case .group: 86 | if let id = s["groupId"]?.stringValue { 87 | return Source(type: sourceType, id: id) 88 | } 89 | case .room: 90 | if let id = s["roomId"]?.stringValue { 91 | return Source(type: sourceType, id: id) 92 | } 93 | } 94 | } 95 | return nil 96 | } 97 | } 98 | } 99 | 100 | public protocol MessageEvent: Event {} 101 | 102 | extension MessageEvent { 103 | public var message: JSON? { 104 | return json["message"] 105 | } 106 | 107 | public var messageId: String? { 108 | return message 109 | .flatMap { $0.objectValue } 110 | .flatMap { $0["id"]?.stringValue } 111 | } 112 | 113 | public var messageType: MessageType? { 114 | return message 115 | .flatMap { $0.objectValue } 116 | .flatMap { $0["type"]?.stringValue } 117 | .flatMap { MessageType(rawValue: $0) } 118 | } 119 | } 120 | 121 | public struct TextMessage: MessageEvent { 122 | public var json: JSON 123 | 124 | public init(json: JSON) { 125 | self.json = json 126 | } 127 | 128 | public var text: String? { 129 | return message 130 | .flatMap { $0.objectValue } 131 | .flatMap { $0["text"]?.stringValue } 132 | } 133 | } 134 | 135 | public struct ImageMessage: MessageEvent, GetContentAPI { 136 | public var json: JSON 137 | 138 | public init(json: JSON) { 139 | self.json = json 140 | } 141 | } 142 | 143 | public struct VideoMessage: MessageEvent, GetContentAPI { 144 | public var json: JSON 145 | 146 | public init(json: JSON) { 147 | self.json = json 148 | } 149 | } 150 | 151 | public struct AudioMessage: MessageEvent, GetContentAPI { 152 | public var json: JSON 153 | 154 | public init(json: JSON) { 155 | self.json = json 156 | } 157 | } 158 | 159 | public struct LocationMessage: MessageEvent { 160 | public var json: JSON 161 | 162 | public init(json: JSON) { 163 | self.json = json 164 | } 165 | 166 | var title: String? { 167 | return message 168 | .flatMap { $0["title"]?.stringValue } 169 | } 170 | var address: String? { 171 | return message 172 | .flatMap { $0["address"]?.stringValue } 173 | } 174 | var latitude: String? { 175 | return message 176 | .flatMap { $0["latitude"]?.stringValue } 177 | } 178 | var longitude: String? { 179 | return message 180 | .flatMap { $0["latitude"]?.stringValue } 181 | } 182 | } 183 | 184 | // See also: https://devdocs.line.me/files/sticker_list.pdf 185 | public struct StickerMessage: MessageEvent { 186 | public var json: JSON 187 | 188 | public init(json: JSON) { 189 | self.json = json 190 | } 191 | 192 | var packageId: String? { 193 | return message 194 | .flatMap { $0["packageId"]?.stringValue } 195 | } 196 | var stickerId: String? { 197 | return message 198 | .flatMap { $0["stickerId"]?.stringValue } 199 | } 200 | } 201 | 202 | public struct FollowEvent: Event { 203 | public var json: JSON 204 | 205 | public init(json: JSON) { 206 | self.json = json 207 | } 208 | 209 | public var userId: String? { 210 | return source?.id 211 | } 212 | } 213 | 214 | public struct UnfollowEvent: Event { 215 | public var json: JSON 216 | 217 | public init(json: JSON) { 218 | self.json = json 219 | } 220 | 221 | public var userId: String? { 222 | return source?.id 223 | } 224 | } 225 | 226 | public struct JoinEvent: Event { 227 | public var json: JSON 228 | 229 | public init(json: JSON) { 230 | self.json = json 231 | } 232 | } 233 | 234 | 235 | public struct LeaveEvent: Event { 236 | public var json: JSON 237 | 238 | public init(json: JSON) { 239 | self.json = json 240 | } 241 | } 242 | 243 | 244 | public struct PostbackEvent: Event { 245 | public var json: JSON 246 | 247 | public init(json: JSON) { 248 | self.json = json 249 | } 250 | 251 | public var postback: JSON? { 252 | return json["postback"] 253 | } 254 | 255 | public var data: String? { 256 | return postback 257 | .flatMap { $0.objectValue } 258 | .flatMap { $0["data"]?.stringValue } 259 | } 260 | } 261 | 262 | public struct BeaconEvent: Event { 263 | public var json: JSON 264 | 265 | public init(json: JSON) { 266 | self.json = json 267 | } 268 | 269 | public var beacon: JSON? { 270 | return json["beacon"] 271 | } 272 | 273 | public var hwid: String? { 274 | return beacon 275 | .flatMap { $0.objectValue } 276 | .flatMap { $0["hwid"]?.stringValue } 277 | } 278 | 279 | public var type: String? { 280 | return beacon 281 | .flatMap { $0.objectValue } 282 | .flatMap { $0["type"]?.stringValue } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Sources/EventParser.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public struct EventParser { 4 | private static func parseMessage(_ json: JSON) -> Event? { 5 | if let message = json["message"]?.objectValue, 6 | let type = message["type"]?.stringValue, 7 | let messageType = MessageType(rawValue: type) { 8 | switch messageType { 9 | case .text: 10 | return TextMessage(json: json) 11 | case .image: 12 | return ImageMessage(json: json) 13 | case .video: 14 | return VideoMessage(json: json) 15 | case .audio: 16 | return AudioMessage(json: json) 17 | case .location: 18 | return LocationMessage(json: json) 19 | case .sticker: 20 | return StickerMessage(json: json) 21 | default: 22 | return nil 23 | } 24 | } else { 25 | return nil 26 | } 27 | } 28 | 29 | public static func parse(_ json: JSON) -> Event? { 30 | let eventType = json["type"] 31 | .flatMap { $0.stringValue } 32 | .flatMap { EventType(rawValue: $0) } 33 | if let eventType = eventType { 34 | switch eventType { 35 | case .message: 36 | return parseMessage(json) 37 | case .follow: 38 | return FollowEvent(json: json) 39 | case .unfollow: 40 | return UnfollowEvent(json: json) 41 | case .join: 42 | return JoinEvent(json: json) 43 | case .leave: 44 | return LeaveEvent(json: json) 45 | case .postback: 46 | return PostbackEvent(json: json) 47 | case .beacon: 48 | return BeaconEvent(json: json) 49 | } 50 | } else { 51 | return nil 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Sources/ImagemapMessageBuilder.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public typealias ImagemapBuilder = () -> Imagemap 4 | 5 | // Base Size 1040, 700, 460, 300, 240 6 | 7 | public enum ImagemapBaseSize: Int { 8 | case length1040 = 1040 9 | case length700 = 700 10 | case length460 = 460 11 | case length300 = 300 12 | case lenght240 = 240 13 | 14 | public var asJSON: JSON { 15 | return JSON.infer(rawValue) 16 | } 17 | } 18 | 19 | public enum ImagemapActionType: String{ 20 | case uri = "uri" 21 | case message = "message" 22 | 23 | public var asJSON: JSON { 24 | return JSON.infer(rawValue) 25 | } 26 | } 27 | 28 | public struct Bounds { 29 | let x: Int 30 | let y: Int 31 | let width: Int 32 | let height: Int 33 | 34 | public init(x: Int = 0, y: Int = 0, width: Int = 1040, height: Int = 1040) { 35 | self.x = x 36 | self.y = y 37 | self.width = width 38 | self.height = height 39 | } 40 | 41 | private var array: [Int] { 42 | return [x, y, width, height] 43 | } 44 | 45 | public var asJSON: JSON { 46 | return JSON.infer([ 47 | "x": x.asJSON, 48 | "y": y.asJSON, 49 | "width": width.asJSON, 50 | "height": height.asJSON 51 | ]) 52 | } 53 | } 54 | 55 | public protocol ImagemapActionMutable { 56 | var actions: [ImagemapAction] { get } 57 | func addAction(action: ImagemapAction) 58 | } 59 | 60 | public protocol ImagemapAction { 61 | var type: ImagemapActionType { get } 62 | var area: Bounds { get } 63 | var asJSON: JSON { get } 64 | } 65 | 66 | public struct MessageImagemapAction: ImagemapAction { 67 | public let type: ImagemapActionType = .message 68 | public let area: Bounds 69 | private let text: String 70 | 71 | public init(text: String, area: Bounds) { 72 | self.text = text 73 | self.area = area 74 | } 75 | 76 | public var asJSON: JSON { 77 | return JSON.infer([ 78 | "type": type.asJSON, 79 | "text": text.asJSON, 80 | "area": area.asJSON 81 | ]) 82 | } 83 | } 84 | 85 | public struct UriImagemapAction: ImagemapAction { 86 | public let type: ImagemapActionType = .uri 87 | public let area: Bounds 88 | private let linkUri: String 89 | 90 | public init(linkUri: String, area: Bounds) { 91 | self.linkUri = linkUri 92 | self.area = area 93 | } 94 | 95 | public var asJSON: JSON { 96 | return JSON.infer([ 97 | "type": type.asJSON, 98 | "linkUri": linkUri.asJSON, 99 | "area": area.asJSON 100 | ]) 101 | } 102 | } 103 | 104 | public class Imagemap: ImagemapActionMutable { 105 | private struct BaseSize { 106 | let width: ImagemapBaseSize 107 | let height: ImagemapBaseSize 108 | var asJSON: JSON { 109 | return JSON.infer([ 110 | "width": width.asJSON, 111 | "height": height.asJSON 112 | ]) 113 | } 114 | } 115 | private let baseUrl: String 116 | private let altText: String 117 | private let baseSize: BaseSize 118 | 119 | public var actions = [ImagemapAction]() 120 | 121 | public init(baseUrl: String, 122 | altText: String, 123 | width: ImagemapBaseSize = .length1040, 124 | height: ImagemapBaseSize = .length1040) { 125 | self.baseUrl = baseUrl 126 | self.altText = altText 127 | self.baseSize = BaseSize(width: width, height: height) 128 | } 129 | 130 | public func addAction(action: ImagemapAction) { 131 | actions.append(action) 132 | } 133 | 134 | public var asJSON: JSON { 135 | return JSON.infer([ 136 | "type": MessageType.imagemap.asJSON, 137 | "baseUrl": baseUrl.asJSON, 138 | "altText": altText.asJSON, 139 | "baseSize": baseSize.asJSON, 140 | "actions": JSON.infer(actions.flatMap { $0.asJSON }) 141 | ]) 142 | } 143 | } 144 | 145 | public struct ImagemapMessageBuilder: Builder { 146 | private let imagemap: Imagemap 147 | 148 | public init(imagemap: Imagemap) { 149 | self.imagemap = imagemap 150 | } 151 | 152 | public func build() -> JSON? { 153 | return imagemap.asJSON 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/Int.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | extension Int { 4 | public var asJSON: JSON { 5 | return JSON.infer(self) 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Sources/JSON.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | extension JSON { 4 | public func get(path: String) -> JSON? { 5 | let paths = path.characters.split(separator: ".").map { String($0) } 6 | var json = Optional(self) 7 | paths.forEach { key in 8 | json = json.flatMap { 9 | if let index = Int(key), let arr = $0.arrayValue, index < arr.count { 10 | return $0[index] 11 | } else { 12 | return $0[key] 13 | } 14 | } 15 | } 16 | return json 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/LINEBotAPI.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | import OpenSSL 3 | import Base64 4 | import HTTP 5 | 6 | public enum LINEBotAPIError: Error { 7 | case channelSecretNotFound 8 | } 9 | 10 | public class LINEBotAPI { 11 | private typealias EventHandler = (Event) throws -> Void 12 | 13 | internal let client: Client 14 | private let headers: Headers 15 | 16 | public let channelSecret: String? 17 | 18 | public var failureResponse: Response = Response(status: .forbidden) 19 | 20 | public init(accessToken: String, channelSecret: String? = nil) { 21 | self.headers = [ 22 | ("Content-Type", "application/json; charset=utf-8"), 23 | ("Authorization", "Bearer \(accessToken)") 24 | ] 25 | self.channelSecret = channelSecret 26 | self.client = Client(headers: headers) 27 | } 28 | 29 | public func validateSignature(message: String, signature: String) throws -> Bool { 30 | guard let channelSecret = channelSecret else { 31 | throw LINEBotAPIError.channelSecretNotFound 32 | } 33 | let hashed = Hash.hmac(.sha256, key: Data(channelSecret), message: Data(message)) 34 | let base64 = Base64.encode(Data(hashed)) 35 | return (base64 == signature) 36 | } 37 | 38 | public func parseRequest(_ request: Request, handler: EventHandler) throws -> Response { 39 | var body: String = "" 40 | if case .buffer(let data) = request.body { 41 | body = try String(data: data) 42 | } else { 43 | return failureResponse 44 | } 45 | 46 | // validate signature 47 | guard let signature = request.headers["X-Line-Signature"] else { 48 | return Response(status: .forbidden) 49 | } 50 | 51 | let isValidSignature = try validateSignature( 52 | message: body, 53 | signature: signature 54 | ) 55 | 56 | if isValidSignature { 57 | let json = try JSONParser().parse(data: Data(body)) 58 | if let events = json["events"]?.arrayValue { 59 | try events.forEach { 60 | try EventParser.parse($0).flatMap(handler) 61 | } 62 | return Response(status: .ok) 63 | } 64 | } 65 | return failureResponse 66 | } 67 | } 68 | 69 | extension LINEBotAPI { 70 | public func pushMessage(to userId: String, messages: C7.JSON) { 71 | let json = JSON.infer([ "to": userId.asJSON, "messages": messages ]) 72 | let _ = client.post(uri: URLHelper.pushMessageURL(), json: json) 73 | } 74 | 75 | public func replyMessage(replyToken: String, messages: C7.JSON) { 76 | let json = JSON.infer([ "replyToken": replyToken.asJSON, "messages": messages ]) 77 | let _ = client.post(uri: URLHelper.replyMessageURL(), json: json) 78 | } 79 | 80 | public func leaveGroup(groupId: String) { 81 | let _ = client.post(uri: URLHelper.leaveGroupURL(groupId: groupId)) 82 | } 83 | 84 | public func leaveRoom(roomId: String) { 85 | let _ = client.post(uri: URLHelper.leaveRoomURL(roomId: roomId)) 86 | } 87 | 88 | public func getContent(messageId: String) -> C7.Data? { 89 | return client.get(uri: URLHelper.getContentURL(messageId: messageId)) 90 | } 91 | 92 | public func getProfile(userId: String) throws -> Profile? { 93 | if let res = client.get(uri: URLHelper.getProfileURL(userId: userId)) { 94 | let json = try JSONParser().parse(data: res) 95 | guard let displayName = json["displayName"]?.stringValue, 96 | let userId = json["userId"]?.stringValue, 97 | let pictureUrl = json["pictureUrl"]?.stringValue, 98 | let statusMessage = json["statusMessage"]?.stringValue else { 99 | return nil 100 | } 101 | return Profile(displayName: displayName, userId: userId, pictureUrl: pictureUrl, statusMessage: statusMessage) 102 | } else { 103 | return nil 104 | } 105 | } 106 | } 107 | 108 | extension LINEBotAPI { 109 | public func sendText(to userId: String, text: String) throws { 110 | let builder = MessageBuilder() 111 | builder.addText(text: text) 112 | if let messages = try builder.build() { 113 | pushMessage(to: userId, messages: messages) 114 | } 115 | } 116 | 117 | public func sendImage(to userId: String, imageUrl: String, previewUrl: String) throws { 118 | let builder = MessageBuilder() 119 | builder.addImage(imageUrl: imageUrl, previewUrl: previewUrl) 120 | if let messages = try builder.build() { 121 | pushMessage(to: userId, messages: messages) 122 | } 123 | } 124 | 125 | public func sendVideo(to userId: String, videoUrl: String, previewUrl: String) throws { 126 | let builder = MessageBuilder() 127 | builder.addVideo(videoUrl: videoUrl, previewUrl: previewUrl) 128 | if let messages = try builder.build() { 129 | pushMessage(to: userId, messages: messages) 130 | } 131 | } 132 | 133 | public func sendAudio(to userId: String, audioUrl: String, duration: Int) throws { 134 | let builder = MessageBuilder() 135 | builder.addAudio(audioUrl: audioUrl, duration: duration) 136 | if let messages = try builder.build() { 137 | pushMessage(to: userId, messages: messages) 138 | } 139 | } 140 | 141 | public func sendLocation(to userId: String, title: String, address: String, latitude: String, longitude: String) throws { 142 | let builder = MessageBuilder() 143 | builder.addLocation(title: title, address: address, latitude: latitude, longitude: longitude) 144 | if let messages = try builder.build() { 145 | pushMessage(to: userId, messages: messages) 146 | } 147 | } 148 | 149 | public func sendSticker(to userId: String, stickerId: String, packageId: String) throws { 150 | let builder = MessageBuilder() 151 | builder.addSticker(stickerId: stickerId, packageId: packageId) 152 | if let messages = try builder.build() { 153 | pushMessage(to: userId, messages: messages) 154 | } 155 | } 156 | 157 | public func sendImagemap(to userId: String, 158 | imagemapBuilder: ImagemapBuilder) throws { 159 | let builder = MessageBuilder() 160 | builder.addImagemap(imagemapBuilder: imagemapBuilder) 161 | if let messages = try builder.build() { 162 | pushMessage(to: userId, messages: messages) 163 | } 164 | } 165 | 166 | public func sendTemplate(to userId: String, altText: String, templateBuilder: TemplateBuilder) throws { 167 | let builder = MessageBuilder() 168 | try builder.addTemplate(altText: altText, templateBuilder: templateBuilder) 169 | if let messages = try builder.build() { 170 | pushMessage(to: userId, messages: messages) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/MessageBuilder.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public class MessageBuilder: Builder { 4 | private var messages = [JSON]() 5 | 6 | public init() {} 7 | 8 | public func build() throws -> JSON? { 9 | guard messages.count > 0 else { 10 | throw BuilderError.messagesNotFound 11 | } 12 | return JSON.infer(messages) 13 | } 14 | 15 | public func addText(text: String) { 16 | messages.append(JSON.infer([ 17 | "type": MessageType.text.asJSON, 18 | "text": text.asJSON 19 | ])) 20 | } 21 | 22 | public func addImage(imageUrl: String, previewUrl: String) { 23 | messages.append(JSON.infer([ 24 | "type": MessageType.image.asJSON, 25 | "originalContentUrl": imageUrl.asJSON, 26 | "previewImageUrl": previewUrl.asJSON, 27 | ])) 28 | } 29 | 30 | public func addVideo(videoUrl: String, previewUrl: String) { 31 | messages.append(JSON.infer([ 32 | "type": MessageType.video.asJSON, 33 | "originalContentUrl": videoUrl.asJSON, 34 | "previewImageUrl": previewUrl.asJSON, 35 | ])) 36 | } 37 | 38 | public func addAudio(audioUrl: String, duration: Int) { 39 | messages.append(JSON.infer([ 40 | "type": MessageType.audio.asJSON, 41 | "originalContentUrl": audioUrl.asJSON, 42 | "duration": duration.asJSON 43 | ])) 44 | } 45 | 46 | public func addLocation(title: String, address: String, latitude: String, longitude: String) { 47 | messages.append(JSON.infer([ 48 | "type": MessageType.location.asJSON, 49 | "title": title.asJSON, 50 | "address": address.asJSON, 51 | "latitude": latitude.asJSON, 52 | "longitude": longitude.asJSON, 53 | ])) 54 | } 55 | 56 | // See also: https://devdocs.line.me/files/sticker_list.pdf 57 | public func addSticker(stickerId: String, packageId: String) { 58 | messages.append(JSON.infer([ 59 | "type": MessageType.sticker.asJSON, 60 | "packageId": packageId.asJSON, 61 | "stickerId": stickerId.asJSON 62 | ])) 63 | } 64 | 65 | public func addImagemap(imagemapBuilder: ImagemapBuilder) { 66 | let imagemap = imagemapBuilder() 67 | let builder = ImagemapMessageBuilder(imagemap: imagemap) 68 | if let message = builder.build() { 69 | messages.append(message) 70 | } 71 | } 72 | 73 | public func addTemplate(altText: String, templateBuilder: TemplateBuilder) throws { 74 | let template = templateBuilder() 75 | let builder = TemplateMessageBuilder(altText: altText, template: template) 76 | if let message = try builder.build() { 77 | messages.append(message) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Profile.swift: -------------------------------------------------------------------------------- 1 | public struct Profile { 2 | public let displayName: String 3 | public let userId: String 4 | public let pictureUrl: String 5 | public let statusMessage: String 6 | } 7 | -------------------------------------------------------------------------------- /Sources/String.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | extension String { 4 | public var asJSON: JSON { 5 | return JSON.infer(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/TemplateMessageBuilder.swift: -------------------------------------------------------------------------------- 1 | import JSON 2 | 3 | public typealias TemplateBuilder = () -> Template 4 | 5 | public enum TemplateType: String { 6 | case buttons = "buttons" 7 | case confirm = "confirm" 8 | case carousel = "carousel" 9 | 10 | public var asJSON: JSON { 11 | return JSON.infer(rawValue) 12 | } 13 | } 14 | 15 | public enum TemplateActionType: String { 16 | case postback = "postback" 17 | case message = "message" 18 | case uri = "uri" 19 | 20 | public var asJSON: JSON { 21 | return JSON.infer(rawValue) 22 | } 23 | } 24 | 25 | public protocol TemplateAction { 26 | var type: TemplateActionType { get } 27 | var asJSON: JSON { get } 28 | } 29 | 30 | public struct PostbackTemplateAction: TemplateAction { 31 | public let type: TemplateActionType = .postback 32 | private let label: String 33 | private let data: String 34 | private let text: String? 35 | 36 | public init(label: String, data: String, text: String? = nil) { 37 | self.label = label 38 | self.data = data 39 | self.text = text 40 | } 41 | 42 | public var asJSON: JSON { 43 | var action = [ 44 | "type": type.asJSON, 45 | "label": label.asJSON, 46 | "data": data.asJSON 47 | ] 48 | if let text = text { 49 | action["text"] = text.asJSON 50 | } 51 | return JSON.infer(action) 52 | } 53 | } 54 | 55 | public struct MessageTemplateAction: TemplateAction { 56 | public let type: TemplateActionType = .message 57 | private let label: String 58 | private let text: String 59 | 60 | public init(label: String, text: String) { 61 | self.label = label 62 | self.text = text 63 | } 64 | 65 | public var asJSON: JSON { 66 | return JSON.infer([ 67 | "type": type.asJSON, 68 | "label": label.asJSON, 69 | "text": text.asJSON 70 | ]) 71 | } 72 | } 73 | 74 | public struct UriTemplateAction: TemplateAction { 75 | public let type: TemplateActionType = .uri 76 | private let label: String 77 | private let data: String 78 | private let uri: String 79 | 80 | public init(label: String, data: String, uri: String) { 81 | self.label = label 82 | self.data = data 83 | self.uri = uri 84 | } 85 | 86 | public var asJSON: JSON { 87 | return JSON.infer([ 88 | "type": type.asJSON, 89 | "label": label.asJSON, 90 | "uri": uri.asJSON 91 | ]) 92 | } 93 | } 94 | 95 | public protocol TemplateActionMutable { 96 | var actions: [TemplateAction] { get } 97 | func addAction(action: TemplateAction) 98 | } 99 | 100 | public protocol TemplateColumnMutable { 101 | var columns: [CarouselColumn] { get } 102 | func addColumn(column: CarouselColumn) 103 | } 104 | 105 | public protocol Template { 106 | var type: TemplateType { get } 107 | var asJSON: JSON { get } 108 | } 109 | 110 | public class ButtonsTemplate: Template, TemplateActionMutable { 111 | public let type: TemplateType = .buttons 112 | private let thumbnailImageUrl: String? 113 | private let title: String? 114 | private let text: String 115 | public var actions = [TemplateAction]() 116 | 117 | public init(thumbnailImageUrl: String? = nil, title: String? = nil, text: String) { 118 | self.thumbnailImageUrl = thumbnailImageUrl 119 | self.title = title 120 | self.text = text 121 | } 122 | 123 | public func addAction(action: TemplateAction) { 124 | actions.append(action) 125 | } 126 | 127 | public var asJSON: JSON { 128 | var tmpl = [ 129 | "type": type.asJSON, 130 | "text": text.asJSON 131 | ] 132 | if let thumbnailImageUrl = thumbnailImageUrl { 133 | tmpl["thumbnailImageUrl"] = thumbnailImageUrl.asJSON 134 | } 135 | if let title = title { 136 | tmpl["title"] = title.asJSON 137 | } 138 | if !actions.isEmpty { 139 | tmpl["actions"] = JSON.infer(actions.flatMap { $0.asJSON }) 140 | } 141 | return JSON.infer(tmpl) 142 | } 143 | } 144 | 145 | public class ConfirmTemplate: Template, TemplateActionMutable { 146 | public var type: TemplateType = .confirm 147 | private let text: String 148 | public var actions = [TemplateAction]() 149 | 150 | public init(text: String) { 151 | self.text = text 152 | } 153 | 154 | public func addAction(action: TemplateAction) { 155 | actions.append(action) 156 | } 157 | 158 | public var asJSON: JSON { 159 | var tmpl = [ 160 | "type": type.asJSON, 161 | "text": text.asJSON 162 | ] 163 | if !actions.isEmpty { 164 | tmpl["actions"] = JSON.infer(actions.flatMap { $0.asJSON }) 165 | } 166 | return JSON.infer(tmpl) 167 | } 168 | } 169 | 170 | public class CarouselTemplate: Template, TemplateColumnMutable { 171 | public var type: TemplateType = .carousel 172 | public var columns = [CarouselColumn]() 173 | 174 | public init() {} 175 | 176 | public func addColumn(column: CarouselColumn) { 177 | columns.append(column) 178 | } 179 | 180 | public var asJSON: JSON { 181 | var tmpl = [ 182 | "type": type.asJSON, 183 | ] 184 | if !columns.isEmpty { 185 | tmpl["columns"] = JSON.infer(columns.flatMap { $0.asJSON }) 186 | } 187 | return JSON.infer(tmpl) 188 | } 189 | } 190 | 191 | public class CarouselColumn: TemplateActionMutable { 192 | private let thumbnailImageUrl: String? 193 | private let title: String? 194 | private let text: String 195 | public var actions = [TemplateAction]() 196 | 197 | public init(thumbnailImageUrl: String? = nil, title: String? = nil, text: String) { 198 | self.thumbnailImageUrl = thumbnailImageUrl 199 | self.title = title 200 | self.text = text 201 | } 202 | 203 | public func addAction(action: TemplateAction) { 204 | actions.append(action) 205 | } 206 | 207 | public var asJSON: JSON { 208 | var tmpl = [ 209 | "text": text.asJSON 210 | ] 211 | if let thumbnailImageUrl = thumbnailImageUrl { 212 | tmpl["thumbnailImageUrl"] = thumbnailImageUrl.asJSON 213 | } 214 | if let title = title { 215 | tmpl["title"] = title.asJSON 216 | } 217 | if !actions.isEmpty { 218 | tmpl["actions"] = JSON.infer(actions.flatMap { $0.asJSON }) 219 | } 220 | return JSON.infer(tmpl) 221 | } 222 | } 223 | 224 | public enum TemplateError: Error { 225 | case tooManyActions, tooManyColumns, actionsNotFound, columnsNotFound 226 | } 227 | 228 | public struct TemplateMessageBuilder: Builder { 229 | private let altText: String 230 | private let template: Template 231 | 232 | public init(altText: String, template: Template) { 233 | self.altText = altText 234 | self.template = template 235 | } 236 | 237 | public func build() throws -> JSON? { 238 | try validate() 239 | return JSON.infer([ 240 | "type": MessageType.template.asJSON, 241 | "altText": altText.asJSON, 242 | "template": template.asJSON 243 | ]) 244 | } 245 | 246 | private func validate() throws { 247 | if case .buttons = template.type, let t = template as? ButtonsTemplate { 248 | if t.actions.isEmpty { 249 | throw TemplateError.actionsNotFound 250 | } else if t.actions.count > 4 { 251 | throw TemplateError.tooManyActions 252 | } 253 | } else if case .confirm = template.type, let t = template as? ConfirmTemplate { 254 | if t.actions.isEmpty { 255 | throw TemplateError.actionsNotFound 256 | } else if t.actions.count > 2 { 257 | throw TemplateError.tooManyActions 258 | } 259 | } else if case .carousel = template.type, let t = template as? CarouselTemplate { 260 | if t.columns.isEmpty { 261 | throw TemplateError.columnsNotFound 262 | } else if t.columns.count > 5 { 263 | throw TemplateError.tooManyColumns 264 | } 265 | for column in t.columns { 266 | if column.actions.isEmpty { 267 | throw TemplateError.actionsNotFound 268 | } else if column.actions.count > 4 { 269 | throw TemplateError.tooManyActions 270 | } 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /Sources/URLHelper.swift: -------------------------------------------------------------------------------- 1 | public struct URLHelper { 2 | public static let BotAPIBaseURL = "https://api.line.me/v2/bot" 3 | 4 | public static func getContentURL(messageId: String) -> String { 5 | return "\(BotAPIBaseURL)/message/\(messageId)/content" 6 | } 7 | 8 | public static func getProfileURL(userId: String) -> String { 9 | return "\(BotAPIBaseURL)/profile/\(userId)" 10 | } 11 | 12 | public static func leaveRoomURL(roomId: String) -> String { 13 | return "\(BotAPIBaseURL)/room/\(roomId)/leave" 14 | } 15 | 16 | public static func leaveGroupURL(groupId: String) -> String { 17 | return "\(BotAPIBaseURL)/group/\(groupId)/leave" 18 | } 19 | 20 | public static func replyMessageURL() -> String { 21 | return "\(BotAPIBaseURL)/message/reply" 22 | } 23 | 24 | public static func pushMessageURL() -> String { 25 | return "\(BotAPIBaseURL)/message/push" 26 | } 27 | } 28 | --------------------------------------------------------------------------------