swift-server-community / APNSwift

📱HTTP/2 Apple Push Notification Service built with swift - send push notifications to iOS, iPadOS, tvOS, macOS, watchOS, visionOS, and Safari!
Apache License 2.0
695 stars 105 forks source link

Support for raw payloads #50

Closed daniel-beard closed 5 years ago

daniel-beard commented 5 years ago

I occasionally have a requirement where I have raw string payloads that I'd like to send over APNS. I couldn't see a way to pass a string payload with the current set of classes and protocols. Is this something you'd be willing to support?

E.g. I just want to be able to pass something like the following:

{ "aps": { "alert": "hello world", "badge": 9, "sound": "bingbong.aiff"} ,"g": {"l": "appme:///dispatch/us?utm_medium=notification&utm_source=ec&utm_campaign=20171018"}}
kylebrowning commented 5 years ago

You should be able to conform to APNSwiftNotification and use that.

struct G {
    let l: String
}
struct AcmeNotification: APNSwiftNotification {
    let acme2: [String]
    let g: G
    let aps: APNSwiftPayload

    init(acme2: [String], aps: APNSwiftPayload, g: G) {
        self.acme2 = acme2
        self.aps = aps
        self.g = g
    }
}
daniel-beard commented 5 years ago

@kylebrowning the structure varies quite a bit, and the source for this comes from external systems. I would conform to APNSwiftNotification, but I don't know the structure ahead of time.

I'd also like to replace our aging, internal rails based push testing tool, which takes a freeform string input from QA peeps.

kylebrowning commented 5 years ago

@daniel-beard Let me think about that. We need to find a way to some sort of AnyCodable concept.

This might work but im hesitant to depend on third party dependencies.

weissi commented 5 years ago

Can't we just do

struct SpecialNotification<ExtraPayload: Codable>: APNSwiftNotification {
    let extra: ExtraPayload
    let aps: APNSwiftPayload
}

?

daniel-beard commented 5 years ago

@weissi how in that case would I create the aps object from that single string payload?

kylebrowning commented 5 years ago

What if we did

    public func send(withCustomPayload buffer: ByteBuffer, pushType: APNSwiftConnection.PushType, to deviceToken: String, expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil) -> EventLoopFuture<Void> {
            let streamPromise = channel.eventLoop.makePromise(of: Channel.self)
            multiplexer.createStreamChannel(promise: streamPromise) { channel, streamID in
                let handlers: [ChannelHandler] = [
                    HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: .https),
                    APNSwiftRequestEncoder(deviceToken: deviceToken, configuration: self.configuration, bearerToken: self.bearerTokenFactory?.currentBearerToken, pushType: pushType, expiration: expiration, priority: priority, collapseIdentifier: collapseIdentifier, topic: topic),
                    APNSwiftResponseDecoder(),
                    APNSwiftStreamHandler(),
                ]
                return channel.pipeline.addHandlers(handlers)
            }

            let responsePromise = channel.eventLoop.makePromise(of: Void.self)
            let context = APNSwiftRequestContext(
                request: buffer,
                responsePromise: responsePromise
            )

            return streamPromise.futureResult.flatMap { stream in
                return stream.writeAndFlush(context)
            }.flatMap {
                responsePromise.futureResult
            }
    }
kylebrowning commented 5 years ago

Then you just construct the byte buffer yourself, but there's no guarantees the payload is legit safe.

daniel-beard commented 5 years ago

@kylebrowning I think that will work, you don't need the JSONEncoder param though. I'm cool with the payload being able to be malformed.

kylebrowning commented 5 years ago

Correct I just did it quick and dirty haha.

kylebrowning commented 5 years ago

I just tested this, and it seems to work just fine, not sure if @weissi Has any other ideas.

public func send<Notification: APNSwiftNotification>(_ notification: Notification, pushType: APNSwiftConnection.PushType, to deviceToken: String, with encoder: JSONEncoder = JSONEncoder(), expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil) -> EventLoopFuture<Void> {
    let data: Data = try! encoder.encode(notification)

    var buffer = ByteBufferAllocator().buffer(capacity: data.count)
    buffer.writeBytes(data)
    return send(withByteBuffer: buffer, pushType: pushType, to: deviceToken)
}

public func send(withByteBuffer buffer: ByteBuffer, pushType: APNSwiftConnection.PushType, to deviceToken: String, expiration: Date? = nil, priority: Int? = nil, collapseIdentifier: String? = nil, topic: String? = nil) -> EventLoopFuture<Void> {
        let streamPromise = channel.eventLoop.makePromise(of: Channel.self)
        multiplexer.createStreamChannel(promise: streamPromise) { channel, streamID in
            let handlers: [ChannelHandler] = [
                HTTP2ToHTTP1ClientCodec(streamID: streamID, httpProtocol: .https),
                APNSwiftRequestEncoder(deviceToken: deviceToken, configuration: self.configuration, bearerToken: self.bearerTokenFactory?.currentBearerToken, pushType: pushType, expiration: expiration, priority: priority, collapseIdentifier: collapseIdentifier, topic: topic),
                APNSwiftResponseDecoder(),
                APNSwiftStreamHandler(),
            ]
            return channel.pipeline.addHandlers(handlers)
        }

        let responsePromise = channel.eventLoop.makePromise(of: Void.self)
        let context = APNSwiftRequestContext(
            request: buffer,
            responsePromise: responsePromise
        )

        return streamPromise.futureResult.flatMap { stream in
            return stream.writeAndFlush(context)
        }.flatMap {
            responsePromise.futureResult
        }
}
weissi commented 5 years ago

@daniel-beard wait, you want to inject an arbitrary string into the JSON?

kylebrowning commented 5 years ago

@weissi It appears so. Sounds like they attach arbitrary data on the push notification. So aps still exists, but then they use basically what appears to be [String: AnyObject]

weissi commented 5 years ago

@kylebrowning hmm, if it were actually structured data, I think what I proposed would’ve worked, no? But I think @daniel-beard wants to have the aps object to be an arbitrary sequence of bytes they get from elsewhere. If that is the case, then Codable won’t support that because it’s no longer typed.

I’m happy adding a function like you propose, I think the parameter name should maybe be rawBytes or so instead of withByteBuffer (the word ‘with’ is usually omitted and ‘ByteBuffer’ is the type name already so doesn’t add much value)

kylebrowning commented 5 years ago

@weissi Main problem is that APNSwiftPayload is not fully Codable but just Encodable so updating that would be API breaking.

Furthermore I went down the route of testing that and couldn't get it to work with structured data. That being said I tried for like 5 minutes.

Finally I believe @daniel-beard is saying that he won't know the input, so it all has to be arbitrary.

daniel-beard commented 5 years ago

@kylebrowning yes, that's correct. I don't have structured data, and want to be able to directly load logged payloads from disk and send them.

kylebrowning commented 5 years ago

@daniel-beard If you don't mind, Im going to let #51 marinate for around 24 hours and then cut a new release of 1.2.1

daniel-beard commented 5 years ago

Sounds good, thanks for the quick responses and implementation, much appreciated!

kylebrowning commented 5 years ago

closing in favor of PR #51. Can continue discussion there if needed.