diff --git a/Package.resolved b/Package.resolved index a1c76345a..43eff05c6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,22 +10,13 @@ "version": "1.1.0" } }, - { - "package": "SSCommonCrypto", - "repositoryURL": "https://github.com/daltoniam/common-crypto-spm", - "state": { - "branch": null, - "revision": "2eb3aff0fb57f92f5722fac5d6d20bf64669ca66", - "version": "1.1.0" - } - }, { "package": "COPUS", "repositoryURL": "https://github.com/nuclearace/copus", "state": { "branch": null, - "revision": "365743902efc1c93730757cea288bef4b90637a0", - "version": "2.0.0" + "revision": "a2af57fa582c0191789f446cc430615ee7e41d4f", + "version": "2.1.1" } }, { @@ -100,15 +91,6 @@ "version": "2.0.0" } }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream", - "state": { - "branch": null, - "revision": "114e5df9b6251970a069e8f1c0cbb5802759f0a9", - "version": "3.0.5" - } - }, { "package": "TLS", "repositoryURL": "https://github.com/vapor/tls.git", @@ -117,15 +99,6 @@ "revision": "02a47309249e69358aa3c28b5853897585d7a750", "version": "2.1.2" } - }, - { - "package": "SSCZLib", - "repositoryURL": "https://github.com/daltoniam/zlib-spm.git", - "state": { - "branch": null, - "revision": "83ac8d719a2f3aa775dbdf116a57f56fb2c49abb", - "version": "1.1.0" - } } ] }, diff --git a/Package.swift b/Package.swift index a39195a2d..3b44ad532 100644 --- a/Package.swift +++ b/Package.swift @@ -20,12 +20,12 @@ import PackageDescription var deps: [Package.Dependency] = [ - .package(url: "https://github.com/nuclearace/copus", .upToNextMinor(from: "2.0.0")), + .package(url: "https://github.com/nuclearace/copus", .upToNextMinor(from: "2.1.1")), .package(url: "https://github.com/nuclearace/Sodium", .upToNextMinor(from: "2.0.0")), .package(url: "https://github.com/vapor/engine", .upToNextMinor(from: "2.2.0")), ] -var targetDeps: [Target.Dependency] = ["DiscordOpus", "WebSockets"] +var targetDeps: [Target.Dependency] = ["WebSockets"] #if !os(Linux) deps += [.package(url: "https://github.com/daltoniam/Starscream", .upToNextMinor(from: "3.0.0")),] @@ -41,7 +41,6 @@ let package = Package( dependencies: deps, targets: [ .target(name: "SwiftDiscord", dependencies: targetDeps), - .target(name: "DiscordOpus"), .testTarget(name: "SwiftDiscordTests", dependencies: ["SwiftDiscord"]), ] ) diff --git a/README.md b/README.md index aef7fefb7..3f8508f49 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ A Discord API client for Swift. - Create your Swift Package Manager project - Add `.package(url: "https://github.com/nuclearace/SwiftDiscord", .upToNextMajor(from: "6.0.0"))` to your dependencies in Package.swift - Add `import SwiftDiscord` to files you wish to use the module in. - - Run `swift build -Xlinker -L/usr/local/lib -Xlinker -lopus -Xcc -I/usr/local/include`. The Xlinker options are needed to tell the package manager where to find the libsodium and opus libraries that were installed through Homebrew. The Xcc option tells clang where to find the headers for opus. + - Run `swift build` Xcode: -If you wish to use Xcode with your Swift Package Manager project, you can do `swift package generate-xcodeproj`. However after doing that, you'll have to make a change to SwiftDiscord's build settings. Just like when compiling from the command line, we have to tell Xcode where to find libsodium and libopus. This can be done by adding `/usr/local/lib` to the library search paths and `/usr/local/include` to the header search paths. This should be done for the SwiftDiscord and DiscordOpus targets. The DiscordOpus target also needs the `-lopus` option in "Other Linker Flags". +If you wish to use Xcode with your Swift Package Manager project, you can do `swift package generate-xcodeproj`. In Xcode 11 and higher, you can also add SwiftDiscord as a dependency under the `Link Binary With Libraries` section of the `Build Phases` tab of an existing Xcode project. ![](https://i.imgur.com/JR97eTO.png) diff --git a/Sources/DiscordOpus/configure.c b/Sources/DiscordOpus/configure.c deleted file mode 100644 index 678a49c86..000000000 --- a/Sources/DiscordOpus/configure.c +++ /dev/null @@ -1,33 +0,0 @@ -// The MIT License (MIT) -// Copyright (c) 2017 Erik Little - -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without -// limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -// Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -// Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -#include "configure.h" - -int configure_encoder(OpusEncoder *enc, int bitrate, int vbr) -{ - int err; - - err = opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate)); - err = opus_encoder_ctl(enc, OPUS_SET_VBR(vbr)); - - return err; -} - -int configure_decoder(OpusDecoder *dec, int gain) -{ - return opus_decoder_ctl(dec, OPUS_SET_GAIN(gain)); -} diff --git a/Sources/DiscordOpus/include/configure.h b/Sources/DiscordOpus/include/configure.h deleted file mode 100644 index 67c7b1577..000000000 --- a/Sources/DiscordOpus/include/configure.h +++ /dev/null @@ -1,26 +0,0 @@ -// The MIT License (MIT) -// Copyright (c) 2017 Erik Little - -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without -// limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -// Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the -// Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -#ifndef configure_h -#define configure_h - -#include - -int configure_encoder(OpusEncoder *enc, int bitrate, int vbr); -int configure_decoder(OpusDecoder *dec, int gain); - -#endif /* configure_h */ diff --git a/Sources/SwiftDiscord/DiscordJSON.swift b/Sources/SwiftDiscord/DiscordJSON.swift index 2d9555346..4f6a7d63f 100644 --- a/Sources/SwiftDiscord/DiscordJSON.swift +++ b/Sources/SwiftDiscord/DiscordJSON.swift @@ -66,6 +66,12 @@ enum JSON { return nil } + guard response.statusCode != 204 else { + DefaultDiscordLogger.Logger.debug("Response code 204: No content", type: "JSON") + + return nil + } + guard response.statusCode == 200 || response.statusCode == 201 else { DefaultDiscordLogger.Logger.error("Invalid response code \(response.statusCode)", type: "JSON") DefaultDiscordLogger.Logger.error("Response: \(stringData)", type: "JSON") diff --git a/Sources/SwiftDiscord/Gateway/DiscordGateway.swift b/Sources/SwiftDiscord/Gateway/DiscordGateway.swift index a6ac78f65..c55cda3e4 100644 --- a/Sources/SwiftDiscord/Gateway/DiscordGateway.swift +++ b/Sources/SwiftDiscord/Gateway/DiscordGateway.swift @@ -164,29 +164,16 @@ extension DiscordGatewayPayloadData { guard let data = data else { return .null } // TODO this is very ugly. See https://bugs.swift.org/browse/SR-5863 - #if !os(Linux) switch data { case let object as [String: Any]: return .object(object) - case let number as NSNumber where number === kCFBooleanTrue || number === kCFBooleanFalse: + case let number as NSNumber where number === (true as NSNumber) || number === (false as NSNumber): return .bool(number.boolValue) case let integer as Int: return .integer(integer) default: return .null } - #else - switch data { - case let object as [String: Any]: - return .object(object) - case let bool as Bool: - return .bool(bool) - case let integer as Int: - return .integer(integer) - default: - return .null - } - #endif } } diff --git a/Sources/SwiftDiscord/Rest/DiscordEndpoint.swift b/Sources/SwiftDiscord/Rest/DiscordEndpoint.swift index 8ce3f8d50..ee5040fdb 100644 --- a/Sources/SwiftDiscord/Rest/DiscordEndpoint.swift +++ b/Sources/SwiftDiscord/Rest/DiscordEndpoint.swift @@ -51,6 +51,13 @@ public enum DiscordEndpoint : CustomStringConvertible { /// The channel typing endpoint. case typing(channel: ChannelID) + // Reactions + /// The endpoint for creating/deleting own reactions. + case reactions(channel: ChannelID, message: MessageID, emoji: String) + + /// The endpoint for another user's reactions + case userReactions(channel: ChannelID, message: MessageID, emoji: String, user: UserID) + // Permissions /// The base channel permissions endpoint. case permissions(channel: ChannelID) @@ -264,6 +271,11 @@ public extension DiscordEndpoint { return "/channels/\(channel)/messages/\(message)" case let .typing(channel): return "/channels/\(channel)/typing" + // Reactions + case let .reactions(channel, message, emoji): + return "/channels/\(channel)/messages/\(message)/reactions/\(emoji)/@me" + case let .userReactions(channel, message, emoji, user): + return "/channels/\(channel)/messages/\(message)/reactions/\(emoji)/\(user)" // Permissions case let .permissions(channel): return "/channels/\(channel)/permissions" @@ -362,6 +374,11 @@ public extension DiscordEndpoint { return DiscordRateLimitKey(id: channel, urlParts: [.channels, .channelID, .messagesDelete, .messageID]) case let .typing(channel): return DiscordRateLimitKey(id: channel, urlParts: [.channels, .channelID, .typing]) + // Reactions + case let .reactions(channel, _, _): + return DiscordRateLimitKey(id: channel, urlParts: [.channels, .channelID, .messages, .messageID, .reactions, .emoji, .me]) + case let .userReactions(channel, _, _, _): + return DiscordRateLimitKey(id: channel, urlParts: [.channels, .channelID, .messages, .messageID, .reactions, .emoji, .userID]) // Permissions case let .permissions(channel): return DiscordRateLimitKey(id: channel, urlParts: [.channels, .channelID, .permissions]) diff --git a/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer+Channels.swift b/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer+Channels.swift index 3e9b05c04..5abbfe5bd 100644 --- a/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer+Channels.swift +++ b/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer+Channels.swift @@ -83,6 +83,26 @@ public extension DiscordEndpointConsumer where Self: DiscordUserActor { callback: requestCallback) } + /// Default implementation + public func createReaction(for messageId: MessageID, + on channelId: ChannelID, + emoji: String, + callback: ((DiscordMessage?, HTTPURLResponse?) -> ())? = nil) { + let requestCallback: DiscordRequestCallback = { data, response, error in + guard case let .object(message)? = JSON.jsonFromResponse(data: data, response: response) else { + callback?(nil, response) + return + } + + callback?(DiscordMessage(messageObject: message, client: nil), response) + } + + rateLimiter.executeRequest(endpoint: .reactions(channel: channelId, message: messageId, emoji: emoji.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? emoji), + token: token, + requestInfo: .put(content: nil, extraHeaders: nil), + callback: requestCallback) + } + /// Default implementation public func deleteChannel(_ channelId: ChannelID, reason: String? = nil, @@ -338,6 +358,51 @@ public extension DiscordEndpointConsumer where Self: DiscordUserActor { callback: requestCallback) } + /** + Sends multiple messages in a row + + Guarantees that the messages will be sent (and received by Discord) in the specified order + - parameter messages: The list of messages to send + - parameter channelID: The ID of the channel to send the messages to + - parameter callback: The function that will be called after all messages are sent or one fails to send. + The HTTPURLResponse will be from the last attempt to send a message (first failure or final success). + If all messages were sent successfully, the length of the array will be the same as the length of the input. + Otherwise, the callback's array will be shorter. + */ + public func sendMessages(_ messages: [DiscordMessage], + to channelID: ChannelID, + callback: (([DiscordMessage], HTTPURLResponse?) -> ())? = nil ) { + guard let firstMessage = messages.first else { + callback?([], nil) + return + } + + var messagesToSend = messages.dropFirst() + var sentMessages: [DiscordMessage] = [] + + // This function strongly captures `self` (which, being a protocol that doesn't require + // its implementors to be classes, can't be made weak). It shouldn't lead to any reference + // cycles due to the fact that `self` never holds onto a reference to the function. + // It does, however, keep `self` alive until all messages are sent. If this is undesirable, + // maybe we should include an `: AnyClass` on `DiscordEndpointConsumer` + func handlerFunc(sentMessage: DiscordMessage?, response: HTTPURLResponse?) { + guard let sentMessage = sentMessage else { + callback?(sentMessages, response) + return + } + if callback != nil { // Save a bit of memory in the case that we won't need `sentMessages` + sentMessages.append(sentMessage) + } + guard let nextMessage = messagesToSend.first else { + callback?(sentMessages, response) + return + } + messagesToSend = messagesToSend.dropFirst() + sendMessage(nextMessage, to: channelID, callback: handlerFunc) + } + sendMessage(firstMessage, to: channelID, callback: handlerFunc) + } + /// Default implementation public func triggerTyping(on channelId: ChannelID, callback: ((Bool, HTTPURLResponse?) -> ())? = nil) { diff --git a/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer.swift b/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer.swift index f3ee8b919..0ce041d54 100644 --- a/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer.swift +++ b/Sources/SwiftDiscord/Rest/DiscordEndpointConsumer.swift @@ -69,6 +69,19 @@ public protocol DiscordEndpointConsumer { reason: String?, callback: @escaping (DiscordInvite?, HTTPURLResponse?) -> ()) + /// + /// Creates a reaction for the specified message. + /// + /// - parameter for: The message that is to be edited's snowflake id + /// - parameter on: The channel that we are editing on + /// - parameter emoji: The emoji name + /// - parameter callback: An optional callback containing the edited message, if successful. + /// + func createReaction(for messageId: MessageID, + on channelId: ChannelID, + emoji: String, + callback: ((DiscordMessage?, HTTPURLResponse?) -> ())?) + /// /// Deletes the specified channel. /// diff --git a/Sources/SwiftDiscord/Rest/DiscordRateLimiter.swift b/Sources/SwiftDiscord/Rest/DiscordRateLimiter.swift index 594c6e3c4..4398b88c3 100644 --- a/Sources/SwiftDiscord/Rest/DiscordRateLimiter.swift +++ b/Sources/SwiftDiscord/Rest/DiscordRateLimiter.swift @@ -245,6 +245,9 @@ public struct DiscordRateLimitKey : Hashable { static let slack = DiscordRateLimitURLParts(rawValue: 1 << 23) static let github = DiscordRateLimitURLParts(rawValue: 1 << 24) static let auditLog = DiscordRateLimitURLParts(rawValue: 1 << 25) + static let reactions = DiscordRateLimitURLParts(rawValue: 1 << 26) + static let emoji = DiscordRateLimitURLParts(rawValue: 1 << 27) + static let me = DiscordRateLimitURLParts(rawValue: 1 << 28) public init(rawValue: Int) { self.rawValue = rawValue diff --git a/Sources/SwiftDiscord/Voice/DiscordOpusCoding.swift b/Sources/SwiftDiscord/Voice/DiscordOpusCoding.swift index 2f55f23c7..f4276ca98 100644 --- a/Sources/SwiftDiscord/Voice/DiscordOpusCoding.swift +++ b/Sources/SwiftDiscord/Voice/DiscordOpusCoding.swift @@ -16,7 +16,6 @@ // DEALINGS IN THE SOFTWARE. import COPUS -import DiscordOpus import Foundation /// Declares that a type has enough information to encode/decode Opus data. @@ -93,9 +92,10 @@ open class DiscordOpusEncoder : DiscordOpusCodeable { var err = 0 as Int32 encoderState = opus_encoder_create(Int32(sampleRate), Int32(channels), OPUS_APPLICATION_VOIP, &err) - err = configure_encoder(encoderState, Int32(bitrate), vbr ? 1 : 0) + let err2 = opus_encoder_set_bitrate(encoderState, Int32(bitrate)) + let err3 = opus_encoder_set_vbr(encoderState, vbr ? 1 : 0) - guard err == 0 else { + guard err == 0, err2 == 0, err3 == 0 else { destroyState() throw DiscordVoiceError.creationFail @@ -163,9 +163,9 @@ open class DiscordOpusDecoder : DiscordOpusCodeable { var err = 0 as Int32 decoderState = opus_decoder_create(Int32(sampleRate), Int32(channels), &err) - err = configure_decoder(decoderState, Int32(gain)) + let err2 = opus_decoder_set_gain(decoderState, Int32(gain)) - guard err == 0 else { + guard err == 0, err2 == 0 else { destroyState() throw DiscordVoiceError.creationFail diff --git a/Sources/SwiftDiscord/Voice/DiscordVoiceDataSource.swift b/Sources/SwiftDiscord/Voice/DiscordVoiceDataSource.swift index b8541a33e..5b617be8e 100644 --- a/Sources/SwiftDiscord/Voice/DiscordVoiceDataSource.swift +++ b/Sources/SwiftDiscord/Voice/DiscordVoiceDataSource.swift @@ -16,7 +16,6 @@ // DEALINGS IN THE SOFTWARE. import COPUS -import DiscordOpus import Dispatch import Foundation diff --git a/Sources/SwiftDiscord/Voice/DiscordVoiceDecoder.swift b/Sources/SwiftDiscord/Voice/DiscordVoiceDecoder.swift index b1aeb0aa1..fbaee8e82 100644 --- a/Sources/SwiftDiscord/Voice/DiscordVoiceDecoder.swift +++ b/Sources/SwiftDiscord/Voice/DiscordVoiceDecoder.swift @@ -16,7 +16,6 @@ // DEALINGS IN THE SOFTWARE. import COPUS -import DiscordOpus import Foundation /// Class that decodes Opus voice data into raw PCM data for a VoiceEngine. It can decode multiple streams. Decoding is diff --git a/Sources/SwiftDiscord/Voice/DiscordVoiceEngine.swift b/Sources/SwiftDiscord/Voice/DiscordVoiceEngine.swift index 24b135a01..2bd5cd96c 100644 --- a/Sources/SwiftDiscord/Voice/DiscordVoiceEngine.swift +++ b/Sources/SwiftDiscord/Voice/DiscordVoiceEngine.swift @@ -232,7 +232,7 @@ public final class DiscordVoiceEngine : DiscordVoiceEngineSpec { defer { encrypted.deallocate() } - let success = crypto_secretbox_easy(encrypted, &buf, UInt64(buf.count), &nonce, &secret!) + let success = crypto_secretbox_easy(encrypted, buf, UInt64(buf.count), nonce, secret) guard success != -1 else { throw EngineError.encryptionError } @@ -252,7 +252,7 @@ public final class DiscordVoiceEngine : DiscordVoiceEngineSpec { defer { unencrypted.deallocate() } - let success = crypto_secretbox_open_easy(unencrypted, voiceData, UInt64(data.count - 12), &nonce, &secret!) + let success = crypto_secretbox_open_easy(unencrypted, voiceData, UInt64(data.count - 12), nonce, secret) guard success != -1 else { throw EngineError.decryptionError }