From cad603059288477da5014f08a29ff7354e1e9cc4 Mon Sep 17 00:00:00 2001 From: JR Conlin Date: Thu, 22 Sep 2022 12:42:12 -0700 Subject: [PATCH 1/6] Feat/deserialize and certificate parts (#3) * Derive Deserialize for APS and sub-structs * Add Client::certificate_parts to support more use cases * Fix stack-overflow in Error's Display impl Also removed some deprecated impls and changed the "reason" string from the debug string to the human-readable message. * Bump to v0.6.2 * Migrate to new maintainers * fix: Adds tcp feature to hyper (#61) Co-authored-by: Mark Drobnak Co-authored-by: Julius de Bruijn Co-authored-by: Julius de Bruijn Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Neswold Co-authored-by: Alex Hortopan Co-authored-by: Harry Bairstow Co-authored-by: Austin Evans --- .envrc | 4 + .github/pull_request_template.md | 25 +++ .github/workflows/ci.yml | 111 ++++++++++++ .gitignore | 2 + .travis.yml | 23 --- .travis/docs.sh | 52 ------ CHANGELOG.md | 92 +++++----- Cargo.toml | 18 +- LICENSE | 2 +- README.md | 18 +- a2_travis.enc | Bin 416 -> 0 bytes examples/certificate_client.rs | 33 +--- examples/token_client.rs | 26 +-- rustfmt.toml | 4 +- shell.nix | 10 ++ src/client.rs | 160 +++++++---------- src/error.rs | 94 +++------- src/lib.rs | 30 +--- src/request/notification.rs | 6 +- src/request/notification/localized.rs | 237 +++++++++++++------------- src/request/notification/options.rs | 18 +- src/request/notification/plain.rs | 36 ++-- src/request/notification/silent.rs | 33 ++-- src/request/notification/web.rs | 127 ++++++++++++++ src/request/payload.rs | 23 +-- src/response.rs | 40 +---- src/signer.rs | 56 +++--- 27 files changed, 659 insertions(+), 621 deletions(-) create mode 100644 .envrc create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100755 .travis/docs.sh delete mode 100644 a2_travis.enc create mode 100644 shell.nix create mode 100644 src/request/notification/web.rs diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..6882bef9 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +if command -v nix-shell &> /dev/null +then + use nix +fi diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..41161cd8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +# Description + + + +Resolves # (issue) + +## How Has This Been Tested? + + + + + +## Due Dilligence + +* [ ] Breaking change +* [ ] Requires a documentation update +* [ ] Requires a e2e/integration test update \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..706f8128 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: ci + +on: + pull_request: + paths-ignore: + - '.github/**' + - 'README.md' + + push: + branches: ['main'] + paths-ignore: + - '.github/**' + - 'README.md' + +concurrency: + # Support push/pr as event types with different behaviors each: + # 1. push: queue up builds + # 2. pr: only allow one run per PR + group: ${{ github.workflow }}-${{ github.event.type }}${{ github.event.pull_request.number }} + # If there is already a workflow running for the same pull request, cancel it + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + tasks: + name: "[${{ matrix.os }}] ${{ matrix.cargo.name }}" + runs-on: "${{ matrix.os }}" + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + rust: + - stable + cargo: + - name: "Clippy" + cmd: clippy + args: -- -D clippy::all + cache: {} + - name: "Formatting" + cmd: fmt + args: -- --check + cache: {} + - name: "Unit Tests" + cmd: test + args: --all-features + cache: { sharedKey: "tests" } + include: + - os: ubuntu-latest + sccache-path: /home/runner/.cache/sccache + env: + RUST_BACKTRACE: full + RUSTC_WRAPPER: sccache + SCCACHE_CACHE_SIZE: 1G + SCCACHE_DIR: ${{ matrix.sccache-path }} + steps: + # Checkout code + - name: "Git checkout" + uses: actions/checkout@v2 + + # Install sccache + - name: "Install sccache" + if: matrix.os == 'ubuntu-latest' + env: + SCCACHE_URL: https://github.com/mozilla/sccache/releases/download + SCCACHE_VERSION: v0.2.15 + run: | + SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl + curl -sSL "$SCCACHE_URL/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz + install -vDm 755 "$SCCACHE_FILE/sccache" "$HOME/.local/bin/sccache" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + # Install Rust toolchain + - name: "Install Rust ${{ matrix.rust }}" + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + profile: minimal + override: true + + # Rebuild cache + - name: Cache cargo registry + uses: Swatinem/rust-cache@3bb3a9a087029c7bc392586cdc88cb6f66b9c6ef + with: ${{ matrix.cargo.cache }} + continue-on-error: false + + - name: Cache sccache + uses: actions/cache@v2 + continue-on-error: false + with: + path: ${{ matrix.sccache-path }} + key: ${{ runner.os }}-sccache-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-sccache- + + # Run job + - name: "Start sccache server" + run: | + sccache --stop-server || true + sccache --start-server + + - name: "Task ${{ matrix.cargo.name }}" + uses: actions-rs/cargo@v1 + with: + command: ${{ matrix.cargo.cmd }} + args: ${{ matrix.cargo.args }} + + - name: "Print sccache stats" + run: sccache --show-stats + + - name: "Stop sccache server" + run: sccache --stop-server || true diff --git a/.gitignore b/.gitignore index 2b604352..fee1f314 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ Cargo.lock /examples/*.p8 /examples/*.p12 + +.direnv diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b329b784..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: rust -matrix: - include: - - rust: stable - - rust: beta - - rust: nightly - -cache: - apt: true - directories: - - target/debug/deps - - target/debug/build - -script: - - cargo build - - cargo test -env: - global: - - ENCRYPTION_LABEL: "927b4e4f8874" - - COMMIT_AUTHOR_EMAIL: "julius@nauk.io" - -after_success: - - '[ $TRAVIS_PULL_REQUEST = false ] && [ $TRAVIS_RUST_VERSION = stable ] && { [ "$TRAVIS_TAG" != "" ] || [ "$TRAVIS_BRANCH" == "master" ]; } && ./.travis/docs.sh' diff --git a/.travis/docs.sh b/.travis/docs.sh deleted file mode 100755 index c3cdbdf7..00000000 --- a/.travis/docs.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash - -set -o errexit - -shopt -s globstar - -cargo doc --no-deps - -REPO=`git config remote.origin.url` -SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} -SHA=`git rev-parse --verify HEAD` - -git clone --branch gh-pages $REPO deploy_docs -cd deploy_docs - -git config user.name "Julius de Bruijn" -git config user.email "julius.debruijn@360dialog.com" - -if [ "$TRAVIS_TAG" = "" ]; then - rm -rf master - mv ../target/doc ./master - echo "" > ./master/index.html -else - rm -rf $TRAVIS_TAG - mv ../target/doc ./$TRAVIS_TAG - echo "" > ./$TRAVIS_TAG/index.html - - latest=$(echo * | tr " " "\n" | sort -V -r | head -n1) - if [ "$TRAVIS_TAG" = "$latest" ]; then - echo "" > index.html - fi -fi - -git add -A . -git commit -m "rebuild pages at ${TRAVIS_COMMIT}" - -ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" -ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" -ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} -ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} - -openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../a2_travis.enc -out a2_travis -d -chmod 600 a2_travis -eval `ssh-agent -s` -ssh-add a2_travis - -echo -echo "Pushing docs..." -git push $SSH_REPO gh-pages -echo -echo "Docs published." -echo diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef843cc..130b6838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,73 +1,81 @@ -# Changelog + # Changelog -## v0.5.2 + ## v0.6.2 -- Fix `TooManyProviderTokenUpdates` issue [#44](https://github.com/pimeys/a2/pull/44) + - Add support for Safari web push -## v0.5.1 + ## v0.6.0 -- Enforcing static lifetimes for client send [#43](https://github.com/pimeys/a2/pull/43) + - Update to Tokio 1.0 -## v0.5.0 + ## v0.5.2 -- Stable Hyper 0.13 and Tokio 0.2 support + - Fix `TooManyProviderTokenUpdates` issue [#44](https://github.com/pimeys/a2/pull/44) -## v0.5.0-alpha.6 + ## v0.5.1 -- Fix a bug in ALPN resolving. + - Enforcing static lifetimes for client send [#43](https://github.com/pimeys/a2/pull/43) -## v0.5.0-alpha.5 + ## v0.5.0 -- And down to async-std's new `ToSocketAddrs` resolver + - Stable Hyper 0.13 and Tokio 0.2 support -## v0.5.0-alpha.4 + ## v0.5.0-alpha.6 -- Switch to Hyper's GaiResolver to go around of a bug in the latest nightly. + - Fix a bug in ALPN resolving. -## v0.5.0-alpha.1 + ## v0.5.0-alpha.5 -- Update to `std::future` and async/await, requiring a nightly compiler for now. + - And down to async-std's new `ToSocketAddrs` resolver -## v0.4.1 + ## v0.5.0-alpha.4 -- Fix token_client example not building due to unresolvable `as_ref()`. [#35](https://github.com/pimeys/a2/pull/35) -- Move indoc to dev-dependencies so that crates depending on us don't need it. [#36](https://github.com/pimeys/a2/pull/36) -- Move pretty_env_logger to dev-dependencies, it's not useful to crates depending on us. [#37](https://github.com/pimeys/a2/pull/37) -- Remove unused tokio-io dependency. [#38](https://github.com/pimeys/a2/pull/38) + - Switch to Hyper's GaiResolver to go around of a bug in the latest nightly. -## v0.4.0 + ## v0.5.0-alpha.1 -Introduces two changes that are a bit more drastic and hence increasing the -major version. The 2018 syntax requires a Rust compiler version 1.31 or newer -and the locking primitive hasn't been measured with high traffic yet in a2. + - Update to `std::future` and async/await, requiring a nightly compiler for now. -- Upgrade to Rust 2018 syntax [#29](https://github.com/pimeys/a2/pull/29) -- Switch from deprecated crossbeam ArcCell to parking_lot RwLock - [#32](https://github.com/pimeys/a2/pull/32) + ## v0.4.1 -## v0.3.5 + - Fix token_client example not building due to unresolvable `as_ref()`. [#35](https://github.com/pimeys/a2/pull/35) + - Move indoc to dev-dependencies so that crates depending on us don't need it. [#36](https://github.com/pimeys/a2/pull/36) + - Move pretty_env_logger to dev-dependencies, it's not useful to crates depending on us. [#37](https://github.com/pimeys/a2/pull/37) + - Remove unused tokio-io dependency. [#38](https://github.com/pimeys/a2/pull/38) -- Implement `fmt::Display` for `ErrorReason` [#28](https://github.com/pimeys/a2/pull/28) + ## v0.4.0 -## v0.3.4 + Introduces two changes that are a bit more drastic and hence increasing the + major version. The 2018 syntax requires a Rust compiler version 1.31 or newer + and the locking primitive hasn't been measured with high traffic yet in a2. -- Changing the author email due to company breakdown to the private one. + - Upgrade to Rust 2018 syntax [#29](https://github.com/pimeys/a2/pull/29) + - Switch from deprecated crossbeam ArcCell to parking_lot RwLock + [#32](https://github.com/pimeys/a2/pull/32) -## v0.3.3 + ## v0.3.5 -- Taking the alpn connector out to its own crate, using tokio-dns for resolving + - Implement `fmt::Display` for `ErrorReason` [#28](https://github.com/pimeys/a2/pull/28) -## v0.3.2 + ## v0.3.4 -- OK responses don't have a body, so we don't need to handle it and gain a bit - more performance + - Changing the author email due to company breakdown to the private one. -## v0.3.1 + ## v0.3.3 -- Bunch of examples to the builder documentation + - Taking the alpn connector out to its own crate, using tokio-dns for resolving -## v0.3.0 + ## v0.3.2 -- Convert the API to not clone the input data, using references until - converting to JSON, remove tokio-service dependency - [#25](https://github.com/pimeys/a2/pull/25) + - OK responses don't have a body, so we don't need to handle it and gain a bit + more performance + + ## v0.3.1 + + - Bunch of examples to the builder documentation + + ## v0.3.0 + + - Convert the API to not clone the input data, using references until + converting to JSON, remove tokio-service dependency + [#25](https://github.com/pimeys/a2/pull/25) diff --git a/Cargo.toml b/Cargo.toml index 8a2826ff..0ce2ba73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "a2" -version = "0.5.2" +version = "0.6.2" authors = [ + "Harry Bairstow ", "Julius de Bruijn ", "Sergey Tkachenko ", ] @@ -9,8 +10,8 @@ license = "MIT" readme = "README.md" description = "A native, asynchronous Apple push notification client" keywords = ["apns", "apple", "push", "async", "http2"] -repository = "https://github.com/pimeys/a2.git" -homepage = "https://github.com/pimeys/a2" +repository = "https://github.com/walletconnect/a2.git" +homepage = "https://github.com/walletconnect/a2" documentation = "https://docs.rs/a2" edition = "2018" @@ -19,16 +20,17 @@ serde = "1" erased-serde = "0.3" serde_derive = "1" serde_json = "1" +thiserror = "1" openssl = "0.10" futures = "0.3" http = "0.2" -base64 = "0.12" +base64 = "0.13" log = "0.4" -hyper = "0.13" -hyper-alpn = "0.2" +hyper = { version = "0.14", features = ["client", "http2", "tcp"] } +hyper-alpn = "0.3" [dev-dependencies] argparse = "0.2" pretty_env_logger = "0.4" -indoc = "0.3" -tokio = { version = "0.2", features = ["rt-threaded", "macros"] } +indoc = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/LICENSE b/LICENSE index 3bf8c3b5..49bd034d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Sergey Tkachenko +Copyright (c) 2022 WalletConnect Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c7055caf..70a40add 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # a2 -[![Travis Build Status](https://travis-ci.org/pimeys/a2.svg?branch=master)](https://travis-ci.org/pimeys/a2) +[![CI Status](https://github.com/walletconnect/a2/actions/workflows/ci.yml/badge.svg)](https://github.com/walletconnect/a2/actions/workflows/ci.yml) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -[![crates.io](http://meritbadge.herokuapp.com/a2)](https://crates.io/crates/a2) +[![crates.io](https://img.shields.io/crates/v/a2)](https://crates.io/crates/a2) HTTP/2 Apple Push Notification Service for Rust using Tokio and async sending. -## Help needed - -The main author is not currently owning any Apple phones, so would be nice to have some help from a co-author with needed devices and an Apple developer account. If you happen to have them and are willing to help, please contact! - ## Requirements Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or later. @@ -17,7 +13,7 @@ Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or ## Documentation * [Released](https://docs.rs/a2/) -* [Master](https://pimeys.github.io/a2/master/) +* [Master](https://walletconnect.github.io/a2/master/) ## Features @@ -36,9 +32,9 @@ Needs a Tokio executor version 0.2 or later and Rust compiler version 1.39.0 or The library supports connecting to Apple Push Notification service [either using a -certificate](https://github.com/pimeys/a2/blob/master/examples/certificate_client.rs) +certificate](https://github.com/walletconnect/a2/blob/master/examples/certificate_client.rs) with a password [or a private -key](https://github.com/pimeys/a2/blob/master/examples/token_client.rs) with +key](https://github.com/walletconnect/a2/blob/master/examples/token_client.rs) with a team id and key id. Both are available from your Apple account and with both it is possible to send push notifications to one application. @@ -68,7 +64,3 @@ for production use: ## Tests `cargo test` - -## Contact - -oh_lawd @ IRC (Freenode, Mozilla) diff --git a/a2_travis.enc b/a2_travis.enc deleted file mode 100644 index 4b16474df29e772fef159a69899e297ff3aa9e8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmV;R0bl+`wW`n?qKLn}>BJARMLN%AsDjjHpnuBPr*{6$$^Qp-io@V(`QA9`a?>AH z6rG+1Q9#i6VR;*2Nq{>-A_GFV$-Vo5)S5g3hb1T6jY*0JDzU9wR>8RuF$y+$9heG- zFfhs9z`);o`EPYK&+hXo5nm){uHA1#O#Kje!~4_-+MQh2@FAJjs8Alk7j|grQVSjh z*6O#r_0`c9Q3$q2O^g< zsFBUAa6dN?rL+!L_-<=Lq{IS-7C26|+={b-FHKsW+?sn>aaKy?nc?eJ1CKizySo}0 z@jRo9qv3$#*4w;t-oq_@M%l`W#T;-v~{18CVX z+Rv%K`Tc@h@49pBP1b((4_MBKfd!rDAhOcmS-f{15~ZqcYy&$}YH+-xR(NHnP5@C` KENkh(H7}0O!q5Hy diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index 0c10c8f2..311ca1d2 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -1,14 +1,6 @@ -use tokio; -use pretty_env_logger; +use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; use std::fs::File; -use a2::{ - NotificationBuilder, - NotificationOptions, - PlainNotificationBuilder, - Client, - Endpoint, -}; // An example client connectiong to APNs with a certificate and key #[tokio::main] @@ -25,25 +17,16 @@ async fn main() -> Result<(), Box> { { let mut ap = ArgumentParser::new(); ap.set_description("APNs certificate-based push"); - ap.refer(&mut certificate_file).add_option( - &["-c", "--certificate"], - Store, - "Certificate PKCS12 file location", - ); + ap.refer(&mut certificate_file) + .add_option(&["-c", "--certificate"], Store, "Certificate PKCS12 file location"); ap.refer(&mut password) .add_option(&["-p", "--password"], Store, "Certificate password"); - ap.refer(&mut device_token).add_option( - &["-d", "--device_token"], - Store, - "APNs device token", - ); + ap.refer(&mut device_token) + .add_option(&["-d", "--device_token"], Store, "APNs device token"); ap.refer(&mut message) .add_option(&["-m", "--message"], Store, "Notification message"); - ap.refer(&mut sandbox).add_option( - &["-s", "--sandbox"], - StoreTrue, - "Use the development APNs servers", - ); + ap.refer(&mut sandbox) + .add_option(&["-s", "--sandbox"], StoreTrue, "Use the development APNs servers"); ap.refer(&mut topic) .add_option(&["-o", "--topic"], StoreOption, "APNS topic"); ap.parse_args_or_exit(); @@ -63,7 +46,7 @@ async fn main() -> Result<(), Box> { let client = Client::certificate(&mut certificate, &password, endpoint).unwrap(); let options = NotificationOptions { - apns_topic: topic.as_ref().map(|s| &**s), + apns_topic: topic.as_deref(), ..Default::default() }; diff --git a/examples/token_client.rs b/examples/token_client.rs index b6dfae7b..7646b266 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -1,15 +1,7 @@ -use tokio; -use pretty_env_logger; use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; use std::fs::File; -use a2::{ - Client, - Endpoint, - NotificationBuilder, - NotificationOptions, - PlainNotificationBuilder, -}; +use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; // An example client connectiong to APNs with a JWT token #[tokio::main] @@ -33,18 +25,12 @@ async fn main() -> Result<(), Box> { .add_option(&["-t", "--team_id"], Store, "APNs team ID"); ap.refer(&mut key_id) .add_option(&["-k", "--key_id"], Store, "APNs key ID"); - ap.refer(&mut device_token).add_option( - &["-d", "--device_token"], - Store, - "APNs device token", - ); + ap.refer(&mut device_token) + .add_option(&["-d", "--device_token"], Store, "APNs device token"); ap.refer(&mut message) .add_option(&["-m", "--message"], Store, "Notification message"); - ap.refer(&mut sandbox).add_option( - &["-s", "--sandbox"], - StoreTrue, - "Use the development APNs servers", - ); + ap.refer(&mut sandbox) + .add_option(&["-s", "--sandbox"], StoreTrue, "Use the development APNs servers"); ap.refer(&mut topic) .add_option(&["-o", "--topic"], StoreOption, "APNS topic"); ap.parse_args_or_exit(); @@ -64,7 +50,7 @@ async fn main() -> Result<(), Box> { let client = Client::token(&mut private_key, key_id, team_id, endpoint).unwrap(); let options = NotificationOptions { - apns_topic: topic.as_ref().map(|s| &**s), + apns_topic: topic.as_deref(), ..Default::default() }; diff --git a/rustfmt.toml b/rustfmt.toml index 7bcfcb7d..9328e6c1 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ -format_strings = false -reorder_imports = true +max_width = 120 +edition = "2018" diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..2ac401a8 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: + +with pkgs; + +mkShell { + buildInputs = with pkgs; [ + openssl + pkg-config + ]; +} diff --git a/src/client.rs b/src/client.rs index b6c80006..834e981f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,26 +1,20 @@ //! The client module for sending requests and parsing responses -use crate::signer::Signer; -use hyper_alpn::AlpnConnector; use crate::error::Error; use crate::error::Error::ResponseError; +use crate::signer::Signer; +use hyper_alpn::AlpnConnector; -use futures::stream::StreamExt; -use hyper::{ - self, - Client as HttpClient, - StatusCode, - Body -}; -use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; use crate::request::payload::Payload; use crate::response::Response; +use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; +use hyper::{self, Body, Client as HttpClient, StatusCode}; +use openssl::pkcs12::Pkcs12; use serde_json; -use std::{fmt, str}; -use std::time::Duration; use std::future::Future; -use openssl::pkcs12::Pkcs12; use std::io::Read; +use std::time::Duration; +use std::{fmt, str}; /// The APNs service endpoint to connect. #[derive(Debug, Clone)] @@ -57,11 +51,7 @@ pub struct Client { } impl Client { - fn new( - connector: AlpnConnector, - signer: Option, - endpoint: Endpoint, - ) -> Client { + fn new(connector: AlpnConnector, signer: Option, endpoint: Endpoint) -> Client { let mut builder = HttpClient::builder(); builder.pool_idle_timeout(Some(Duration::from_secs(600))); builder.http2_only(true); @@ -76,11 +66,7 @@ impl Client { /// Create a connection to APNs using the provider client certificate which /// you obtain from your [Apple developer /// account](https://developer.apple.com/account/). - pub fn certificate( - certificate: &mut R, - password: &str, - endpoint: Endpoint, - ) -> Result + pub fn certificate(certificate: &mut R, password: &str, endpoint: Endpoint) -> Result where R: Read, { @@ -88,10 +74,16 @@ impl Client { certificate.read_to_end(&mut cert_der)?; let pkcs = Pkcs12::from_der(&cert_der)?.parse(password)?; - let connector = AlpnConnector::with_client_cert( - &pkcs.cert.to_pem()?, - &pkcs.pkey.private_key_to_pem_pkcs8()?, - )?; + let connector = AlpnConnector::with_client_cert(&pkcs.cert.to_pem()?, &pkcs.pkey.private_key_to_pem_pkcs8()?)?; + + Ok(Self::new(connector, None, endpoint)) + } + + /// Create a connection to APNs using the raw PEM-formatted certificate and + /// key, extracted from the provider client certificate you obtain from your + /// [Apple developer account](https://developer.apple.com/account/). + pub fn certificate_parts(cert: &[u8], key: &[u8], endpoint: Endpoint) -> Result { + let connector = AlpnConnector::with_client_cert(cert, key)?; Ok(Self::new(connector, None, endpoint)) } @@ -100,12 +92,7 @@ impl Client { /// request with a signature using a private key, key id and team id /// provisioned from your [Apple developer /// account](https://developer.apple.com/account/). - pub fn token( - pkcs8_pem: R, - key_id: S, - team_id: T, - endpoint: Endpoint, - ) -> Result + pub fn token(pkcs8_pem: R, key_id: S, team_id: T, endpoint: Endpoint) -> Result where S: Into, T: Into, @@ -132,30 +119,16 @@ impl Client { .headers() .get("apns-id") .and_then(|s| s.to_str().ok()) - .map(|id| String::from(id)); + .map(String::from); match response.status() { - StatusCode::OK => { - Ok(Response { - apns_id, - error: None, - code: response.status().as_u16(), - }) - }, + StatusCode::OK => Ok(Response { + apns_id, + error: None, + code: response.status().as_u16(), + }), status => { - let content_length: usize = response - .headers() - .get(CONTENT_LENGTH) - .and_then(|s| s.to_str().ok()) - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let mut body: Vec = Vec::with_capacity(content_length); - let mut chunks = response.into_body(); - - while let Some(chunk) = chunks.next().await { - body.extend_from_slice(&chunk?); - } + let body = hyper::body::to_bytes(response).await?; Err(ResponseError(Response { apns_id, @@ -168,10 +141,7 @@ impl Client { } fn build_request(&self, payload: Payload<'_>) -> hyper::Request { - let path = format!( - "https://{}/3/device/{}", - self.endpoint, payload.device_token - ); + let path = format!("https://{}/3/device/{}", self.endpoint, payload.device_token); let mut builder = hyper::Request::builder() .uri(&path) @@ -179,24 +149,24 @@ impl Client { .header(CONTENT_TYPE, "application/json"); if let Some(ref apns_priority) = payload.options.apns_priority { - builder = builder.header("apns-priority", format!("{}", apns_priority).as_bytes()); + builder = builder.header("apns-priority", apns_priority.to_string().as_bytes()); } - if let Some(ref apns_id) = payload.options.apns_id { + if let Some(apns_id) = payload.options.apns_id { builder = builder.header("apns-id", apns_id.as_bytes()); } if let Some(ref apns_expiration) = payload.options.apns_expiration { - builder = builder.header("apns-expiration", format!("{}", apns_expiration).as_bytes()); + builder = builder.header("apns-expiration", apns_expiration.to_string().as_bytes()); } if let Some(ref apns_collapse_id) = payload.options.apns_collapse_id { - builder = builder.header("apns-collapse-id", format!("{}", apns_collapse_id.value).as_bytes()); + builder = builder.header("apns-collapse-id", apns_collapse_id.value.as_bytes()); } - if let Some(ref apns_topic) = payload.options.apns_topic { + if let Some(apns_topic) = payload.options.apns_topic { builder = builder.header("apns-topic", apns_topic.as_bytes()); } if let Some(ref signer) = self.signer { - let auth = signer.with_signature(|signature| { - format!("Bearer {}", signature) - }).unwrap(); + let auth = signer + .with_signature(|signature| format!("Bearer {}", signature)) + .unwrap(); builder = builder.header(AUTHORIZATION, auth.as_bytes()); } @@ -212,15 +182,15 @@ impl Client { #[cfg(test)] mod tests { use super::*; - use crate::request::notification::PlainNotificationBuilder; use crate::request::notification::NotificationBuilder; - use crate::request::notification::{NotificationOptions, Priority, CollapseId}; - use hyper_alpn::AlpnConnector; - use hyper::Method; - use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; + use crate::request::notification::PlainNotificationBuilder; + use crate::request::notification::{CollapseId, NotificationOptions, Priority}; use crate::signer::Signer; + use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; + use hyper::Method; + use hyper_alpn::AlpnConnector; - const PRIVATE_KEY: &'static str = indoc!( + const PRIVATE_KEY: &str = indoc!( "-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 @@ -279,10 +249,7 @@ mod tests { let payload_json = payload.to_json_string().unwrap(); let content_length = request.headers().get(CONTENT_LENGTH).unwrap().to_str().unwrap(); - assert_eq!( - &format!("{}", payload_json.len()), - content_length - ); + assert_eq!(&format!("{}", payload_json.len()), content_length); } #[test] @@ -297,7 +264,13 @@ mod tests { #[test] fn test_request_authorization_with_a_signer() { - let signer = Signer::new(PRIVATE_KEY.as_bytes(), "89AFRD1X22", "ASDFQWERTY", Duration::from_secs(100)).unwrap(); + let signer = Signer::new( + PRIVATE_KEY.as_bytes(), + "89AFRD1X22", + "ASDFQWERTY", + Duration::from_secs(100), + ) + .unwrap(); let builder = PlainNotificationBuilder::new("test"); let payload = builder.build("a_test_id", Default::default()); @@ -346,7 +319,7 @@ mod tests { NotificationOptions { apns_priority: Some(Priority::High), ..Default::default() - } + }, ); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); @@ -360,10 +333,7 @@ mod tests { fn test_request_with_default_apns_id() { let builder = PlainNotificationBuilder::new("test"); - let payload = builder.build( - "a_test_id", - Default::default(), - ); + let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -395,10 +365,7 @@ mod tests { fn test_request_with_default_apns_expiration() { let builder = PlainNotificationBuilder::new("test"); - let payload = builder.build( - "a_test_id", - Default::default(), - ); + let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -430,10 +397,7 @@ mod tests { fn test_request_with_default_apns_collapse_id() { let builder = PlainNotificationBuilder::new("test"); - let payload = builder.build( - "a_test_id", - Default::default(), - ); + let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -465,10 +429,7 @@ mod tests { fn test_request_with_default_apns_topic() { let builder = PlainNotificationBuilder::new("test"); - let payload = builder.build( - "a_test_id", - Default::default(), - ); + let payload = builder.build("a_test_id", Default::default()); let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload); @@ -503,17 +464,16 @@ mod tests { let client = Client::new(AlpnConnector::new(), None, Endpoint::Production); let request = client.build_request(payload.clone()); - let mut body: Vec = Vec::new(); - let mut chunks = request.into_body(); + let body = hyper::body::to_bytes(request).await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + /* while let Some(chunk) = chunks.next().await { body.extend_from_slice(&chunk.unwrap()); } let body_str = String::from_utf8(body).unwrap(); + // */ - assert_eq!( - payload.to_json_string().unwrap(), - body_str, - ); + assert_eq!(payload.to_json_string().unwrap(), body_str,); } } diff --git a/src/error.rs b/src/error.rs index 255eb114..fe91d8f2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,95 +1,41 @@ //! Error and result module -use crate::response::{ErrorBody, Response}; -use openssl::error::ErrorStack; -use serde_json::Error as SerdeError; -use std::convert::From; -use std::error::Error as StdError; -use std::fmt; -use std::io::Error as IoError; +use crate::response::Response; +use std::io; +use thiserror::Error; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum Error { /// User request or Apple response JSON data was faulty. - SerializeError, + #[error("Error serializing to JSON: {0}")] + SerializeError(#[from] serde_json::Error), /// A problem connecting to APNs servers. - ConnectionError, - - /// APNs couldn't response in a timely manner, if using - /// [send_with_timeout](client/struct.Client.html#method.send_with_timeout) - TimeoutError, + #[error("Error connecting to APNs: {0}")] + ConnectionError(#[from] hyper::Error), /// Couldn't generate an APNs token with the given key. - SignerError(String), + #[error("Error creating a signature: {0}")] + SignerError(#[from] openssl::error::ErrorStack), /// APNs couldn't accept the notification. Contains /// [Response](response/struct.Response.html) with additional /// information. + #[error( + "Notification was not accepted by APNs (reason: {})", + .0.error + .as_ref() + .map(|e| e.reason.to_string()) + .unwrap_or_else(|| "Unknown".to_string()) + )] ResponseError(Response), /// Invalid option values given in /// [NotificationOptions](request/notification/struct.NotificationOptions.html) + #[error("Invalid options for APNs payload: {0}")] InvalidOptions(String), - /// TLS connection failed - TlsError(String), - /// Error reading the certificate or private key. - ReadError(String), -} - -impl<'a> fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::ResponseError(Response { - error: Some(ErrorBody { ref reason, .. }), - .. - }) => write!(fmt, "{} (reason: {:?})", self, reason), - _ => write!(fmt, "{}", self), - } - } -} - -impl<'a> StdError for Error { - fn description(&self) -> &str { - match self { - Error::SerializeError => "Error serializing to JSON", - Error::ConnectionError => "Error connecting to APNs", - Error::SignerError(_) => "Error creating a signature", - Error::ResponseError(_) => "Notification was not accepted by APNs", - Error::InvalidOptions(_) => "Invalid options for APNs payload", - Error::TlsError(_) => "Error in creating a TLS connection", - Error::ReadError(_) => "Error in reading a certificate file", - Error::TimeoutError => "Timeout in sending a push notification", - } - } - - fn cause(&self) -> Option<&dyn StdError> { - None - } -} - -impl From for Error { - fn from(_: SerdeError) -> Error { - Error::SerializeError - } -} - -impl From for Error { - fn from(e: ErrorStack) -> Error { - Error::SignerError(format!("{}", e)) - } -} - -impl From for Error { - fn from(e: IoError) -> Error { - Error::ReadError(format!("{}", e)) - } -} - -impl From for Error { - fn from(_: hyper::error::Error) -> Error { - Error::ConnectionError - } + #[error("Error in reading a certificate file: {0}")] + ReadError(#[from] io::Error), } diff --git a/src/lib.rs b/src/lib.rs index 2c48b80b..11a8585f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ //! #[macro_use] extern crate serde_derive; //! //! use a2::{ -//! Client, Endpoint, SilentNotificationBuilder, NotificationBuilder, NotificationOptions, +//! Client, Endpoint, SilentNotificationBuilder, NotificationBuilder, NotificationOptions, //! Priority, //! }; //! use std::fs::File; @@ -119,33 +119,19 @@ extern crate indoc; #[macro_use] extern crate log; -pub mod request; +pub mod client; pub mod error; +pub mod request; pub mod response; -pub mod client; mod signer; -pub use crate::request::{ - notification::{ - NotificationBuilder, - LocalizedNotificationBuilder, - PlainNotificationBuilder, - SilentNotificationBuilder, - CollapseId, - NotificationOptions, - Priority, - } +pub use crate::request::notification::{ + CollapseId, LocalizedNotificationBuilder, NotificationBuilder, NotificationOptions, PlainNotificationBuilder, + Priority, SilentNotificationBuilder, WebNotificationBuilder, WebPushAlert, }; -pub use crate::response::{ - Response, - ErrorBody, - ErrorReason, -}; +pub use crate::response::{ErrorBody, ErrorReason, Response}; -pub use crate::client::{ - Endpoint, - Client, -}; +pub use crate::client::{Client, Endpoint}; pub use crate::error::Error; diff --git a/src/request/notification.rs b/src/request/notification.rs index 98354323..f57b66b7 100644 --- a/src/request/notification.rs +++ b/src/request/notification.rs @@ -1,14 +1,16 @@ //! The `aps` notification content builders mod localized; +mod options; mod plain; mod silent; -mod options; +mod web; pub use self::localized::{LocalizedAlert, LocalizedNotificationBuilder}; +pub use self::options::{CollapseId, NotificationOptions, Priority}; pub use self::plain::PlainNotificationBuilder; pub use self::silent::SilentNotificationBuilder; -pub use self::options::{CollapseId, NotificationOptions, Priority}; +pub use self::web::{WebNotificationBuilder, WebPushAlert}; use crate::request::payload::Payload; diff --git a/src/request/notification/localized.rs b/src/request/notification/localized.rs index 722f98a4..e6846a05 100644 --- a/src/request/notification/localized.rs +++ b/src/request/notification/localized.rs @@ -1,34 +1,34 @@ use crate::request::notification::{NotificationBuilder, NotificationOptions}; use crate::request::payload::{APSAlert, Payload, APS}; -use std::{ - collections::BTreeMap, - borrow::Cow, -}; +use std::{borrow::Cow, collections::BTreeMap}; -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct LocalizedAlert<'a> { - title: &'a str, - body: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - title_loc_key: Option<&'a str>, + pub title_loc_key: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - title_loc_args: Option>>, + pub title_loc_args: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - action_loc_key: Option<&'a str>, + pub action_loc_key: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - loc_key: Option<&'a str>, + pub loc_key: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - loc_args: Option>>, + pub loc_args: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - launch_image: Option<&'a str>, + pub launch_image: Option<&'a str>, } /// A builder to create a localized APNs payload. @@ -37,8 +37,9 @@ pub struct LocalizedAlert<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; -/// # fn main() { -/// let mut builder = LocalizedNotificationBuilder::new("Hi there", "What's up?"); +/// let mut builder = LocalizedNotificationBuilder::default(); +/// builder.set_title("Hi there"); +/// builder.set_body("What's up?"); /// builder.set_badge(420); /// builder.set_category("cat1"); /// builder.set_sound("prööt"); @@ -52,8 +53,8 @@ pub struct LocalizedAlert<'a> { /// builder.set_loc_args(&["narf", "derp"]); /// let payload = builder.build("device_id", Default::default()) /// .to_json_string().unwrap(); -/// # } /// ``` +#[derive(Default)] pub struct LocalizedNotificationBuilder<'a> { alert: LocalizedAlert<'a>, badge: Option, @@ -63,7 +64,10 @@ pub struct LocalizedNotificationBuilder<'a> { } impl<'a> LocalizedNotificationBuilder<'a> { - /// Creates a new builder with the minimum amount of content. + /// Creates a new builder. + /// + /// This is a convenience function for the very common pattern of providing the + /// title and body for a new LocalizedNotification. /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; @@ -77,46 +81,70 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// ); /// # } /// ``` - pub fn new( - title: &'a str, - body: &'a str - ) -> LocalizedNotificationBuilder<'a> - { + pub fn new(title: &'a str, body: &'a str) -> LocalizedNotificationBuilder<'a> { LocalizedNotificationBuilder { alert: LocalizedAlert { - title: title, - body: body, - title_loc_key: None, - title_loc_args: None, - action_loc_key: None, - loc_key: None, - loc_args: None, - launch_image: None, + title: Some(title), + body: Some(body), + ..Default::default() }, - badge: None, - sound: None, - category: None, - mutable_content: 0, + ..Default::default() } } - /// A number to show on a badge on top of the app icon. + /// Set the title + /// + /// ```rust + /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; + /// let payload = LocalizedNotificationBuilder::default(); + /// let mut builder = LocalizedNotificationBuilder::default(); + /// builder.set_title("a title"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// ``` + pub fn set_title(&mut self, title: &'a str) -> &mut Self { + self.alert.title = Some(title); + self + } + + /// Set the body /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); - /// builder.set_badge(4); + /// let mut builder = LocalizedNotificationBuilder::default(); + /// builder.set_body("a body"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"badge\":4,\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"body\":\"a body\"},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); /// # } /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self - { + pub fn set_body(&mut self, body: &'a str) -> &mut Self { + self.alert.body = Some(body); + self + } + + /// A number to show on a badge on top of the app icon. + /// + /// ```rust + /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; + /// let mut builder = LocalizedNotificationBuilder::default(); + /// builder.set_badge(4); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{},\"badge\":4,\"mutable-content\":0}}", + /// &payload.to_json_string().unwrap() + /// ); + /// ``` + pub fn set_badge(&mut self, badge: u32) -> &mut Self { self.badge = Some(badge); self } @@ -125,19 +153,16 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_sound("ping"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0,\"sound\":\"ping\"}}", + /// "{\"aps\":{\"alert\":{},\"mutable-content\":0,\"sound\":\"ping\"}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self - { + pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { self.sound = Some(sound); self } @@ -147,20 +172,17 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_category("cat1"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"category\":\"cat1\",\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{},\"category\":\"cat1\",\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self - { - self.category = Some(category.into()); + pub fn set_category(&mut self, category: &'a str) -> &mut Self { + self.category = Some(category); self } @@ -168,19 +190,16 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_title_loc_key("play"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\",\"title-loc-key\":\"play\"},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"title-loc-key\":\"play\"},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_title_loc_key(&mut self, key: &'a str) -> &mut Self - { + pub fn set_title_loc_key(&mut self, key: &'a str) -> &mut Self { self.alert.title_loc_key = Some(key); self } @@ -189,28 +208,20 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_title_loc_args(&["foo", "bar"]); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\",\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_title_loc_args( - &mut self, - args: &'a [S] - ) -> &mut Self + pub fn set_title_loc_args(&mut self, args: &'a [S]) -> &mut Self where - S: Into> + AsRef + S: Into> + AsRef, { - let converted = args - .iter() - .map(|a| a.as_ref().into()) - .collect(); + let converted = args.iter().map(|a| a.as_ref().into()).collect(); self.alert.title_loc_args = Some(converted); self @@ -220,19 +231,16 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_action_loc_key("stop"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"action-loc-key\":\"stop\",\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"action-loc-key\":\"stop\"},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_action_loc_key(&mut self, key: &'a str) -> &mut Self - { + pub fn set_action_loc_key(&mut self, key: &'a str) -> &mut Self { self.alert.action_loc_key = Some(key); self } @@ -241,19 +249,16 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_loc_key("lol"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"loc-key\":\"lol\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"loc-key\":\"lol\"},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_loc_key(&mut self, key: &'a str) -> &mut Self - { + pub fn set_loc_key(&mut self, key: &'a str) -> &mut Self { self.alert.loc_key = Some(key); self } @@ -262,28 +267,20 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_loc_args(&["omg", "foo"]); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"loc-args\":[\"omg\",\"foo\"],\"title\":\"a title\"},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"loc-args\":[\"omg\",\"foo\"]},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_loc_args( - &mut self, - args: &'a [S] - ) -> &mut Self + pub fn set_loc_args(&mut self, args: &'a [S]) -> &mut Self where - S: Into> + AsRef + S: Into> + AsRef, { - let converted = args - .iter() - .map(|a| a.as_ref().into()) - .collect(); + let converted = args.iter().map(|a| a.as_ref().into()).collect(); self.alert.loc_args = Some(converted); self @@ -293,19 +290,16 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_launch_image("cat.png"); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"launch-image\":\"cat.png\",\"title\":\"a title\"},\"mutable-content\":0}}", + /// "{\"aps\":{\"alert\":{\"launch-image\":\"cat.png\"},\"mutable-content\":0}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_launch_image(&mut self, image: &'a str) -> &mut Self - { + pub fn set_launch_image(&mut self, image: &'a str) -> &mut Self { self.alert.launch_image = Some(image); self } @@ -314,27 +308,23 @@ impl<'a> LocalizedNotificationBuilder<'a> { /// /// ```rust /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::new("a title", "a body"); + /// let mut builder = LocalizedNotificationBuilder::default(); /// builder.set_mutable_content(); /// let payload = builder.build("token", Default::default()); /// /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":1}}", + /// "{\"aps\":{\"alert\":{},\"mutable-content\":1}}", /// &payload.to_json_string().unwrap() /// ); - /// # } /// ``` - pub fn set_mutable_content(&mut self) -> &mut Self - { + pub fn set_mutable_content(&mut self) -> &mut Self { self.mutable_content = 1; self } } impl<'a> NotificationBuilder<'a> for LocalizedNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> - { + fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { Payload { aps: APS { alert: Some(APSAlert::Localized(self.alert)), @@ -343,9 +333,10 @@ impl<'a> NotificationBuilder<'a> for LocalizedNotificationBuilder<'a> { content_available: None, category: self.category, mutable_content: Some(self.mutable_content), + url_args: None, }, - device_token: device_token, - options: options, + device_token, + options, data: BTreeMap::new(), } } @@ -357,7 +348,8 @@ mod tests { #[test] fn test_localized_notification_with_minimal_required_values() { - let payload = LocalizedNotificationBuilder::new("the title", "the body") + let builder = LocalizedNotificationBuilder::new("the title", "the body"); + let payload = builder .build("device-token", Default::default()) .to_json_string() .unwrap(); @@ -370,15 +362,18 @@ mod tests { }, "mutable-content": 0 } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload); } #[test] fn test_localized_notification_with_full_data() { - let mut builder = LocalizedNotificationBuilder::new("the title", "the body"); + let mut builder = LocalizedNotificationBuilder::default(); + builder.set_title("the title"); + builder.set_body("the body"); builder.set_badge(420); builder.set_category("cat1"); builder.set_sound("prööt"); @@ -413,7 +408,8 @@ mod tests { "mutable-content": 1, "sound": "prööt" } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload); } @@ -440,8 +436,8 @@ mod tests { key_struct: SubData { nothing: "here" }, }; - let mut payload = LocalizedNotificationBuilder::new("the title", "the body") - .build("device-token", Default::default()); + let builder = LocalizedNotificationBuilder::new("the title", "the body"); + let mut payload = builder.build("device-token", Default::default()); payload.add_custom_data("custom", &test_data).unwrap(); @@ -461,7 +457,8 @@ mod tests { }, "mutable-content": 0 }, - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload.to_json_string().unwrap()); } diff --git a/src/request/notification/options.rs b/src/request/notification/options.rs index 88e485f6..1fb7c260 100644 --- a/src/request/notification/options.rs +++ b/src/request/notification/options.rs @@ -14,13 +14,13 @@ impl<'a> CollapseId<'a> { "The collapse-id is too big. Maximum 64 bytes.", ))) } else { - Ok(CollapseId { value: value }) + Ok(CollapseId { value }) } } } /// Headers to specify options to the notification. -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct NotificationOptions<'a> { /// A canonical UUID that identifies the notification. If there is an error /// sending the notification, APNs uses this value to identify the @@ -62,18 +62,6 @@ pub struct NotificationOptions<'a> { pub apns_collapse_id: Option>, } -impl<'a> Default for NotificationOptions<'a> { - fn default() -> NotificationOptions<'a> { - NotificationOptions { - apns_id: None, - apns_expiration: None, - apns_priority: None, - apns_topic: None, - apns_collapse_id: None, - } - } -} - /// The importance how fast to bring the notification for the user.. #[derive(Debug, Clone)] pub enum Priority { @@ -114,7 +102,7 @@ mod tests { #[test] fn test_collapse_id_over_64_chars() { let mut long_string = Vec::with_capacity(65); - long_string.extend_from_slice(&[65;65]); + long_string.extend_from_slice(&[65; 65]); let collapse_id = CollapseId::new(str::from_utf8(&long_string).unwrap()); assert!(collapse_id.is_err()); diff --git a/src/request/notification/plain.rs b/src/request/notification/plain.rs index e4b1d40e..675d535b 100644 --- a/src/request/notification/plain.rs +++ b/src/request/notification/plain.rs @@ -39,10 +39,9 @@ impl<'a> PlainNotificationBuilder<'a> { /// ); /// # } /// ``` - pub fn new(body: &'a str) -> PlainNotificationBuilder<'a> - { + pub fn new(body: &'a str) -> PlainNotificationBuilder<'a> { PlainNotificationBuilder { - body: body, + body, badge: None, sound: None, category: None, @@ -64,8 +63,7 @@ impl<'a> PlainNotificationBuilder<'a> { /// ); /// # } /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self - { + pub fn set_badge(&mut self, badge: u32) -> &mut Self { self.badge = Some(badge); self } @@ -85,9 +83,8 @@ impl<'a> PlainNotificationBuilder<'a> { /// ); /// # } /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self - { - self.sound = Some(sound.into()); + pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { + self.sound = Some(sound); self } @@ -107,16 +104,14 @@ impl<'a> PlainNotificationBuilder<'a> { /// ); /// # } /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self - { + pub fn set_category(&mut self, category: &'a str) -> &mut Self { self.category = Some(category); self } } impl<'a> NotificationBuilder<'a> for PlainNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> - { + fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { Payload { aps: APS { alert: Some(APSAlert::Plain(self.body)), @@ -125,9 +120,10 @@ impl<'a> NotificationBuilder<'a> for PlainNotificationBuilder<'a> { content_available: None, category: self.category, mutable_content: None, + url_args: None, }, - device_token: device_token, - options: options, + device_token, + options, data: BTreeMap::new(), } } @@ -148,7 +144,8 @@ mod tests { "aps": { "alert": "kulli", } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload); } @@ -172,7 +169,8 @@ mod tests { "category": "cat1", "sound": "prööt" } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload); } @@ -199,8 +197,7 @@ mod tests { key_struct: SubData { nothing: "here" }, }; - let mut payload = - PlainNotificationBuilder::new("kulli").build("device-token", Default::default()); + let mut payload = PlainNotificationBuilder::new("kulli").build("device-token", Default::default()); payload.add_custom_data("custom", &test_data).unwrap(); @@ -218,7 +215,8 @@ mod tests { "aps": { "alert": "kulli", } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload_json); } diff --git a/src/request/notification/silent.rs b/src/request/notification/silent.rs index 4678368b..ebd22ee7 100644 --- a/src/request/notification/silent.rs +++ b/src/request/notification/silent.rs @@ -30,6 +30,12 @@ pub struct SilentNotificationBuilder { content_available: u8, } +impl Default for SilentNotificationBuilder { + fn default() -> Self { + Self { content_available: 1 } + } +} + impl SilentNotificationBuilder { /// Creates a new builder. /// @@ -46,15 +52,12 @@ impl SilentNotificationBuilder { /// # } /// ``` pub fn new() -> SilentNotificationBuilder { - SilentNotificationBuilder { - content_available: 1, - } + Self::default() } } impl<'a> NotificationBuilder<'a> for SilentNotificationBuilder { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> - { + fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { Payload { aps: APS { alert: None, @@ -63,9 +66,10 @@ impl<'a> NotificationBuilder<'a> for SilentNotificationBuilder { content_available: Some(self.content_available), category: None, mutable_content: None, + url_args: None, }, - device_token: device_token, - options: options, + device_token, + options, data: BTreeMap::new(), } } @@ -87,7 +91,8 @@ mod tests { "aps": { "content-available": 1 } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload); } @@ -114,8 +119,7 @@ mod tests { key_struct: SubData { nothing: "here" }, }; - let mut payload = - SilentNotificationBuilder::new().build("device-token", Default::default()); + let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); payload.add_custom_data("custom", &test_data).unwrap(); @@ -131,7 +135,8 @@ mod tests { "nothing": "here" } } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload.to_json_string().unwrap()); } @@ -142,8 +147,7 @@ mod tests { test_data.insert("key_str", "foo"); test_data.insert("key_str2", "bar"); - let mut payload = - SilentNotificationBuilder::new().build("device-token", Default::default()); + let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); payload.add_custom_data("custom", &test_data).unwrap(); @@ -155,7 +159,8 @@ mod tests { "key_str": "foo", "key_str2": "bar" } - }).to_string(); + }) + .to_string(); assert_eq!(expected_payload, payload.to_json_string().unwrap()); } diff --git a/src/request/notification/web.rs b/src/request/notification/web.rs new file mode 100644 index 00000000..a0ff341c --- /dev/null +++ b/src/request/notification/web.rs @@ -0,0 +1,127 @@ +use crate::request::notification::{NotificationBuilder, NotificationOptions}; +use crate::request::payload::{APSAlert, Payload, APS}; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct WebPushAlert<'a> { + pub title: &'a str, + pub body: &'a str, + pub action: &'a str, +} + +/// A builder to create a simple APNs notification payload. +/// +/// # Example +/// +/// ```rust +/// # use a2::request::notification::{NotificationBuilder, WebNotificationBuilder, WebPushAlert}; +/// # fn main() { +/// let mut builder = WebNotificationBuilder::new(WebPushAlert {title: "Hello", body: "World", action: "View"}, &["arg1"]); +/// builder.set_sound("prööt"); +/// let payload = builder.build("device_id", Default::default()) +/// .to_json_string().unwrap(); +/// # } +/// ``` +pub struct WebNotificationBuilder<'a> { + alert: WebPushAlert<'a>, + sound: Option<&'a str>, + url_args: &'a [&'a str], +} + +impl<'a> WebNotificationBuilder<'a> { + /// Creates a new builder with the minimum amount of content. + /// + /// ```rust + /// # use a2::request::notification::{WebNotificationBuilder, NotificationBuilder, WebPushAlert}; + /// # fn main() { + /// let mut builder = WebNotificationBuilder::new(WebPushAlert {title: "Hello", body: "World", action: "View"}, &["arg1"]); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"action\":\"View\",\"body\":\"World\",\"title\":\"Hello\"},\"url-args\":[\"arg1\"]}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn new(alert: WebPushAlert<'a>, url_args: &'a [&'a str]) -> WebNotificationBuilder<'a> { + WebNotificationBuilder { + alert, + sound: None, + url_args, + } + } + + /// File name of the custom sound to play when receiving the notification. + /// + /// ```rust + /// # use a2::request::notification::{WebNotificationBuilder, NotificationBuilder, WebPushAlert}; + /// # fn main() { + /// let mut builder = WebNotificationBuilder::new(WebPushAlert {title: "Hello", body: "World", action: "View"}, &["arg1"]); + /// builder.set_sound("meow"); + /// let payload = builder.build("token", Default::default()); + /// + /// assert_eq!( + /// "{\"aps\":{\"alert\":{\"action\":\"View\",\"body\":\"World\",\"title\":\"Hello\"},\"sound\":\"meow\",\"url-args\":[\"arg1\"]}}", + /// &payload.to_json_string().unwrap() + /// ); + /// # } + /// ``` + pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { + self.sound = Some(sound); + self + } +} + +impl<'a> NotificationBuilder<'a> for WebNotificationBuilder<'a> { + fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { + Payload { + aps: APS { + alert: Some(APSAlert::WebPush(self.alert)), + badge: None, + sound: self.sound, + content_available: None, + category: None, + mutable_content: None, + url_args: Some(self.url_args), + }, + device_token, + options, + data: BTreeMap::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_webpush_notification() { + let payload = WebNotificationBuilder::new( + WebPushAlert { + action: "View", + title: "Hello", + body: "world", + }, + &["arg1"], + ) + .build("device-token", Default::default()) + .to_json_string() + .unwrap(); + + let expected_payload = json!({ + "aps": { + "alert": { + "body": "world", + "action": "View", + "title": "Hello" + }, + "url-args": ["arg1"] + } + }) + .to_string(); + + assert_eq!(expected_payload, payload); + } +} diff --git a/src/request/payload.rs b/src/request/payload.rs index 5f0ec13c..15f2878b 100644 --- a/src/request/payload.rs +++ b/src/request/payload.rs @@ -1,10 +1,10 @@ //! Payload with `aps` and custom data -use crate::request::notification::{LocalizedAlert, NotificationOptions}; use crate::error::Error; +use crate::request::notification::{LocalizedAlert, NotificationOptions, WebPushAlert}; +use erased_serde::Serialize; use serde_json::{self, Value}; use std::collections::BTreeMap; -use erased_serde::Serialize; /// The data and options for a push notification. #[derive(Debug, Clone)] @@ -69,13 +69,7 @@ impl<'a> Payload<'a> { /// ); /// # } /// ``` - pub fn add_custom_data( - &mut self, - root_key: &'a str, - data: &dyn Serialize, - ) -> Result<&mut Self, Error> - where - { + pub fn add_custom_data(&mut self, root_key: &'a str, data: &dyn Serialize) -> Result<&mut Self, Error> { self.data.insert(root_key, serde_json::to_value(data)?); Ok(self) @@ -83,6 +77,7 @@ impl<'a> Payload<'a> { /// Combine the APS payload and the custom data to a final payload JSON. /// Returns an error if serialization fails. + #[allow(clippy::wrong_self_convention)] pub fn to_json_string(mut self) -> Result { let aps_data = serde_json::to_value(&self.aps)?; @@ -93,8 +88,9 @@ impl<'a> Payload<'a> { } /// The pre-defined notification data. -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Default, Debug, Clone)] #[serde(rename_all = "kebab-case")] +#[allow(clippy::upper_case_acronyms)] pub struct APS<'a> { /// The notification content. Can be empty for silent notifications. #[serde(skip_serializing_if = "Option::is_none")] @@ -121,14 +117,19 @@ pub struct APS<'a> { /// displaying it to the user. #[serde(skip_serializing_if = "Option::is_none")] pub mutable_content: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub url_args: Option<&'a [&'a str]>, } /// Different notification content types. -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum APSAlert<'a> { /// Text-only notification. Plain(&'a str), /// A rich localized notification. Localized(LocalizedAlert<'a>), + /// Safari web push notification + WebPush(WebPushAlert<'a>), } diff --git a/src/response.rs b/src/response.rs index e609954e..73a6b7f9 100644 --- a/src/response.rs +++ b/src/response.rs @@ -27,7 +27,7 @@ pub struct Response { } /// The response body from APNs. Only available for errors. -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Deserialize, Debug, Eq, PartialEq)] pub struct ErrorBody { /// The error indicating the reason for the failure. pub reason: ErrorReason, @@ -42,7 +42,7 @@ pub struct ErrorBody { } /// A description what went wrong with the push notification. -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Deserialize, Debug, Eq, PartialEq)] pub enum ErrorReason { /// The collapse identifier exceeds the maximum allowed size. BadCollapseId, @@ -214,11 +214,7 @@ mod tests { (ErrorReason::BadMessageId, "BadMessageId", None), (ErrorReason::BadPriority, "BadPriority", None), (ErrorReason::BadTopic, "BadTopic", None), - ( - ErrorReason::DeviceTokenNotForTopic, - "DeviceTokenNotForTopic", - None, - ), + (ErrorReason::DeviceTokenNotForTopic, "DeviceTokenNotForTopic", None), (ErrorReason::DuplicateHeaders, "DuplicateHeaders", None), (ErrorReason::IdleTimeout, "IdleTimeout", None), (ErrorReason::MissingDeviceToken, "MissingDeviceToken", None), @@ -231,29 +227,13 @@ mod tests { "BadCertificateEnvironment", None, ), - ( - ErrorReason::ExpiredProviderToken, - "ExpiredProviderToken", - None, - ), + (ErrorReason::ExpiredProviderToken, "ExpiredProviderToken", None), (ErrorReason::Forbidden, "Forbidden", None), - ( - ErrorReason::InvalidProviderToken, - "InvalidProviderToken", - None, - ), - ( - ErrorReason::MissingProviderToken, - "MissingProviderToken", - None, - ), + (ErrorReason::InvalidProviderToken, "InvalidProviderToken", None), + (ErrorReason::MissingProviderToken, "MissingProviderToken", None), (ErrorReason::BadPath, "BadPath", None), (ErrorReason::MethodNotAllowed, "MethodNotAllowed", None), - ( - ErrorReason::Unregistered, - "Unregistered", - Some(1508249865488u64), - ), + (ErrorReason::Unregistered, "Unregistered", Some(1508249865488u64)), (ErrorReason::PayloadTooLarge, "PayloadTooLarge", None), ( ErrorReason::TooManyProviderTokenUpdates, @@ -261,11 +241,7 @@ mod tests { None, ), (ErrorReason::TooManyRequests, "TooManyRequests", None), - ( - ErrorReason::InternalServerError, - "InternalServerError", - None, - ), + (ErrorReason::InternalServerError, "InternalServerError", None), (ErrorReason::ServiceUnavailable, "ServiceUnavailable", None), (ErrorReason::Shutdown, "Shutdown", None), ]; diff --git a/src/signer.rs b/src/signer.rs index 6ea374b5..641e031f 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -1,13 +1,15 @@ -use std::io::Read; -use serde_json; -use base64::encode; use crate::error::Error; -use std::{sync::RwLock, time::{Duration, SystemTime, UNIX_EPOCH}}; +use base64::encode; +use std::io::Read; +use std::{ + sync::RwLock, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; use openssl::{ ec::EcKey, - pkey::{PKey, Private}, hash::MessageDigest, + pkey::{PKey, Private}, sign::Signer as SslSigner, }; @@ -47,12 +49,7 @@ struct JwtPayload<'a> { impl Signer { /// Creates a signer with a pkcs8 private key, APNs key id and team id. /// Can fail if the key is not valid or there is a problem with system OpenSSL. - pub fn new( - mut pk_pem: R, - key_id: S, - team_id: T, - signature_ttl: Duration, - ) -> Result + pub fn new(mut pk_pem: R, key_id: S, team_id: T, signature_ttl: Duration) -> Result where S: Into, T: Into, @@ -71,14 +68,14 @@ impl Signer { let signature = RwLock::new(Signature { key: Self::create_signature(&secret, &key_id, &team_id, issued_at)?, - issued_at: issued_at, + issued_at, }); let signer = Signer { - signature: signature, - key_id: key_id, - team_id: team_id, - secret: secret, + signature, + key_id, + team_id, + secret, expire_after_s: signature_ttl, }; @@ -107,12 +104,7 @@ impl Signer { Ok(f(&signature.key)) } - fn create_signature( - secret: &PKey, - key_id: &str, - team_id: &str, - issued_at: i64, - ) -> Result { + fn create_signature(secret: &PKey, key_id: &str, team_id: &str, issued_at: i64) -> Result { let headers = JwtHeader { alg: JwtAlg::ES256, kid: key_id, @@ -150,7 +142,7 @@ impl Signer { *signature = Signature { key: Self::create_signature(&self.secret, &self.key_id, &self.team_id, issued_at)?, - issued_at: issued_at, + issued_at, }; Ok(()) @@ -174,7 +166,7 @@ fn get_time() -> i64 { mod tests { use super::*; - const PRIVATE_KEY: &'static str = indoc!( + const PRIVATE_KEY: &str = indoc!( "-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8g/n6j9roKvnUkwu lCEIvbDqlUhA5FOzcakkG90E8L+hRANCAATKS2ZExEybUvchRDuKBftotMwVEus3 @@ -184,7 +176,13 @@ mod tests { #[test] fn test_signature_caching() { - let signer = Signer::new(PRIVATE_KEY.as_bytes(), "89AFRD1X22", "ASDFQWERTY", Duration::from_secs(100)).unwrap(); + let signer = Signer::new( + PRIVATE_KEY.as_bytes(), + "89AFRD1X22", + "ASDFQWERTY", + Duration::from_secs(100), + ) + .unwrap(); let mut sig1 = String::new(); signer.with_signature(|sig| sig1.push_str(sig)).unwrap(); @@ -197,7 +195,13 @@ mod tests { #[test] fn test_signature_without_caching() { - let signer = Signer::new(PRIVATE_KEY.as_bytes(), "89AFRD1X22", "ASDFQWERTY", Duration::from_secs(0)).unwrap(); + let signer = Signer::new( + PRIVATE_KEY.as_bytes(), + "89AFRD1X22", + "ASDFQWERTY", + Duration::from_secs(0), + ) + .unwrap(); let mut sig1 = String::new(); signer.with_signature(|sig| sig1.push_str(sig)).unwrap(); From 7d75c60e5d7393c3310f0081ae46fd3e8b34dff8 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Thu, 22 Dec 2022 15:13:03 +0000 Subject: [PATCH 2/6] chore: More merges from upstream --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15e59db5..97c6e191 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,10 @@ jobs: cmd: clippy args: -- -D clippy::all cache: {} -<<<<<<< HEAD -======= - name: "Clippy (ring feature)" cmd: clippy args: --no-default-features --features ring -- -D clippy::all cache: {} ->>>>>>> upstream/master - name: "Formatting" cmd: fmt args: -- --check @@ -51,13 +48,10 @@ jobs: cmd: test args: --all-features cache: { sharedKey: "tests" } -<<<<<<< HEAD -======= - name: "Unit Tests (ring feature)" cmd: test args: --no-default-features --features ring cache: { sharedKey: "tests-ring" } ->>>>>>> upstream/master include: - os: ubuntu-latest sccache-path: /home/runner/.cache/sccache From 497ad03723a281aa28aa70e84ed1661fb289127c Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Thu, 22 Dec 2022 15:13:41 +0000 Subject: [PATCH 3/6] chore: More merges from upstream --- examples/certificate_client.rs | 6 ------ examples/token_client.rs | 4 ---- 2 files changed, 10 deletions(-) diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index 86b838e5..709ba03f 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -1,12 +1,6 @@ -<<<<<<< HEAD -use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; -use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; -use std::fs::File; -======= use a2::{Client, DefaultNotificationBuilder, NotificationBuilder, NotificationOptions}; use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; use tokio; ->>>>>>> upstream/master // An example client connectiong to APNs with a certificate and key #[tokio::main] diff --git a/examples/token_client.rs b/examples/token_client.rs index 0d64c565..32c3c0af 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -2,11 +2,7 @@ use argparse::{ArgumentParser, Store, StoreOption, StoreTrue}; use std::fs::File; use tokio; -<<<<<<< HEAD -use a2::{Client, Endpoint, NotificationBuilder, NotificationOptions, PlainNotificationBuilder}; -======= use a2::{Client, DefaultNotificationBuilder, Endpoint, NotificationBuilder, NotificationOptions}; ->>>>>>> upstream/master // An example client connectiong to APNs with a JWT token #[tokio::main] From 36f18bd05649721c567854b765bfb2d53144b000 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Thu, 22 Dec 2022 15:14:52 +0000 Subject: [PATCH 4/6] chore: Remove old files --- src/request/notification/localized.rs | 465 -------------------------- src/request/notification/plain.rs | 223 ------------ src/request/notification/silent.rs | 167 --------- 3 files changed, 855 deletions(-) delete mode 100644 src/request/notification/localized.rs delete mode 100644 src/request/notification/plain.rs delete mode 100644 src/request/notification/silent.rs diff --git a/src/request/notification/localized.rs b/src/request/notification/localized.rs deleted file mode 100644 index e6846a05..00000000 --- a/src/request/notification/localized.rs +++ /dev/null @@ -1,465 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{APSAlert, Payload, APS}; - -use std::{borrow::Cow, collections::BTreeMap}; - -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct LocalizedAlert<'a> { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub body: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub title_loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub title_loc_args: Option>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub action_loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub loc_key: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub loc_args: Option>>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub launch_image: Option<&'a str>, -} - -/// A builder to create a localized APNs payload. -/// -/// # Example -/// -/// ```rust -/// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; -/// let mut builder = LocalizedNotificationBuilder::default(); -/// builder.set_title("Hi there"); -/// builder.set_body("What's up?"); -/// builder.set_badge(420); -/// builder.set_category("cat1"); -/// builder.set_sound("prööt"); -/// builder.set_mutable_content(); -/// builder.set_action_loc_key("PLAY"); -/// builder.set_launch_image("foo.jpg"); -/// builder.set_loc_args(&["argh", "narf"]); -/// builder.set_title_loc_key("STOP"); -/// builder.set_title_loc_args(&["herp", "derp"]); -/// builder.set_loc_key("PAUSE"); -/// builder.set_loc_args(&["narf", "derp"]); -/// let payload = builder.build("device_id", Default::default()) -/// .to_json_string().unwrap(); -/// ``` -#[derive(Default)] -pub struct LocalizedNotificationBuilder<'a> { - alert: LocalizedAlert<'a>, - badge: Option, - sound: Option<&'a str>, - category: Option<&'a str>, - mutable_content: u8, -} - -impl<'a> LocalizedNotificationBuilder<'a> { - /// Creates a new builder. - /// - /// This is a convenience function for the very common pattern of providing the - /// title and body for a new LocalizedNotification. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = LocalizedNotificationBuilder::new("a title", "a body") - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\",\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new(title: &'a str, body: &'a str) -> LocalizedNotificationBuilder<'a> { - LocalizedNotificationBuilder { - alert: LocalizedAlert { - title: Some(title), - body: Some(body), - ..Default::default() - }, - ..Default::default() - } - } - - /// Set the title - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let payload = LocalizedNotificationBuilder::default(); - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_title("a title"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"title\":\"a title\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_title(&mut self, title: &'a str) -> &mut Self { - self.alert.title = Some(title); - self - } - - /// Set the body - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_body("a body"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"body\":\"a body\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_body(&mut self, body: &'a str) -> &mut Self { - self.alert.body = Some(body); - self - } - - /// A number to show on a badge on top of the app icon. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_badge(4); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{},\"badge\":4,\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self { - self.badge = Some(badge); - self - } - - /// File name of the custom sound to play when receiving the notification. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_sound("ping"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{},\"mutable-content\":0,\"sound\":\"ping\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { - self.sound = Some(sound); - self - } - - /// When a notification includes the category key, the system displays the - /// actions for that category as buttons in the banner or alert interface. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_category("cat1"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{},\"category\":\"cat1\",\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self { - self.category = Some(category); - self - } - - /// The localization key for the notification title. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_title_loc_key("play"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"title-loc-key\":\"play\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_title_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.title_loc_key = Some(key); - self - } - - /// Arguments for the title localization. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_title_loc_args(&["foo", "bar"]); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"title-loc-args\":[\"foo\",\"bar\"]},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_title_loc_args(&mut self, args: &'a [S]) -> &mut Self - where - S: Into> + AsRef, - { - let converted = args.iter().map(|a| a.as_ref().into()).collect(); - - self.alert.title_loc_args = Some(converted); - self - } - - /// The localization key for the action. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_action_loc_key("stop"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"action-loc-key\":\"stop\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_action_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.action_loc_key = Some(key); - self - } - - /// The localization key for the push message body. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_loc_key("lol"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"loc-key\":\"lol\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_loc_key(&mut self, key: &'a str) -> &mut Self { - self.alert.loc_key = Some(key); - self - } - - /// Arguments for the content localization. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_loc_args(&["omg", "foo"]); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"loc-args\":[\"omg\",\"foo\"]},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_loc_args(&mut self, args: &'a [S]) -> &mut Self - where - S: Into> + AsRef, - { - let converted = args.iter().map(|a| a.as_ref().into()).collect(); - - self.alert.loc_args = Some(converted); - self - } - - /// Image to display in the rich notification. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_launch_image("cat.png"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{\"launch-image\":\"cat.png\"},\"mutable-content\":0}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_launch_image(&mut self, image: &'a str) -> &mut Self { - self.alert.launch_image = Some(image); - self - } - - /// Allow client to modify push content before displaying. - /// - /// ```rust - /// # use a2::request::notification::{LocalizedNotificationBuilder, NotificationBuilder}; - /// let mut builder = LocalizedNotificationBuilder::default(); - /// builder.set_mutable_content(); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":{},\"mutable-content\":1}}", - /// &payload.to_json_string().unwrap() - /// ); - /// ``` - pub fn set_mutable_content(&mut self) -> &mut Self { - self.mutable_content = 1; - self - } -} - -impl<'a> NotificationBuilder<'a> for LocalizedNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: Some(APSAlert::Localized(self.alert)), - badge: self.badge, - sound: self.sound, - content_available: None, - category: self.category, - mutable_content: Some(self.mutable_content), - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_localized_notification_with_minimal_required_values() { - let builder = LocalizedNotificationBuilder::new("the title", "the body"); - let payload = builder - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": { - "title": "the title", - "body": "the body", - }, - "mutable-content": 0 - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_localized_notification_with_full_data() { - let mut builder = LocalizedNotificationBuilder::default(); - - builder.set_title("the title"); - builder.set_body("the body"); - builder.set_badge(420); - builder.set_category("cat1"); - builder.set_sound("prööt"); - builder.set_mutable_content(); - builder.set_action_loc_key("PLAY"); - builder.set_launch_image("foo.jpg"); - builder.set_loc_args(&["argh", "narf"]); - builder.set_title_loc_key("STOP"); - builder.set_title_loc_args(&["herp", "derp"]); - builder.set_loc_key("PAUSE"); - builder.set_loc_args(&["narf", "derp"]); - - let payload = builder - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": { - "action-loc-key": "PLAY", - "body": "the body", - "launch-image": "foo.jpg", - "loc-args": ["narf", "derp"], - "loc-key": "PAUSE", - "title": "the title", - "title-loc-args": ["herp", "derp"], - "title-loc-key": "STOP" - }, - "badge": 420, - "category": "cat1", - "mutable-content": 1, - "sound": "prööt" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let builder = LocalizedNotificationBuilder::new("the title", "the body"); - let mut payload = builder.build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - }, - "aps": { - "alert": { - "title": "the title", - "body": "the body", - }, - "mutable-content": 0 - }, - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } -} diff --git a/src/request/notification/plain.rs b/src/request/notification/plain.rs deleted file mode 100644 index 675d535b..00000000 --- a/src/request/notification/plain.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{APSAlert, Payload, APS}; -use std::collections::BTreeMap; - -/// A builder to create a simple APNs notification payload. -/// -/// # Example -/// -/// ```rust -/// # use a2::request::notification::{NotificationBuilder, PlainNotificationBuilder}; -/// # fn main() { -/// let mut builder = PlainNotificationBuilder::new("Hi there"); -/// builder.set_badge(420); -/// builder.set_category("cat1"); -/// builder.set_sound("prööt"); -/// let payload = builder.build("device_id", Default::default()) -/// .to_json_string().unwrap(); -/// # } -/// ``` -pub struct PlainNotificationBuilder<'a> { - body: &'a str, - badge: Option, - sound: Option<&'a str>, - category: Option<&'a str>, -} - -impl<'a> PlainNotificationBuilder<'a> { - /// Creates a new builder with the minimum amount of content. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = PlainNotificationBuilder::new("a body") - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new(body: &'a str) -> PlainNotificationBuilder<'a> { - PlainNotificationBuilder { - body, - badge: None, - sound: None, - category: None, - } - } - - /// A number to show on a badge on top of the app icon. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_badge(4); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"badge\":4}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_badge(&mut self, badge: u32) -> &mut Self { - self.badge = Some(badge); - self - } - - /// File name of the custom sound to play when receiving the notification. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_sound("meow"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"sound\":\"meow\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_sound(&mut self, sound: &'a str) -> &mut Self { - self.sound = Some(sound); - self - } - - /// When a notification includes the category key, the system displays the - /// actions for that category as buttons in the banner or alert interface. - /// - /// ```rust - /// # use a2::request::notification::{PlainNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let mut builder = PlainNotificationBuilder::new("a body"); - /// builder.set_category("cat1"); - /// let payload = builder.build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"alert\":\"a body\",\"category\":\"cat1\"}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn set_category(&mut self, category: &'a str) -> &mut Self { - self.category = Some(category); - self - } -} - -impl<'a> NotificationBuilder<'a> for PlainNotificationBuilder<'a> { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: Some(APSAlert::Plain(self.body)), - badge: self.badge, - sound: self.sound, - content_available: None, - category: self.category, - mutable_content: None, - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_plain_notification_with_text_only() { - let payload = PlainNotificationBuilder::new("kulli") - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": "kulli", - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_full_data() { - let mut builder = PlainNotificationBuilder::new("Hi there"); - builder.set_badge(420); - builder.set_category("cat1"); - builder.set_sound("prööt"); - - let payload = builder - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "alert": "Hi there", - "badge": 420, - "category": "cat1", - "sound": "prööt" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_plain_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let mut payload = PlainNotificationBuilder::new("kulli").build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let payload_json = payload.to_json_string().unwrap(); - - let expected_payload = json!({ - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - }, - "aps": { - "alert": "kulli", - } - }) - .to_string(); - - assert_eq!(expected_payload, payload_json); - } -} diff --git a/src/request/notification/silent.rs b/src/request/notification/silent.rs deleted file mode 100644 index ebd22ee7..00000000 --- a/src/request/notification/silent.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::request::notification::{NotificationBuilder, NotificationOptions}; -use crate::request::payload::{Payload, APS}; -use std::collections::BTreeMap; - -/// A builder to create an APNs silent notification payload which can be used to -/// send custom data to the user's phone if the user hasn't been running the app -/// for a while. The custom data needs to be implementing `Serialize` from Serde. -/// -/// # Example -/// -/// ```rust -/// # use std::collections::HashMap; -/// # use a2::request::notification::{NotificationBuilder, SilentNotificationBuilder}; -/// # fn main() { -/// let mut test_data = HashMap::new(); -/// test_data.insert("a", "value"); -/// -/// let mut payload = SilentNotificationBuilder::new() -/// .build("device_id", Default::default()); -/// -/// payload.add_custom_data("custom", &test_data); -/// -/// assert_eq!( -/// "{\"aps\":{\"content-available\":1},\"custom\":{\"a\":\"value\"}}", -/// &payload.to_json_string().unwrap() -/// ); -/// # } -/// ``` -pub struct SilentNotificationBuilder { - content_available: u8, -} - -impl Default for SilentNotificationBuilder { - fn default() -> Self { - Self { content_available: 1 } - } -} - -impl SilentNotificationBuilder { - /// Creates a new builder. - /// - /// ```rust - /// # use a2::request::notification::{SilentNotificationBuilder, NotificationBuilder}; - /// # fn main() { - /// let payload = SilentNotificationBuilder::new() - /// .build("token", Default::default()); - /// - /// assert_eq!( - /// "{\"aps\":{\"content-available\":1}}", - /// &payload.to_json_string().unwrap() - /// ); - /// # } - /// ``` - pub fn new() -> SilentNotificationBuilder { - Self::default() - } -} - -impl<'a> NotificationBuilder<'a> for SilentNotificationBuilder { - fn build(self, device_token: &'a str, options: NotificationOptions<'a>) -> Payload<'a> { - Payload { - aps: APS { - alert: None, - badge: None, - sound: None, - content_available: Some(self.content_available), - category: None, - mutable_content: None, - url_args: None, - }, - device_token, - options, - data: BTreeMap::new(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - #[test] - fn test_silent_notification_with_no_content() { - let payload = SilentNotificationBuilder::new() - .build("device-token", Default::default()) - .to_json_string() - .unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - } - }) - .to_string(); - - assert_eq!(expected_payload, payload); - } - - #[test] - fn test_silent_notification_with_custom_data() { - #[derive(Serialize, Debug)] - struct SubData { - nothing: &'static str, - } - - #[derive(Serialize, Debug)] - struct TestData { - key_str: &'static str, - key_num: u32, - key_bool: bool, - key_struct: SubData, - } - - let test_data = TestData { - key_str: "foo", - key_num: 42, - key_bool: false, - key_struct: SubData { nothing: "here" }, - }; - - let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - }, - "custom": { - "key_str": "foo", - "key_num": 42, - "key_bool": false, - "key_struct": { - "nothing": "here" - } - } - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } - - #[test] - fn test_silent_notification_with_custom_hashmap() { - let mut test_data = BTreeMap::new(); - test_data.insert("key_str", "foo"); - test_data.insert("key_str2", "bar"); - - let mut payload = SilentNotificationBuilder::new().build("device-token", Default::default()); - - payload.add_custom_data("custom", &test_data).unwrap(); - - let expected_payload = json!({ - "aps": { - "content-available": 1 - }, - "custom": { - "key_str": "foo", - "key_str2": "bar" - } - }) - .to_string(); - - assert_eq!(expected_payload, payload.to_json_string().unwrap()); - } -} From 7de2a71a04db9c36129e8acb1e34230f29415003 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Thu, 22 Dec 2022 15:17:18 +0000 Subject: [PATCH 5/6] fix: Missing derive --- src/request/notification/default.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/request/notification/default.rs b/src/request/notification/default.rs index ab3834eb..0b07f1fe 100644 --- a/src/request/notification/default.rs +++ b/src/request/notification/default.rs @@ -3,7 +3,7 @@ use crate::request::payload::{APSAlert, Payload, APS}; use std::{borrow::Cow, collections::BTreeMap}; -#[derive(Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct DefaultAlert<'a> { #[serde(skip_serializing_if = "Option::is_none")] From 9f6dc1c59210a40365eefb97d09e870baa5b70cb Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Thu, 22 Dec 2022 15:40:28 +0000 Subject: [PATCH 6/6] fix: misc issues --- src/error.rs | 1 - src/request/notification.rs | 1 - src/request/payload.rs | 3 +-- src/signer.rs | 6 +++--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2040893b..a4ed649d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ ///! Error and result module - use crate::{response::Response, signer::SignerError}; use std::io; use thiserror::Error; diff --git a/src/request/notification.rs b/src/request/notification.rs index 29012f1d..354f556e 100644 --- a/src/request/notification.rs +++ b/src/request/notification.rs @@ -1,5 +1,4 @@ ///! The `aps` notification content builders - mod default; mod options; mod web; diff --git a/src/request/payload.rs b/src/request/payload.rs index b96bc8f9..71c7c3f7 100644 --- a/src/request/payload.rs +++ b/src/request/payload.rs @@ -1,5 +1,4 @@ ///! Payload with `aps` and custom data - use crate::error::Error; use crate::request::notification::{DefaultAlert, NotificationOptions, WebPushAlert}; use erased_serde::Serialize; @@ -126,7 +125,7 @@ pub struct APS<'a> { } /// Different notification content types. -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum APSAlert<'a> { /// A notification that supports all of the iOS features diff --git a/src/signer.rs b/src/signer.rs index f9664961..df93fb5f 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -1,11 +1,11 @@ use crate::error::Error; use base64::encode; use std::io::Read; +use std::sync::Arc; use std::{ sync::RwLock, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use std::sync::Arc; #[cfg(feature = "openssl")] use openssl::{ @@ -118,10 +118,10 @@ impl Signer { }); let signer = Signer { - signature, + signature: Arc::new(signature), key_id, team_id, - secret, + secret: Arc::new(secret), expire_after_s: signature_ttl, };