├── .github └── workflows │ └── swift.yml ├── .gitignore ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── Package.swift ├── README.md └── Sources └── NIOIRC ├── Helpers ├── ByteBufferExtras.swift └── Scandinavian.swift ├── IRCChannelHandler.swift ├── IRCCommandParser.swift ├── IRCDispatcher.swift ├── IRCMessageParser.swift ├── IRCMessageTarget.swift └── Model ├── IRCChannelMode.swift ├── IRCChannelName.swift ├── IRCCommand.swift ├── IRCCommandCodes.swift ├── IRCMessage.swift ├── IRCMessageRecipient.swift ├── IRCNickName.swift ├── IRCServerName.swift ├── IRCUserID.swift ├── IRCUserInfo.swift └── IRCUserMode.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 7 * * 1" 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - swift:5.10.1-noble 17 | container: ${{ matrix.image }} 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | - name: Build Swift Debug Package 22 | run: swift build -c debug 23 | - name: Build Swift Release Package 24 | run: swift build -c release 25 | nextstep: 26 | runs-on: macos-latest 27 | steps: 28 | - name: Select latest available Xcode 29 | uses: maxim-lobanov/setup-xcode@v1.2.1 30 | with: 31 | xcode-version: latest 32 | - name: Checkout Repository 33 | uses: actions/checkout@v4 34 | - name: Build Swift Debug Package 35 | run: swift build -c debug 36 | - name: Build Swift Release Package 37 | run: swift build -c release 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | *.xcodeproj 70 | .build-linux* 71 | Package.resolved 72 | dump*.json 73 | .swiftpm 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Helge Hess 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 ZeeZide GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-nio-irc", 7 | products: [ 8 | .library(name: "NIOIRC", targets: [ "NIOIRC" ]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-nio.git", 12 | from: "2.18.0") 13 | ], 14 | targets: [ 15 | .target(name: "NIOIRC", dependencies: [ "NIO" ]) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftNIO IRC 2 | 3 | ![Swift5](https://img.shields.io/badge/swift-5-blue.svg) 4 | ![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat) 5 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) 6 | ![tuxOS](https://img.shields.io/badge/os-tuxOS-green.svg?style=flat) 7 | 8 | 9 | SwiftNIO-IRC is a Internet Relay Chat 10 | [protocol implementation](Sources/NIOIRC) for 11 | [SwiftNIO](https://github.com/apple/swift-nio). 12 | 13 | This module contains just the protocol implementation. We also 14 | provide: 15 | - [swift-nio-irc-client](https://github.com/NozeIO/swift-nio-irc-client) - a simple IRC client lib 16 | - [swift-nio-irc-webclient](https://github.com/NozeIO/swift-nio-irc-webclient) - 17 | a simple IRC webclient + WebSocket gateway based on this module, 18 | - [swift-nio-irc-eliza](https://github.com/NozeIO/swift-nio-irc-eliza) - 19 | a cheap yet scalable therapist, 20 | - [swift-nio-irc-server](https://github.com/NozeIO/swift-nio-irc-server) - 21 | a framework to build IRC servers, and MiniIRCd, a small sample server. 22 | 23 | To get started with this, pull 24 | [swift-nio-irc-server](https://github.com/NozeIO/swift-nio-irc-server) - 25 | a module to rule them all and in the darkness bind them. 26 | 27 | NIOIRC is a SwiftNIO port of the 28 | [Noze.io miniirc](https://github.com/NozeIO/Noze.io/tree/master/Samples/miniirc) 29 | example from 2016. 30 | 31 | 32 | ## Importing the module using Swift Package Manager 33 | 34 | An example `Package.swift `importing the necessary modules: 35 | 36 | ```swift 37 | // swift-tools-version:5.0 38 | 39 | import PackageDescription 40 | 41 | let package = Package( 42 | name: "IRCTests", 43 | dependencies: [ 44 | .package(url: "https://github.com/SwiftNIOExtras/swift-nio-irc.git", 45 | from: "0.6.0") 46 | ], 47 | targets: [ 48 | .target(name: "MyProtocolTool", 49 | dependencies: [ "NIOIRC" ]) 50 | ] 51 | ) 52 | ``` 53 | 54 | 55 | ## Using the SwiftNIO IRC protocol handler 56 | 57 | The IRC protocol is implemented as a regular 58 | `ChannelHandler`, similar to `NIOHTTP1`. 59 | It takes incoming `ByteBuffer` data, parses that, and emits `IRCMessage` 60 | items. 61 | Same the other way around, the user writes `IRCReply` 62 | objects, and the handler renders such into `ByteBuffer`s. 63 | 64 | To add the IRC handler to a NIO Channel pipeline: 65 | 66 | ```swift 67 | import NIOIRC 68 | 69 | bootstrap.channelInitializer { channel in 70 | channel.pipeline 71 | .add(handler: IRCChannelHandler()) 72 | .then { ... } 73 | } 74 | ``` 75 | 76 | 77 | ### Who 78 | 79 | Brought to you by 80 | [ZeeZide](http://zeezide.de). 81 | We like 82 | [feedback](https://twitter.com/ar_institute), 83 | GitHub stars, 84 | cool [contract work](http://zeezide.com/en/services/services.html), 85 | presumably any form of praise you can think of. 86 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Helpers/ByteBufferExtras.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct NIO.ByteBuffer 16 | 17 | public extension ByteBuffer { 18 | // This looks expensive, but isn't. As per @weissi String's store up to 15 19 | // bytes inline, no alloc necessary. 20 | 21 | /** 22 | * Write an Integer as an ASCII String. 23 | */ 24 | @inlinable 25 | @discardableResult 26 | mutating func write(integerAsString integer: T, 27 | as: T.Type = T.self) -> Int 28 | { 29 | return self.writeString(String(integer, radix: 10)) 30 | } 31 | 32 | /** 33 | * Set an Integer as an ASCII String. 34 | */ 35 | @inlinable 36 | @discardableResult 37 | mutating func set(integerAsString integer: T, 38 | at index: Int, 39 | as: T.Type = T.self) -> Int 40 | { 41 | return self.setString(String(integer, radix: 10), at: index) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Helpers/Scandinavian.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension String { 16 | // You wonder why, admit it! ;-) 17 | 18 | @usableFromInline 19 | func ircLowercased() -> String { 20 | return String(lowercased().map { c in 21 | switch c { 22 | case "[": return "{" 23 | case "]": return "}" 24 | case "\\": return "|" 25 | case "~": return "^" 26 | default: return c 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/NIOIRC/IRCChannelHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | 17 | public let DefaultIRCPort = 6667 18 | 19 | /** 20 | * Protocol handler for IRC 21 | * 22 | * The IRC protocol is specified in RFC 2812, which updates RFC 1459. However, 23 | * servers don't usually adhere to the specs :-> 24 | * 25 | * Samples: 26 | * 27 | * NICK noze 28 | * USER noze 0 * :Noze io 29 | * :noze JOIN :#nozechannel 30 | * :cherryh.freenode.net 366 helge99 #GNUstep :End of /NAMES list. 31 | * 32 | * Basic syntax: 33 | * 34 | * [':' SOURCE]? ' ' COMMAND [' ' ARGS]? [' :' LAST-ARG]? 35 | * 36 | */ 37 | open class IRCChannelHandler : ChannelDuplexHandler { 38 | 39 | public typealias InboundErr = IRCParserError 40 | 41 | public typealias InboundIn = ByteBuffer 42 | public typealias InboundOut = IRCMessage 43 | 44 | public typealias OutboundIn = IRCMessage 45 | public typealias OutboundOut = ByteBuffer 46 | 47 | @inlinable 48 | public init() {} 49 | 50 | @inlinable 51 | open func channelActive(context: ChannelHandlerContext) { 52 | context.fireChannelActive() 53 | } 54 | @inlinable 55 | open func channelInactive(context: ChannelHandlerContext) { 56 | context.fireChannelInactive() 57 | } 58 | 59 | 60 | // MARK: - Reading 61 | 62 | @usableFromInline 63 | var parser = IRCMessageParser() 64 | 65 | @inlinable 66 | open func channelRead(context: ChannelHandlerContext, data: NIOAny) { 67 | let buffer = self.unwrapInboundIn(data) 68 | 69 | parser.feed(buffer) { error, message in 70 | if let message = message { 71 | channelRead(context: context, value: message) 72 | } 73 | if let error = error { 74 | context.fireErrorCaught(error) 75 | } 76 | } 77 | } 78 | 79 | @inlinable 80 | open func channelRead(context: ChannelHandlerContext, value: InboundOut) { 81 | context.fireChannelRead(self.wrapInboundOut(value)) 82 | } 83 | 84 | @inlinable 85 | open func errorCaught(context: ChannelHandlerContext, error: Swift.Error) { 86 | context.fireErrorCaught(InboundErr.transportError(error)) 87 | } 88 | 89 | 90 | // MARK: - Writing 91 | 92 | @inlinable 93 | public func write(context: ChannelHandlerContext, data: NIOAny, 94 | promise: EventLoopPromise?) 95 | { 96 | let message : OutboundIn = self.unwrapOutboundIn(data) 97 | write(context: context, value: message, promise: promise) 98 | } 99 | 100 | @inlinable 101 | public final func write(context: ChannelHandlerContext, value: IRCMessage, 102 | promise: EventLoopPromise?) 103 | { 104 | var buffer = context.channel.allocator.buffer(capacity: 200) 105 | encode(value: value, target: value.target, into: &buffer) 106 | 107 | context.write(NIOAny(buffer), promise: promise) 108 | } 109 | 110 | @inlinable 111 | func encode(value: IRCMessage, target: String?, 112 | into buffer: inout ByteBuffer) 113 | { 114 | let cColon : UInt8 = 58 115 | let cSpace : UInt8 = 32 116 | let cStar : UInt8 = 42 117 | let cCR : UInt8 = 13 118 | let cLF : UInt8 = 10 119 | 120 | if let origin = value.origin, !origin.isEmpty { 121 | buffer.writeInteger(cColon) 122 | buffer.writeString(origin) 123 | buffer.writeInteger(cSpace) 124 | } 125 | 126 | buffer.writeString(value.command.commandAsString) 127 | 128 | if let s = target { 129 | buffer.writeInteger(cSpace) 130 | buffer.writeString(s) 131 | } 132 | 133 | switch value.command { 134 | case .PING(let s, let s2), .PONG(let s, let s2): 135 | if let s2 = s2 { 136 | buffer.writeInteger(cSpace) 137 | buffer.writeString(s) 138 | buffer.writeLastArgument(s2) 139 | } 140 | else { 141 | buffer.writeLastArgument(s) 142 | } 143 | 144 | case .QUIT(.some(let v)): 145 | buffer.writeLastArgument(v) 146 | 147 | case .NICK(let v), .MODEGET(let v): 148 | buffer.writeInteger(cSpace) 149 | buffer.writeString(v.stringValue) 150 | 151 | case .MODE(let nick, let add, let remove): 152 | buffer.writeInteger(cSpace) 153 | buffer.writeString(nick.stringValue) 154 | 155 | let adds = add .stringValue.map { "+\($0)" } 156 | let rems = remove.stringValue.map { "-\($0)" } 157 | if adds.isEmpty && rems.isEmpty { 158 | buffer.writeLastArgument("") 159 | } 160 | else { 161 | buffer.writeArguments(adds + rems, useLast: true) 162 | } 163 | 164 | case .CHANNELMODE_GET(let v): 165 | buffer.writeInteger(cSpace) 166 | buffer.writeString(v.stringValue) 167 | 168 | case .CHANNELMODE_GET_BANMASK(let v): 169 | buffer.writeInteger(cSpace) 170 | buffer.writeInteger(UInt8(98)) // 'b' 171 | buffer.writeInteger(cSpace) 172 | buffer.writeString(v.stringValue) 173 | 174 | case .CHANNELMODE(let channel, let add, let remove): 175 | buffer.writeInteger(cSpace) 176 | buffer.writeString(channel.stringValue) 177 | 178 | let adds = add .stringValue.map { "+\($0)" } 179 | let rems = remove.stringValue.map { "-\($0)" } 180 | buffer.writeArguments(adds + rems, useLast: true) 181 | 182 | case .USER(let userInfo): 183 | buffer.writeInteger(cSpace) 184 | buffer.writeString(userInfo.username) 185 | if let mask = userInfo.usermask { 186 | buffer.writeInteger(cSpace) 187 | buffer.write(integerAsString: Int(mask.maskValue)) 188 | buffer.writeInteger(cSpace) 189 | buffer.writeInteger(cStar) 190 | } 191 | else { 192 | buffer.writeInteger(cSpace) 193 | buffer.writeString(userInfo.hostname ?? "*") 194 | buffer.writeInteger(cSpace) 195 | buffer.writeString(userInfo.servername ?? "*") 196 | } 197 | buffer.writeLastArgument(userInfo.realname) 198 | 199 | case .QUIT(.none): 200 | break 201 | 202 | case .ISON(let nicks): 203 | buffer.writeArguments(nicks.lazy.map { $0.stringValue }) 204 | 205 | case .JOIN0: 206 | buffer.writeString(" *") 207 | 208 | case .JOIN(let channels, let keys): 209 | buffer.writeCSVArgument(channels.lazy.map { $0.stringValue }) 210 | if let keys = keys { buffer.writeCSVArgument(keys) } 211 | 212 | case .PART(let channels, let message): 213 | buffer.writeCSVArgument(channels.lazy.map { $0.stringValue }) 214 | if let message = message { buffer.writeLastArgument(message) } 215 | 216 | case .LIST(let channels, let target): 217 | if let channels = channels { 218 | buffer.writeCSVArgument(channels.lazy.map { $0.stringValue }) 219 | } 220 | else { buffer.writeString(" *") } 221 | if let target = target { buffer.writeLastArgument(target) } 222 | 223 | case .PRIVMSG(let recipients, let message), 224 | .NOTICE (let recipients, let message): 225 | buffer.writeCSVArgument(recipients.lazy.map { $0.stringValue }) 226 | buffer.writeLastArgument(message) 227 | 228 | case .CAP(let subcmd, let capIDs): 229 | buffer.writeInteger(cSpace) 230 | buffer.writeString(subcmd.commandAsString) 231 | buffer.writeLastArgument(capIDs.joined(separator: " ")) 232 | 233 | case .WHOIS(let target, let masks): 234 | if let target = target { 235 | buffer.writeInteger(cSpace) 236 | buffer.writeString(target) 237 | } 238 | buffer.writeInteger(cSpace) 239 | buffer.writeString(masks.joined(separator: ",")) 240 | 241 | case .WHO(let mask, let opOnly): 242 | if let mask = mask { 243 | buffer.writeInteger(cSpace) 244 | buffer.writeString(mask) 245 | if opOnly { 246 | buffer.writeInteger(cSpace) 247 | buffer.writeInteger(UInt8(111)) // o 248 | } 249 | } 250 | 251 | case .otherCommand(_, let args), 252 | .otherNumeric(_, let args), 253 | .numeric (_, let args): 254 | buffer.writeArguments(args, useLast: true) 255 | } 256 | 257 | buffer.writeInteger(cCR) 258 | buffer.writeInteger(cLF) 259 | } 260 | } 261 | 262 | extension ByteBuffer { 263 | 264 | @usableFromInline 265 | mutating func writeCSVArgument(_ args: T) 266 | where T.Element == String 267 | { 268 | let cSpace : UInt8 = 32 269 | let cComma : UInt8 = 44 270 | 271 | writeInteger(cSpace) 272 | 273 | var isFirst = true 274 | for arg in args { 275 | if isFirst { isFirst = false } 276 | else { writeInteger(cComma) } 277 | writeString(arg) 278 | } 279 | } 280 | 281 | @usableFromInline 282 | mutating func writeArguments(_ args: T) 283 | where T.Element == String 284 | { 285 | let cSpace : UInt8 = 32 286 | 287 | for arg in args { 288 | writeInteger(cSpace) 289 | writeString(arg) 290 | } 291 | } 292 | 293 | @usableFromInline 294 | mutating func writeArguments(_ args: T, useLast: Bool = false) 295 | where T.Element == String 296 | { 297 | let cSpace : UInt8 = 32 298 | 299 | guard !args.isEmpty else { return } 300 | 301 | for arg in args.dropLast() { 302 | writeInteger(cSpace) 303 | writeString(arg) 304 | } 305 | 306 | let lastIdx = args.index(args.startIndex, offsetBy: args.count - 1) 307 | return writeLastArgument(args[lastIdx]) 308 | } 309 | 310 | @usableFromInline 311 | mutating func writeLastArgument(_ s: String) { 312 | let cSpace : UInt8 = 32 313 | let cColon : UInt8 = 58 314 | 315 | writeInteger(cSpace) 316 | writeInteger(cColon) 317 | writeString(s) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Sources/NIOIRC/IRCCommandParser.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public extension IRCCommand { 16 | 17 | /** 18 | * This initializer creates `IRCCommand` values from String command names and 19 | * string arguments (as parsed by the `IRCMessageParser`). 20 | * 21 | * The parser validates the argument counts etc and throws exceptions on 22 | * unexpected input. 23 | */ 24 | init(_ command: String, arguments: [ String ]) throws { 25 | typealias Error = IRCParserError 26 | 27 | func expect(argc: Int) throws { 28 | guard argc == arguments.count else { 29 | throw Error.invalidArgumentCount(command: command, 30 | count: arguments.count, expected: argc) 31 | } 32 | } 33 | func expect(min: Int? = nil, max: Int? = nil) throws { 34 | if let max = max { 35 | guard arguments.count <= max else { 36 | throw Error.invalidArgumentCount(command: command, 37 | count: arguments.count, 38 | expected: max) 39 | } 40 | } 41 | if let min = min { 42 | guard arguments.count >= min else { 43 | throw Error.invalidArgumentCount(command: command, 44 | count: arguments.count, 45 | expected: min) 46 | } 47 | } 48 | } 49 | 50 | func splitChannelsString(_ s: String) throws -> [ IRCChannelName ] { 51 | return try arguments[0].split(separator: ",").map { 52 | guard let n = IRCChannelName(String($0)) else { 53 | throw Error.invalidChannelName(String($0)) 54 | } 55 | return n 56 | } 57 | } 58 | func splitRecipientString(_ s: String) throws -> [ IRCMessageRecipient ] { 59 | return try arguments[0].split(separator: ",").map { 60 | guard let n = IRCMessageRecipient(String($0)) else { 61 | throw Error.invalidMessageTarget(String($0)) 62 | } 63 | return n 64 | } 65 | } 66 | 67 | switch command.uppercased() { 68 | case "QUIT": try expect(max: 1); self = .QUIT(arguments.first) 69 | 70 | case "PING": 71 | try expect(min: 1, max: 2) 72 | self = .PING(server: arguments[0], 73 | server2: arguments.count > 1 ? arguments[1] : nil) 74 | case "PONG": 75 | try expect(min: 1, max: 2) 76 | self = .PONG(server: arguments[0], 77 | server2: arguments.count > 1 ? arguments[1] : nil) 78 | 79 | case "NICK": 80 | try expect(argc: 1) 81 | guard let nick = IRCNickName(arguments[0]) else { 82 | throw Error.invalidNickName(arguments[0]) 83 | } 84 | self = .NICK(nick) 85 | 86 | case "MODE": 87 | try expect(min: 1) 88 | guard let recipient = IRCMessageRecipient(arguments[0]) else { 89 | throw Error.invalidMessageTarget(arguments[0]) 90 | } 91 | 92 | switch recipient { 93 | case .everything: 94 | throw Error.invalidMessageTarget(arguments[0]) 95 | 96 | case .nickname(let nick): 97 | if arguments.count > 1 { 98 | var add = IRCUserMode() 99 | var remove = IRCUserMode() 100 | for arg in arguments.dropFirst() { 101 | var isAdd = true 102 | for c in arg { 103 | if c == "+" { isAdd = true } 104 | else if c == "-" { isAdd = false } 105 | else if let mode = IRCUserMode(String(c)) { 106 | if isAdd { add .insert(mode) } 107 | else { remove.insert(mode) } 108 | } 109 | else { 110 | // else: warn? throw? 111 | print("IRCParser: unexpected IRC mode: \(c) \(arg)") 112 | } 113 | } 114 | } 115 | self = .MODE(nick, add: add, remove: remove) 116 | } 117 | else { 118 | self = .MODEGET(nick) 119 | } 120 | 121 | case .channel(let channelName): 122 | if arguments.count > 1 { 123 | var add = IRCChannelMode() 124 | var remove = IRCChannelMode() 125 | for arg in arguments.dropFirst() { 126 | var isAdd = true 127 | for c in arg { 128 | if c == "+" { isAdd = true } 129 | else if c == "-" { isAdd = false } 130 | else if let mode = IRCChannelMode(String(c)) { 131 | if isAdd { add .insert(mode) } 132 | else { remove.insert(mode) } 133 | } 134 | else { 135 | // else: warn? throw? 136 | print("IRCParser: unexpected IRC mode: \(c) \(arg)") 137 | } 138 | } 139 | } 140 | if add == IRCChannelMode.banMask && remove.isEmpty { 141 | self = .CHANNELMODE_GET_BANMASK(channelName) 142 | } 143 | else { 144 | self = .CHANNELMODE(channelName, add: add, remove: remove) 145 | } 146 | } 147 | else { 148 | self = .CHANNELMODE_GET(channelName) 149 | } 150 | } 151 | 152 | case "USER": 153 | // RFC 1459 154 | // RFC 2812 155 | try expect(argc: 4) 156 | if let mask = UInt16(arguments[1]) { 157 | self = .USER(IRCUserInfo(username : arguments[0], 158 | usermask : IRCUserMode(rawValue: mask), 159 | realname : arguments[3])) 160 | } 161 | else { 162 | self = .USER(IRCUserInfo(username : arguments[0], 163 | hostname : arguments[1], 164 | servername : arguments[2], 165 | realname : arguments[3])) 166 | } 167 | 168 | 169 | case "JOIN": 170 | try expect(min: 1, max: 2) 171 | if arguments[0] == "0" { 172 | self = .JOIN0 173 | } 174 | else { 175 | let channels = try splitChannelsString(arguments[0]) 176 | let keys = arguments.count > 1 177 | ? arguments[1].split(separator: ",").map(String.init) 178 | : nil 179 | self = .JOIN(channels: channels, keys: keys) 180 | } 181 | 182 | case "PART": 183 | try expect(min: 1, max: 2) 184 | let channels = try splitChannelsString(arguments[0]) 185 | self = .PART(channels: channels, 186 | message: arguments.count > 1 ? arguments[1] : nil) 187 | 188 | case "LIST": 189 | try expect(max: 2) 190 | 191 | let channels = arguments.count > 0 192 | ? try splitChannelsString(arguments[0]) : nil 193 | let target = arguments.count > 1 ? arguments[1] : nil 194 | self = .LIST(channels: channels, target: target) 195 | 196 | case "ISON": 197 | try expect(min: 1) 198 | var nicks = [ IRCNickName ]() 199 | for arg in arguments { 200 | nicks += try arg.split(separator: " ").map(String.init).map { 201 | guard let nick = IRCNickName($0) else { 202 | throw Error.invalidNickName($0) 203 | } 204 | return nick 205 | } 206 | } 207 | self = .ISON(nicks) 208 | 209 | case "PRIVMSG": 210 | try expect(argc: 2) 211 | let targets = try splitRecipientString(arguments[0]) 212 | self = .PRIVMSG(targets, arguments[1]) 213 | 214 | case "NOTICE": 215 | try expect(argc: 2) 216 | let targets = try splitRecipientString(arguments[0]) 217 | self = .NOTICE(targets, arguments[1]) 218 | 219 | case "CAP": 220 | try expect(min: 1, max: 2) 221 | guard let subcmd = CAPSubCommand(rawValue: arguments[0]) else { 222 | throw IRCParserError.invalidCAPCommand(arguments[0]) 223 | } 224 | let capIDs = arguments.count > 1 225 | ? arguments[1].components(separatedBy: " ") 226 | : [] 227 | self = .CAP(subcmd, capIDs) 228 | 229 | case "WHOIS": 230 | try expect(min: 1, max: 2) 231 | let maskArg = arguments.count == 1 ? arguments[0] : arguments[1] 232 | let masks = maskArg.split(separator: ",").map(String.init) 233 | self = .WHOIS(server: arguments.count == 1 ? nil : arguments[0], 234 | usermasks: Array(masks)) 235 | 236 | case "WHO": 237 | try expect(max: 2) 238 | switch arguments.count { 239 | case 0: self = .WHO(usermask: nil, onlyOperators: false) 240 | case 1: self = .WHO(usermask: arguments[0], onlyOperators: false) 241 | case 2: self = .WHO(usermask: arguments[0], 242 | onlyOperators: arguments[1] == "o") 243 | default: fatalError("unexpected argument count \(arguments.count)") 244 | } 245 | 246 | default: 247 | self = .otherCommand(command.uppercased(), arguments) 248 | } 249 | } 250 | 251 | /** 252 | * This initializer creates `IRCCommand` values from numeric commands and 253 | * string arguments (as parsed by the `IRCMessageParser`). 254 | * 255 | * The parser validates the argument counts etc and throws exceptions on 256 | * unexpected input. 257 | */ 258 | @inlinable 259 | init(_ v: Int, arguments: [ String ]) throws { 260 | if let code = IRCCommandCode(rawValue: v) { 261 | self = .numeric(code, arguments) 262 | } 263 | else { 264 | self = .otherNumeric(v, arguments) 265 | } 266 | } 267 | 268 | /** 269 | * This initializer creates `IRCCommand` values from String command names and 270 | * string arguments (as parsed by the `IRCMessageParser`). 271 | * 272 | * The parser validates the argument counts etc and throws exceptions on 273 | * unexpected input. 274 | */ 275 | @inlinable 276 | init(_ s: String, _ arguments: String...) throws { 277 | try self.init(s, arguments: arguments) 278 | } 279 | 280 | /** 281 | * This initializer creates `IRCCommand` values from numeric commands and 282 | * string arguments (as parsed by the `IRCMessageParser`). 283 | * 284 | * The parser validates the argument counts etc and throws exceptions on 285 | * unexpected input. 286 | */ 287 | @inlinable 288 | init(_ v: Int, _ arguments: String...) throws { 289 | try self.init(v, arguments: arguments) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /Sources/NIOIRC/IRCDispatcher.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /** 16 | * Dispatches incoming IRCMessage's to protocol methods. 17 | * 18 | * This has a main entry point `irc_msgSend` which takes an `IRCMessage` and 19 | * then calls the respective protocol functions matching the command of the 20 | * message. 21 | * 22 | * If a dispatcher doesn't implement a method, the 23 | * `IRCDispatcherError.doesNotRespondTo` 24 | * error is thrown. 25 | * 26 | * Note: Implementors *can* re-implement `irc_msgSend` and still access the 27 | * default implementation by calling `irc_defaultMsgSend`. Which contains 28 | * the actual dispatcher implementation. 29 | */ 30 | public protocol IRCDispatcher { 31 | // TODO: Improve this, I don't like anything about this except the dispatcher 32 | // name :-> 33 | 34 | // MARK: - Dispatching Function 35 | 36 | func irc_msgSend(_ message: IRCMessage) throws 37 | 38 | // MARK: - Implementations 39 | 40 | func doPing (_ server : String, 41 | server2 : String?) throws 42 | func doCAP (_ cmd : IRCCommand.CAPSubCommand, 43 | _ capIDs : [ String ]) throws 44 | 45 | func doNick (_ nick : IRCNickName) throws 46 | func doUserInfo (_ info : IRCUserInfo) throws 47 | func doModeGet (nick : IRCNickName) throws 48 | func doModeGet (channel : IRCChannelName) throws 49 | func doMode (nick : IRCNickName, 50 | add : IRCUserMode, 51 | remove : IRCUserMode) throws 52 | 53 | func doWhoIs (server : String?, 54 | usermasks : [ String ]) throws 55 | func doWho (mask : String?, operatorsOnly opOnly: Bool) throws 56 | 57 | func doJoin (_ channels : [ IRCChannelName ]) throws 58 | func doPart (_ channels : [ IRCChannelName ], 59 | message : String?) throws 60 | func doPartAll () throws 61 | func doGetBanMask(_ channel : IRCChannelName) throws 62 | 63 | func doNotice (recipients : [ IRCMessageRecipient ], 64 | message : String) throws 65 | func doMessage (sender : IRCUserID?, 66 | recipients : [ IRCMessageRecipient ], 67 | message : String) throws 68 | 69 | func doIsOnline (_ nicks : [ IRCNickName ]) throws 70 | func doList (_ channels : [ IRCChannelName ]?, 71 | _ target : String?) throws 72 | 73 | func doQuit (_ message : String?) throws 74 | } 75 | 76 | public enum IRCDispatcherError : Swift.Error { 77 | 78 | case doesNotRespondTo(IRCMessage) 79 | 80 | case nicknameInUse(IRCNickName) 81 | case noSuchNick (IRCNickName) 82 | case noSuchChannel(IRCChannelName) 83 | case alreadyRegistered 84 | case notRegistered 85 | case cantChangeModeForOtherUsers 86 | } 87 | 88 | public extension IRCDispatcher { 89 | 90 | @inlinable 91 | func irc_msgSend(_ message: IRCMessage) throws { 92 | try irc_defaultMsgSend(message) 93 | } 94 | 95 | func irc_defaultMsgSend(_ message: IRCMessage) throws { 96 | do { 97 | switch message.command { 98 | 99 | case .PING(let server, let server2): 100 | try doPing(server, server2: server2) 101 | 102 | case .PRIVMSG(let recipients, let payload): 103 | let sender = message.origin != nil 104 | ? IRCUserID(message.origin!) : nil 105 | try doMessage(sender: sender, 106 | recipients: recipients, message: payload) 107 | case .NOTICE(let recipients, let message): 108 | try doNotice(recipients: recipients, message: message) 109 | 110 | case .NICK (let nickName): try doNick (nickName) 111 | case .USER (let info): try doUserInfo(info) 112 | case .ISON (let nicks): try doIsOnline(nicks) 113 | case .MODEGET(let nickName): try doModeGet (nick: nickName) 114 | case .CAP (let subcmd, let capIDs): try doCAP (subcmd, capIDs) 115 | case .QUIT (let message): try doQuit (message) 116 | 117 | case .CHANNELMODE_GET(let channelName): 118 | try doModeGet(channel: channelName) 119 | case .CHANNELMODE_GET_BANMASK(let channelName): 120 | try doGetBanMask(channelName) 121 | 122 | case .MODE(let nickName, let add, let remove): 123 | try doMode(nick: nickName, add: add, remove: remove) 124 | 125 | case .WHOIS(let server, let masks): 126 | try doWhoIs(server: server, usermasks: masks) 127 | 128 | case .WHO(let mask, let opOnly): 129 | try doWho(mask: mask, operatorsOnly: opOnly) 130 | 131 | case .JOIN(let channels, _): try doJoin(channels) 132 | case .JOIN0: try doPartAll() 133 | 134 | case .PART(let channels, let message): 135 | try doPart(channels, message: message) 136 | 137 | case .LIST(let channels, let target): 138 | try doList(channels, target) 139 | 140 | default: 141 | throw IRCDispatcherError.doesNotRespondTo(message) 142 | } 143 | } 144 | catch let error as InternalDispatchError { 145 | switch error { 146 | case .notImplemented: 147 | throw IRCDispatcherError.doesNotRespondTo(message) 148 | } 149 | } 150 | catch { 151 | throw error 152 | } 153 | } 154 | } 155 | 156 | fileprivate enum InternalDispatchError : Swift.Error { 157 | case notImplemented(function: String) 158 | } 159 | 160 | public extension IRCDispatcher { 161 | 162 | func doPing(_ server: String, server2: String?) throws { 163 | throw InternalDispatchError.notImplemented(function: #function) 164 | } 165 | func doCAP(_ cmd: IRCCommand.CAPSubCommand, _ capIDs: [ String ]) throws { 166 | throw InternalDispatchError.notImplemented(function: #function) 167 | } 168 | 169 | func doNick(_ nick: IRCNickName) throws { 170 | throw InternalDispatchError.notImplemented(function: #function) 171 | } 172 | func doUserInfo(_ info: IRCUserInfo) throws { 173 | throw InternalDispatchError.notImplemented(function: #function) 174 | } 175 | func doModeGet(nick: IRCNickName) throws { 176 | throw InternalDispatchError.notImplemented(function: #function) 177 | } 178 | func doModeGet(channel: IRCChannelName) throws { 179 | throw InternalDispatchError.notImplemented(function: #function) 180 | } 181 | func doMode(nick: IRCNickName, add: IRCUserMode, remove: IRCUserMode) throws { 182 | throw InternalDispatchError.notImplemented(function: #function) 183 | } 184 | 185 | func doWhoIs(server: String?, usermasks: [ String ]) throws { 186 | throw InternalDispatchError.notImplemented(function: #function) 187 | } 188 | func doWho(mask: String?, operatorsOnly opOnly: Bool) throws { 189 | throw InternalDispatchError.notImplemented(function: #function) 190 | } 191 | 192 | func doJoin(_ channels: [ IRCChannelName ]) throws { 193 | throw InternalDispatchError.notImplemented(function: #function) 194 | } 195 | func doPart(_ channels: [ IRCChannelName ], message: String?) throws { 196 | throw InternalDispatchError.notImplemented(function: #function) 197 | } 198 | func doPartAll() throws { 199 | throw InternalDispatchError.notImplemented(function: #function) 200 | } 201 | func doGetBanMask(_ channel: IRCChannelName) throws { 202 | throw InternalDispatchError.notImplemented(function: #function) 203 | } 204 | 205 | func doNotice(recipients: [ IRCMessageRecipient ], message: String) throws { 206 | throw InternalDispatchError.notImplemented(function: #function) 207 | } 208 | func doMessage(sender: IRCUserID?, recipients: [ IRCMessageRecipient ], 209 | message: String) throws 210 | { 211 | throw InternalDispatchError.notImplemented(function: #function) 212 | } 213 | 214 | func doIsOnline(_ nicks: [ IRCNickName ]) throws { 215 | throw InternalDispatchError.notImplemented(function: #function) 216 | } 217 | func doList(_ channels : [ IRCChannelName ]?, _ target: String?) throws { 218 | throw InternalDispatchError.notImplemented(function: #function) 219 | } 220 | 221 | func doQuit(_ message: String?) throws { 222 | throw InternalDispatchError.notImplemented(function: #function) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Sources/NIOIRC/IRCMessageParser.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct NIO.ByteBuffer 16 | import struct NIO.ByteBufferAllocator 17 | import struct Foundation.Data 18 | 19 | // Compat, remove me. 20 | public typealias IRCParserError = IRCMessageParser.Error 21 | 22 | /** 23 | * Parses `IRCMessage` objects from ByteBuffers. 24 | * 25 | * The parser is tolerant, if a line fails to parse, it yields an error and 26 | * continues parsing. 27 | */ 28 | public struct IRCMessageParser { 29 | // Note: IRC does not actually specify an encoding. Lets be ignorant and 30 | // consider it UTF8 ;-) 31 | 32 | public enum Error : Swift.Error { 33 | case invalidPrefix (Data) 34 | case invalidCommand (Data) 35 | case tooManyArguments (Data) 36 | case invalidArgument (Data) 37 | 38 | case invalidArgumentCount(command: String, count: Int, expected: Int) 39 | case invalidMask (command: String, mask: String) 40 | case invalidChannelName (String) 41 | case invalidNickName (String) 42 | case invalidMessageTarget(String) 43 | case invalidCAPCommand (String) 44 | 45 | case transportError(Swift.Error) 46 | case syntaxError 47 | case notImplemented 48 | } 49 | 50 | public typealias Yield = ( ( Error?, IRCMessage? ) ) -> Void 51 | 52 | @usableFromInline let allocator = ByteBufferAllocator() 53 | @usableFromInline var overflowBuffer : ByteBuffer? = nil 54 | 55 | @inlinable 56 | public mutating func feed(_ buffer: ByteBuffer, yield: Yield) { 57 | if var ob = overflowBuffer { 58 | overflowBuffer = nil 59 | var bb = buffer 60 | ob.writeBuffer(&bb) 61 | return feed(ob, yield: yield) 62 | } 63 | 64 | assert(overflowBuffer == nil, "OB should not be set! \(overflowBuffer!)") 65 | buffer.withUnsafeReadableBytes { bp in 66 | let cNewline : UInt8 = 10 67 | let cCarriageReturn : UInt8 = 13 68 | var cursor = bp[bp.startIndex.. cursor.startIndex && cursor[idx - 1] == cCarriageReturn { 75 | idx -= 1 76 | } 77 | guard cursor.startIndex < idx else { // skip empty lines 78 | cursor = nextCursor 79 | continue 80 | } 81 | 82 | do { 83 | let message = try processLine(cursor[cursor.startIndex..) throws 102 | -> IRCMessage 103 | { 104 | // Basic syntax: 105 | // [':' SOURCE]? ' ' COMMAND [' ' ARGS]? [' :' LAST-ARG]? 106 | let cSpace : UInt8 = 32 107 | let cColon : UInt8 = 58 108 | let c0 : UInt8 = 48 + 0 109 | let c9 : UInt8 = 48 + 9 110 | guard !line.isEmpty else { throw Error.syntaxError } 111 | 112 | var cursor = line 113 | 114 | func isDigit(_ c: UInt8) -> Bool { return c >= c0 && c <= c9 } 115 | func isLetter(_ c: UInt8) -> Bool { 116 | return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) 117 | } 118 | func skipSpaces() { 119 | while !cursor.isEmpty && cursor[cursor.startIndex] == cSpace { 120 | cursor = cursor[cursor.startIndex.advanced(by: 1).. Bool { 124 | switch c { 125 | case 0x01...0x09, 0x0B...0x0C, 0x0E...0x1F, 0x21...0x39, 0x3B...0xFF: 126 | return true 127 | default: 128 | return false 129 | } 130 | } 131 | 132 | func makeString(from slice: Swift.Slice?) -> String? 133 | { 134 | guard let slice = slice else { return nil } 135 | return String(data: Data(slice), encoding: .utf8) // Sigh, the pain. 136 | } 137 | 138 | /* parse source */ 139 | 140 | let source : Swift.Slice? 141 | 142 | if cursor[cursor.startIndex] == cColon { 143 | let startIndex = cursor.startIndex.advanced(by: 1) 144 | let spaceIdx = line.firstIndex(of: cSpace) 145 | 146 | guard let endSourceIdx = spaceIdx, endSourceIdx > startIndex else { 147 | throw Error.invalidPrefix(Data(line)) 148 | } 149 | 150 | source = cursor[startIndex..= 3, 179 | isDigit(cursor[idx1]), isDigit(cursor[idx2]) else { 180 | throw Error.invalidCommand(Data(line)) 181 | } 182 | let i0 = cursor[idx0] - c0, i1 = cursor[idx1] - c0, i2 = cursor[idx2] - c0 183 | commandKey = .int(Int(i0) * 100 + Int(i1) * 10 + Int(i2)) 184 | cursor = cursor[idx2.advanced(by: 1).. 212 | let argSlice : Swift.Slice 213 | if cursor[cursor.startIndex] == cColon { 214 | argSlice = cursor[cursor.startIndex.advanced(by: 1)..(_ messages: T, 30 | promise: EventLoopPromise?) 31 | where T.Element == IRCMessage 32 | 33 | } 34 | 35 | public extension IRCMessageTarget { 36 | 37 | @inlinable 38 | func sendMessage(_ message: IRCMessage, 39 | promise: EventLoopPromise? = nil) 40 | { 41 | sendMessages([ message ], promise: promise) 42 | } 43 | } 44 | 45 | public extension IRCMessageTarget { 46 | 47 | @inlinable 48 | func sendMessage(_ text: String, to recipients: IRCMessageRecipient...) { 49 | guard !recipients.isEmpty else { return } 50 | 51 | let lines = text.components(separatedBy: "\n") 52 | .map { $0.replacingOccurrences(of: "\r", with: "") } 53 | 54 | let messages = lines.map { 55 | IRCMessage(origin: origin, command: .PRIVMSG(recipients, $0)) 56 | } 57 | sendMessages(messages, promise: nil) 58 | } 59 | 60 | @inlinable 61 | func sendNotice(_ text: String, to recipients: IRCMessageRecipient...) { 62 | guard !recipients.isEmpty else { return } 63 | 64 | let lines = text.components(separatedBy: "\n") 65 | .map { $0.replacingOccurrences(of: "\r", with: "") } 66 | 67 | let messages = lines.map { 68 | IRCMessage(origin: origin, command: .NOTICE(recipients, $0)) 69 | } 70 | sendMessages(messages, promise: nil) 71 | } 72 | 73 | @inlinable 74 | func sendRawReply(_ code: IRCCommandCode, _ args: String...) { 75 | sendMessage(IRCMessage(origin: origin, command: .numeric(code, args))) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCChannelMode.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public struct IRCChannelMode : OptionSet { 16 | 17 | public let rawValue: UInt16 18 | 19 | @inlinable 20 | public init(rawValue: UInt16) { 21 | self.rawValue = rawValue 22 | } 23 | 24 | public static let channelOperator = IRCChannelMode(rawValue: 1 << 0) 25 | public static let `private` = IRCChannelMode(rawValue: 1 << 1) 26 | public static let secret = IRCChannelMode(rawValue: 1 << 2) 27 | public static let inviteOnly = IRCChannelMode(rawValue: 1 << 3) 28 | public static let topicOnlyByOperator = IRCChannelMode(rawValue: 1 << 4) 29 | public static let noOutsideClients = IRCChannelMode(rawValue: 1 << 5) 30 | public static let moderated = IRCChannelMode(rawValue: 1 << 6) 31 | public static let userLimit = IRCChannelMode(rawValue: 1 << 7) 32 | public static let banMask = IRCChannelMode(rawValue: 1 << 8) 33 | public static let speakControl = IRCChannelMode(rawValue: 1 << 9) 34 | public static let password = IRCChannelMode(rawValue: 1 << 10) 35 | 36 | @inlinable 37 | public var maskValue : UInt16 { return rawValue } 38 | 39 | @inlinable 40 | public init?(_ string: String) { 41 | var mask : UInt16 = 0 42 | for c in string { 43 | switch c { 44 | case "o": mask += IRCChannelMode.channelOperator.rawValue 45 | case "p": mask += IRCChannelMode.`private`.rawValue 46 | case "s": mask += IRCChannelMode.secret.rawValue 47 | case "i": mask += IRCChannelMode.inviteOnly.rawValue 48 | case "t": mask += IRCChannelMode.topicOnlyByOperator.rawValue 49 | case "n": mask += IRCChannelMode.noOutsideClients.rawValue 50 | case "m": mask += IRCChannelMode.moderated.rawValue 51 | case "l": mask += IRCChannelMode.userLimit.rawValue 52 | case "b": mask += IRCChannelMode.banMask.rawValue 53 | case "v": mask += IRCChannelMode.speakControl.rawValue 54 | case "k": mask += IRCChannelMode.password.rawValue 55 | default: return nil 56 | } 57 | } 58 | 59 | self.init(rawValue: mask) 60 | } 61 | 62 | @inlinable 63 | public var stringValue : String { 64 | var mode = "" 65 | if contains(.channelOperator) { mode += "o" } 66 | if contains(.`private`) { mode += "p" } 67 | if contains(.secret) { mode += "s" } 68 | if contains(.inviteOnly) { mode += "i" } 69 | if contains(.topicOnlyByOperator) { mode += "t" } 70 | if contains(.noOutsideClients) { mode += "n" } 71 | if contains(.moderated) { mode += "m" } 72 | if contains(.userLimit) { mode += "l" } 73 | if contains(.banMask) { mode += "b" } 74 | if contains(.speakControl) { mode += "v" } 75 | if contains(.password) { mode += "k" } 76 | return mode 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCChannelName.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /** 16 | * An IRC channel name 17 | * 18 | * Channel names are case-insensitive! 19 | * 20 | * Strings beginning with a type code (see [IRC-CHAN]): 21 | * - &, #, +, ! 22 | * 23 | * - length: max 50 24 | * - shall not contain spaces 25 | * - shall not contain ASCII 7 (^G) 26 | * - shall not contain a ',' 27 | */ 28 | public struct IRCChannelName : Hashable, CustomStringConvertible { 29 | 30 | public typealias StringLiteralType = String 31 | 32 | @usableFromInline let storage : String 33 | @usableFromInline let normalized : String 34 | 35 | @inlinable 36 | public init?(_ s: String) { 37 | guard IRCChannelName.validate(string: s) else { return nil } 38 | storage = s 39 | normalized = s.ircLowercased() 40 | } 41 | 42 | @inlinable 43 | public var stringValue : String { return storage } 44 | 45 | @inlinable 46 | public func hash(into hasher: inout Hasher) { 47 | normalized.hash(into: &hasher) 48 | } 49 | 50 | @inlinable 51 | public static func ==(lhs: IRCChannelName, rhs: IRCChannelName) -> Bool { 52 | return lhs.normalized == rhs.normalized 53 | } 54 | 55 | @inlinable 56 | public var description : String { return stringValue } 57 | 58 | @inlinable 59 | public static func validate(string: String) -> Bool { 60 | guard string.count > 1 && string.count <= 50 else { return false } 61 | 62 | switch string.first! { 63 | case "&", "#", "+", "!": break 64 | default: return false 65 | } 66 | 67 | func isValidCharacter(_ c: UInt8) -> Bool { 68 | return c != 7 && c != 32 && c != 44 69 | } 70 | guard !string.utf8.contains(where: { !isValidCharacter($0) }) else { 71 | return false 72 | } 73 | 74 | // TODO: RFC 2812 2.3.1 75 | 76 | return true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCCommand.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | 17 | public enum IRCCommand { 18 | 19 | case NICK(IRCNickName) 20 | case USER(IRCUserInfo) 21 | 22 | case ISON([ IRCNickName ]) 23 | 24 | case QUIT(String?) 25 | case PING(server: String, server2: String?) 26 | case PONG(server: String, server2: String?) 27 | 28 | case JOIN(channels: [ IRCChannelName ], keys: [ String ]?) 29 | 30 | /// JOIN-0 is actually "unsubscribe all channels" 31 | case JOIN0 32 | 33 | /// Unsubscribe the given channels. 34 | case PART(channels: [ IRCChannelName ], message: String?) 35 | 36 | case LIST(channels: [ IRCChannelName ]?, target: String?) 37 | 38 | case PRIVMSG([ IRCMessageRecipient ], String) 39 | case NOTICE ([ IRCMessageRecipient ], String) 40 | 41 | case MODE(IRCNickName, add: IRCUserMode, remove: IRCUserMode) 42 | case MODEGET(IRCNickName) 43 | case CHANNELMODE(IRCChannelName, add: IRCChannelMode, remove: IRCChannelMode) 44 | case CHANNELMODE_GET(IRCChannelName) 45 | case CHANNELMODE_GET_BANMASK(IRCChannelName) 46 | 47 | case WHOIS(server: String?, usermasks: [ String ]) 48 | case WHO(usermask: String?, onlyOperators: Bool) 49 | 50 | case numeric (IRCCommandCode, [ String ]) 51 | case otherCommand(String, [ String ]) 52 | case otherNumeric(Int, [ String ]) 53 | 54 | 55 | // MARK: - IRCv3.net 56 | 57 | public enum CAPSubCommand : String { 58 | case LS, LIST, REQ, ACK, NAK, END 59 | 60 | @inlinable 61 | public var commandAsString : String { return rawValue } 62 | } 63 | case CAP(CAPSubCommand, [ String ]) 64 | } 65 | 66 | 67 | // MARK: - Description 68 | 69 | extension IRCCommand : CustomStringConvertible { 70 | 71 | @inlinable 72 | public var commandAsString : String { 73 | switch self { 74 | case .NICK: return "NICK" 75 | case .USER: return "USER" 76 | case .ISON: return "ISON" 77 | case .QUIT: return "QUIT" 78 | case .PING: return "PING" 79 | case .PONG: return "PONG" 80 | case .JOIN, .JOIN0: return "JOIN" 81 | case .PART: return "PART" 82 | case .LIST: return "LIST" 83 | case .PRIVMSG: return "PRIVMSG" 84 | case .NOTICE: return "NOTICE" 85 | case .CAP: return "CAP" 86 | case .MODE, .MODEGET: return "MODE" 87 | case .WHOIS: return "WHOIS" 88 | case .WHO: return "WHO" 89 | case .CHANNELMODE: return "MODE" 90 | case .CHANNELMODE_GET, .CHANNELMODE_GET_BANMASK: return "MODE" 91 | 92 | case .otherCommand(let cmd, _): return cmd 93 | case .otherNumeric(let cmd, _): 94 | let s = String(cmd) 95 | if s.count >= 3 { return s } 96 | return String(repeating: "0", count: 3 - s.count) + s 97 | case .numeric(let cmd, _): 98 | let s = String(cmd.rawValue) 99 | if s.count >= 3 { return s } 100 | return String(repeating: "0", count: 3 - s.count) + s 101 | } 102 | } 103 | 104 | @inlinable 105 | public var arguments : [ String ] { 106 | switch self { 107 | case .NICK(let nick): return [ nick.stringValue ] 108 | case .USER(let info): 109 | if let usermask = info.usermask { 110 | return [ info.username, usermask.stringValue, "*", info.realname ] 111 | } 112 | else { 113 | return [ info.username, 114 | info.hostname ?? info.usermask?.stringValue ?? "*", 115 | info.servername ?? "*", 116 | info.realname ] 117 | } 118 | 119 | case .ISON(let nicks): return nicks.map { $0.stringValue } 120 | 121 | case .QUIT(.none): return [] 122 | case .QUIT(.some(let message)): return [ message ] 123 | case .PING(let server, .none): return [ server ] 124 | case .PONG(let server, .none): return [ server ] 125 | case .PING(let server, .some(let server2)): return [ server, server2 ] 126 | case .PONG(let server, .some(let server2)): return [ server, server2 ] 127 | 128 | case .JOIN(let channels, .none): 129 | return [ channels.map { $0.stringValue }.joined(separator: ",") ] 130 | case .JOIN(let channels, .some(let keys)): 131 | return [ channels.map { $0.stringValue }.joined(separator: ","), 132 | keys.joined(separator: ",")] 133 | 134 | case .JOIN0: return [ "0" ] 135 | 136 | case .PART(let channels, .none): 137 | return [ channels.map { $0.stringValue }.joined(separator: ",") ] 138 | case .PART(let channels, .some(let m)): 139 | return [ channels.map { $0.stringValue }.joined(separator: ","), m ] 140 | 141 | case .LIST(let channels, .none): 142 | guard let channels = channels else { return [] } 143 | return [ channels.map { $0.stringValue }.joined(separator: ",") ] 144 | case .LIST(let channels, .some(let target)): 145 | return [ (channels ?? []).map { $0.stringValue }.joined(separator: ","), 146 | target ] 147 | 148 | case .PRIVMSG(let recipients, let m), .NOTICE (let recipients, let m): 149 | return [ recipients.map { $0.stringValue }.joined(separator: ","), m ] 150 | 151 | case .MODE(let name, let add, let remove): 152 | if add.isEmpty && remove.isEmpty { return [ name.stringValue, "" ] } 153 | else if !add.isEmpty && !remove.isEmpty { 154 | return [ name.stringValue, 155 | "+" + add.stringValue, "-" + remove.stringValue ] 156 | } 157 | else if !remove.isEmpty { 158 | return [ name.stringValue, "-" + remove.stringValue ] 159 | } 160 | else { 161 | return [ name.stringValue, "+" + add.stringValue ] 162 | } 163 | case .CHANNELMODE(let name, let add, let remove): 164 | if add.isEmpty && remove.isEmpty { return [ name.stringValue, "" ] } 165 | else if !add.isEmpty && !remove.isEmpty { 166 | return [ name.stringValue, 167 | "+" + add.stringValue, "-" + remove.stringValue ] 168 | } 169 | else if !remove.isEmpty { 170 | return [ name.stringValue, "-" + remove.stringValue ] 171 | } 172 | else { 173 | return [ name.stringValue, "+" + add.stringValue ] 174 | } 175 | case .MODEGET(let name): return [ name.stringValue ] 176 | case .CHANNELMODE_GET(let name), .CHANNELMODE_GET_BANMASK(let name): 177 | return [ name.stringValue ] 178 | 179 | case .WHOIS(.some(let server), let usermasks): 180 | return [ server, usermasks.joined(separator: ",")] 181 | case .WHOIS(.none, let usermasks): 182 | return [ usermasks.joined(separator: ",") ] 183 | 184 | case .WHO(.none, _): return [] 185 | case .WHO(.some(let usermask), false): return [ usermask ] 186 | case .WHO(.some(let usermask), true): return [ usermask, "o" ] 187 | 188 | case .numeric (_, let args), 189 | .otherCommand(_, let args), 190 | .otherNumeric(_, let args): return args 191 | 192 | default: // TBD: which case do we miss??? 193 | fatalError("unexpected case \(self)") 194 | } 195 | } 196 | 197 | @inlinable 198 | public var description : String { 199 | switch self { 200 | case .PING(let server, let server2), .PONG(let server, let server2): 201 | if let server2 = server2 { 202 | return "\(commandAsString) '\(server)' '\(server2)'" 203 | } 204 | else { 205 | return "\(commandAsString) '\(server)'" 206 | } 207 | 208 | case .QUIT(.some(let v)): return "QUIT '\(v)'" 209 | case .QUIT(.none): return "QUIT" 210 | case .NICK(let v): return "NICK \(v)" 211 | case .USER(let v): return "USER \(v)" 212 | case .ISON(let v): 213 | let nicks = v.map { $0.stringValue} 214 | return "ISON \(nicks.joined(separator: ","))" 215 | 216 | case .MODEGET(let nick): 217 | return "MODE \(nick)" 218 | case .MODE(let nick, let add, let remove): 219 | var s = "MODE \(nick)" 220 | if !add .isEmpty { s += " +\(add .stringValue)" } 221 | if !remove.isEmpty { s += " -\(remove.stringValue)" } 222 | return s 223 | 224 | case .CHANNELMODE_GET(let v): return "MODE \(v)" 225 | case .CHANNELMODE_GET_BANMASK(let v): return "MODE b \(v)" 226 | case .CHANNELMODE(let nick, let add, let remove): 227 | var s = "MODE \(nick)" 228 | if !add .isEmpty { s += " +\(add .stringValue)" } 229 | if !remove.isEmpty { s += " -\(remove.stringValue)" } 230 | return s 231 | 232 | case .JOIN0: return "JOIN0" 233 | 234 | case .JOIN(let channels, .none): 235 | let names = channels.map { $0.stringValue} 236 | return "JOIN \(names.joined(separator: ","))" 237 | 238 | case .JOIN(let channels, .some(let keys)): 239 | let names = channels.map { $0.stringValue} 240 | return "JOIN \(names.joined(separator: ","))" 241 | + " keys: \(keys.joined(separator: ","))" 242 | 243 | case .PART(let channels, .none): 244 | let names = channels.map { $0.stringValue} 245 | return "PART \(names.joined(separator: ","))" 246 | 247 | case .PART(let channels, .some(let message)): 248 | let names = channels.map { $0.stringValue} 249 | return "PART \(names.joined(separator: ",")) '\(message)'" 250 | 251 | case .LIST(.none, .none): return "LIST *" 252 | case .LIST(.none, .some(let target)): return "LIST * @\(target)" 253 | 254 | case .LIST(.some(let channels), .none): 255 | let names = channels.map { $0.stringValue} 256 | return "LIST \(names.joined(separator: ",") )" 257 | 258 | case .LIST(.some(let channels), .some(let target)): 259 | let names = channels.map { $0.stringValue} 260 | return "LIST @\(target) \(names.joined(separator: ",") )" 261 | 262 | case .PRIVMSG(let recipients, let message): 263 | let to = recipients.map { $0.description } 264 | return "PRIVMSG \(to.joined(separator: ",")) '\(message)'" 265 | case .NOTICE (let recipients, let message): 266 | let to = recipients.map { $0.description } 267 | return "NOTICE \(to.joined(separator: ",")) '\(message)'" 268 | 269 | case .CAP(let subcmd, let capIDs): 270 | return "CAP \(subcmd) \(capIDs.joined(separator: ","))" 271 | case .WHOIS(.none, let masks): 272 | return "WHOIS \(masks.joined(separator: ","))" 273 | case .WHOIS(.some(let target), let masks): 274 | return "WHOIS @\(target) \(masks.joined(separator: ","))" 275 | case .WHO(.none, _): 276 | return "WHO" 277 | case .WHO(.some(let mask), let opOnly): 278 | return "WHO \(mask)\(opOnly ? " o" : "")" 279 | 280 | case .otherCommand(let cmd, let args): 281 | return "" 282 | case .otherNumeric(let cmd, let args): 283 | return "" 284 | case .numeric(let cmd, let args): 285 | return "" 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCCommandCodes.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public enum IRCCommandCode : Int { 16 | 17 | // RFC 2812 18 | 19 | case replyWelcome = 1 20 | case replyYourHost = 2 21 | case replyCreated = 3 22 | case replyMyInfo = 4 23 | case replyBounce = 5 24 | 25 | case replyAway = 301 26 | case replyUserhost = 302 27 | case replyISON = 303 28 | case replyUnAway = 305 29 | case replyNowAway = 306 30 | 31 | case replyWhoIsUser = 311 32 | case replyWhoIsServer = 312 33 | case replyWhoIsOperator = 313 34 | case replyWhoWasUser = 314 35 | case replyWhoIsIdle = 317 36 | case replyEndOfWhoIs = 318 37 | case replyWhoIsChannels = 319 38 | case replyEndOfWhoWas = 369 39 | 40 | case replyListStart = 321 // Obsolete 41 | case replyList = 322 42 | case replyListEnd = 323 43 | 44 | case replyChannelModeIs = 324 45 | case replyUniqOpIs = 325 46 | 47 | case replyIsLoggedInAs = 330 // Freenode 48 | 49 | case replyNoTopic = 331 50 | case replyTopic = 332 51 | 52 | case replyInviting = 341 53 | case replySummoning = 342 54 | case replyInviteList = 346 55 | case replyEndOfInviteList = 347 56 | case replyExceptList = 348 57 | case replyEndOfExceptList = 349 58 | 59 | case replyVersion = 351 60 | case replyWhoReply = 352 61 | case replyEndOfWho = 315 62 | case replyNameReply = 353 63 | case replyEndOfNames = 366 64 | 65 | case replyLinks = 364 66 | case replyEndOfLinks = 365 67 | 68 | case replyBanList = 367 69 | case replyEndOfBanList = 368 70 | 71 | case replyInfo = 371 72 | case replyEndOfInfo = 374 73 | case replyMotDStart = 375 74 | case replyMotD = 372 75 | case replyEndOfMotD = 376 76 | 77 | case replyIsConnectingFrom = 378 // Freenode 78 | 79 | case replyYouROper = 381 80 | case replyRehashing = 382 81 | case replyYourService = 383 82 | 83 | case replyTime = 391 84 | case replyUsersStart = 392 85 | case replyUsers = 393 86 | case replyEndOfUsers = 394 87 | case replyNoUsers = 395 88 | 89 | case replyTracelink = 200 90 | case replyTraceConnecting = 201 91 | case replyTraceHandshake = 202 92 | case replyTraceUnknown = 203 93 | case replyTraceOperator = 204 94 | case replyTraceUser = 205 95 | case replyTraceServer = 206 96 | case replyTraceService = 207 97 | case replyTraceNewType = 208 98 | case replyTraceClass = 209 99 | case replyTraceReConnect = 210 100 | case replyTraceLog = 261 101 | case replyTraceEnd = 262 102 | 103 | case replyStatsLinkInfo = 211 104 | case replyStatsCommands = 212 105 | case replyEndOfStats = 219 106 | case replyStatsUptime = 242 107 | case replyStatsOLine = 243 108 | case replyUModeIs = 221 109 | 110 | case replyServList = 234 111 | case replyServListEnd = 235 112 | 113 | case replyLUserClient = 251 114 | case replyLUserOp = 252 115 | case replyLUserUnknown = 253 116 | case replyLUserChannels = 254 117 | case replyLUserMe = 255 118 | 119 | case replyAdminMe = 256 120 | case replyAdminLoc1 = 257 121 | case replyAdminLoc2 = 258 122 | case replyAdminEMail = 259 123 | 124 | case replyTryAgain = 263 125 | 126 | // MARK: - Error Replies (400...599) 127 | 128 | case errorNoSuchNick = 401 129 | case errorNoSuchServer = 402 130 | case errorNoSuchChannel = 403 131 | case errorCannotSendToChain = 404 132 | case errorTooManyChannels = 405 133 | case errorWasNoSuchNick = 406 134 | case errorTooManyTargets = 407 135 | case errorNoSuchService = 408 136 | case errorNoOrigin = 409 137 | case errorInvalidCAPCommand = 410 // IRCv3.net 138 | case errorNoRecipient = 411 139 | case errorNoTextToSend = 412 140 | case errorNoTopLevel = 413 141 | case errorWildTopLevel = 414 142 | case errorBadMask = 415 143 | case errorUnknownCommand = 421 144 | case errorNoMotD = 422 145 | case errorNoAdminInfo = 423 146 | case errorFileError = 424 147 | case errorNoNickNameGiven = 431 148 | case errorErrorneusNickname = 432 149 | case errorNicknameInUse = 433 150 | case errorNickCollision = 436 151 | case errorUnavailResource = 437 152 | case errorUserNotInChannel = 441 153 | case errorNotOnChannel = 442 154 | case errorUserOnChannel = 443 155 | case errorNoLogin = 444 156 | case errorSummonDisabled = 445 157 | case errorUsersDisabled = 446 158 | case errorNotRegistered = 451 159 | case errorNeedMoreParams = 461 160 | case errorAlreadyRegistered = 462 161 | case errorNoPermForHost = 463 162 | case errorPasswdMismatch = 464 163 | case errorYouReBannedCreep = 465 164 | case errorYouWillBeBanned = 466 165 | case errorKeySet = 467 166 | case errorChannelIsFull = 471 167 | case errorUnknownMode = 472 168 | case errorInviteOnlyChan = 473 169 | case errorBannedFromChan = 474 170 | case errorBadChannelKey = 475 171 | case errorBadChannelMask = 476 172 | case errorNoChannelModels = 477 173 | case errorBanListFull = 478 174 | case errorNoProvileges = 481 175 | case errorChanOPrivsNeeded = 482 176 | case errorCantKillServer = 483 177 | case errorRestricted = 484 178 | case errorUniqOpPrivIsNeeded = 485 179 | case errorNoOperHost = 491 180 | 181 | case errorUModeUnknownFlag = 501 182 | case errorUsersDontMatch = 502 183 | 184 | // MARK: - Freenode 185 | 186 | case errorIllegalChannelName = 479 187 | } 188 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCMessage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | 17 | /** 18 | * An IRC message 19 | * 20 | * An optional origin, an optional target and the actual command (including its 21 | * arguments). 22 | */ 23 | public struct IRCMessage : Codable, CustomStringConvertible { 24 | 25 | public enum CodingKeys: String, CodingKey { 26 | case origin, target, command, arguments 27 | } 28 | 29 | @inlinable 30 | public init(origin: String? = nil, target: String? = nil, 31 | command: IRCCommand) 32 | { 33 | self._storage = _Storage(origin: origin, target: target, command: command) 34 | } 35 | 36 | /** 37 | * True origin of message. Do not set in clients. 38 | * 39 | * Examples: 40 | * - `:helge55!~textual@213.211.198.125` 41 | * - `:helge99` 42 | * - `:cherryh.freenode.net` 43 | * 44 | * This is a server name or a nickname w/ user@host parts. 45 | */ 46 | @inlinable 47 | public var origin : String? { 48 | set { copyStorageIfNeeded(); _storage.origin = newValue } 49 | get { return _storage.origin } 50 | } 51 | 52 | @inlinable 53 | public var target : String? { 54 | set { copyStorageIfNeeded(); _storage.target = newValue } 55 | get { return _storage.target } 56 | } 57 | 58 | /** 59 | * The IRC command and its arguments (max 15). 60 | */ 61 | @inlinable 62 | public var command : IRCCommand { 63 | set { copyStorageIfNeeded(); _storage.command = newValue } 64 | get { return _storage.command } 65 | } 66 | 67 | @inlinable 68 | public var description: String { 69 | var ms = " Bool 37 | { 38 | switch ( lhs, rhs ) { 39 | case ( .everything, .everything ): return true 40 | case ( .channel (let lhs), .channel (let rhs)): return lhs == rhs 41 | case ( .nickname(let lhs), .nickname(let rhs)): return lhs == rhs 42 | default: return false 43 | } 44 | } 45 | } 46 | 47 | public extension IRCMessageRecipient { 48 | 49 | @inlinable 50 | init?(_ s: String) { 51 | if s == "*" { self = .everything } 52 | else if let channel = IRCChannelName(s) { self = .channel(channel) } 53 | else if let nick = IRCNickName (s) { self = .nickname(nick) } 54 | else { return nil } 55 | } 56 | 57 | @inlinable 58 | var stringValue : String { 59 | switch self { 60 | case .channel (let name) : return name.stringValue 61 | case .nickname(let name) : return name.stringValue 62 | case .everything : return "*" 63 | } 64 | } 65 | } 66 | 67 | extension IRCMessageRecipient : CustomStringConvertible { 68 | 69 | @inlinable 70 | public var description : String { 71 | switch self { 72 | case .channel (let name) : return name.description 73 | case .nickname(let name) : return name.description 74 | case .everything : return "" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCNickName.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /** 16 | * An IRC nickname 17 | * 18 | * Channel names are case-insensitive! 19 | * 20 | * Maximum length is 9 characters, but clients should support longer for 21 | * future compat. 22 | */ 23 | public struct IRCNickName : Hashable, CustomStringConvertible { 24 | 25 | public typealias StringLiteralType = String 26 | 27 | @usableFromInline let storage : String 28 | @usableFromInline let normalized : String 29 | 30 | public struct ValidationFlags: OptionSet { 31 | public let rawValue : UInt8 32 | public init(rawValue: UInt8) { self.rawValue = rawValue } 33 | 34 | /** 35 | * A violation of the IRC spec, but Twitch IRC chatrooms seem to allow 36 | * usernames that start w/ a digit. 37 | * As per issue #6, thanks @gbeaman. 38 | */ 39 | public static let allowStartingDigit = ValidationFlags(rawValue: 1 << 0) 40 | 41 | /** 42 | * Per RFC a nickname has to be between 2...9 characters, but that is 43 | * rarely the case in practice. 44 | * By default we allow up to 1024 characters. 45 | */ 46 | public static let strictLengthLimit = ValidationFlags(rawValue: 1 << 1) 47 | } 48 | 49 | @inlinable 50 | public init?(_ s: String, 51 | validationFlags: ValidationFlags = [ .allowStartingDigit ]) 52 | { 53 | guard IRCNickName.validate(string: s, 54 | validationFlags: validationFlags) else 55 | { 56 | return nil 57 | } 58 | storage = s 59 | normalized = s.ircLowercased() 60 | } 61 | 62 | @inlinable 63 | public var stringValue : String { 64 | return storage 65 | } 66 | 67 | @inlinable 68 | public func hash(into hasher: inout Hasher) { 69 | normalized.hash(into: &hasher) 70 | } 71 | 72 | @inlinable 73 | public static func ==(lhs: IRCNickName, rhs: IRCNickName) -> Bool { 74 | return lhs.normalized == rhs.normalized 75 | } 76 | 77 | @inlinable 78 | public var description : String { return stringValue } 79 | 80 | public static func validate(string: String, validationFlags: ValidationFlags) 81 | -> Bool 82 | { 83 | let strict = validationFlags.contains(.strictLengthLimit) 84 | guard string.count > 1 && string.count <= (strict ? 9 : 1024) else { 85 | return false 86 | } 87 | 88 | let firstCS = validationFlags.contains(.allowStartingDigit) 89 | ? CharacterSets.letterDigitOrSpecial 90 | : CharacterSets.letterOrSpecial 91 | let innerCS = CharacterSets.letterDigitSpecialOrDash 92 | 93 | let scalars = string.unicodeScalars 94 | guard firstCS.contains(scalars[scalars.startIndex]) else { return false } 95 | 96 | for scalar in scalars.dropFirst() { 97 | guard innerCS.contains(scalar) else { return false } 98 | } 99 | return true 100 | } 101 | } 102 | 103 | import struct Foundation.CharacterSet 104 | 105 | fileprivate enum CharacterSets { 106 | static let letter = CharacterSet.letters 107 | static let digit = CharacterSet.decimalDigits 108 | static let special = CharacterSet(charactersIn: "[]\\`_^{|}") 109 | static let letterOrSpecial = letter.union(special) 110 | static let letterDigitOrSpecial = letter.union(digit).union(special) 111 | static let letterDigitSpecialOrDash = letterDigitOrSpecial 112 | .union(CharacterSet(charactersIn: "-")) 113 | } 114 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCServerName.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /** 16 | * An IRC server name. 17 | * 18 | * Maximum length is 63 characters. 19 | */ 20 | public struct IRCServerName : Hashable { 21 | 22 | public typealias StringLiteralType = String 23 | 24 | @usableFromInline let storage : String 25 | @usableFromInline let normalized : String 26 | 27 | @inlinable 28 | public init?(_ s: String) { 29 | guard IRCNickName.validate(string: s, validationFlags: []) else { 30 | return nil 31 | } 32 | storage = s 33 | normalized = s.ircLowercased() 34 | } 35 | 36 | @inlinable 37 | public var stringValue : String { return storage } 38 | 39 | @inlinable 40 | public func hash(into hasher: inout Hasher) { normalized.hash(into: &hasher) } 41 | 42 | @inlinable 43 | public static func ==(lhs: IRCServerName, rhs: IRCServerName) -> Bool { 44 | return lhs.normalized == rhs.normalized 45 | } 46 | 47 | @inlinable 48 | public static func validate(string: String) -> Bool { 49 | guard string.count > 1 && string.count <= 63 else { 50 | return false 51 | } 52 | 53 | // TODO: RFC 2812 2.3.1 54 | 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCUserID.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public struct IRCUserID : Hashable, CustomStringConvertible { 16 | // TBD: is that really called the user-mask? Or more like "fullusername"? 17 | 18 | public let nick : IRCNickName 19 | public let user : String? 20 | public let host : String? 21 | 22 | @inlinable 23 | public init(nick: IRCNickName, user: String? = nil, host: String? = nil) { 24 | self.nick = nick 25 | self.user = user 26 | self.host = host 27 | } 28 | 29 | @inlinable 30 | public init?(_ s: String) { 31 | if let atIdx = s.firstIndex(of: "@") { 32 | let hs = s.index(after: atIdx) 33 | self.host = String(s[hs.. Bool { 62 | return lhs.nick == rhs.nick && lhs.user == rhs.user && lhs.host == rhs.host 63 | } 64 | 65 | @inlinable 66 | public var stringValue : String { 67 | var ms = "\(nick)" 68 | if let host = host { 69 | if let user = user { ms += "!\(user)" } 70 | ms += "@\(host)" 71 | } 72 | return ms 73 | } 74 | 75 | @inlinable 76 | public var description: String { return stringValue } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/NIOIRC/Model/IRCUserInfo.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-irc open source project 4 | // 5 | // Copyright (c) 2018-2021 ZeeZide GmbH. and the swift-nio-irc project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIOIRC project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public struct IRCUserInfo : Equatable { 16 | 17 | public let username : String 18 | public let usermask : IRCUserMode? 19 | public let hostname : String? 20 | public let servername : String? 21 | public let realname : String 22 | 23 | @inlinable 24 | public init(username: String, usermask: IRCUserMode, realname: String) { 25 | self.username = username 26 | self.usermask = usermask 27 | self.realname = realname 28 | self.hostname = nil 29 | self.servername = nil 30 | } 31 | @inlinable 32 | public init(username: String, hostname: String, servername: String, 33 | realname: String) 34 | { 35 | self.username = username 36 | self.hostname = hostname 37 | self.servername = servername 38 | self.realname = realname 39 | self.usermask = nil 40 | } 41 | 42 | @inlinable 43 | public static func ==(lhs: IRCUserInfo, rhs: IRCUserInfo) -> Bool { 44 | if lhs.username != rhs.username { return false } 45 | if lhs.realname != rhs.realname { return false } 46 | if lhs.usermask != rhs.usermask { return false } 47 | if lhs.servername != rhs.servername { return false } 48 | if lhs.hostname != rhs.hostname { return false } 49 | return true 50 | } 51 | } 52 | 53 | extension IRCUserInfo : CustomStringConvertible { 54 | 55 | @inlinable 56 | public var description : String { 57 | var ms = "