public class Encryption {

    /**
     * Iterations
     */
    private final static int ITERATIONS = 30000;

    /**
     * Derived key length
     */
    private final static int DERIVED_KEY_LENGTH_BITS = 256;

    /**
     * GCM init vector length in bits
     */
    private final static int GCM_INIT_VECTOR_LENGTH_BITS = 96;

    /**
     * GCM tag length in bits
     */
    private final static int GCM_TAG_LENGTH_BITS = 128;

    /**
     * Current version of the encryption
     */
    private final static String VERSION = "1";

    /**
     * Charset of the messages
     */
    private final static String CHARSET = "UTF-8";

    /**
     * The {@link Pushbullet} instance
     */
    private Pushbullet pushbullet;

    /**
     * The {@link SecretKey} for the algorithm
     */
    private SecretKey secretKey;

    /**
     * The {@link SecureRandom} generating the bytes for the init vector
     */
    private SecureRandom secureRandom;

    /**
     * Creates a new {@link Encryption} instance
     * @param pushbullet The {@link Pushbullet} instance
     * @param password   The password to use for the encryption
     */
    @SneakyThrows
    public Encryption(@NonNull Pushbullet pushbullet, @NonNull String password) {
        this.pushbullet = pushbullet;
        this.secretKey = generateKey(password);
        this.secureRandom = SecureRandom.getInstanceStrong();
    }

    /**
     * Generates the key from the password, algorithm: PBKDF2 with Hmac SHA256
     * @param password The password given for the key
     * @return The {@link SecretKey} made of the password
     */
    @SneakyThrows
    private SecretKey generateKey(String password) {
        String salt = pushbullet.getCurrentUser().getIdentity();
        SecretKeyFactory pbkdf2WithHmacSHA256 = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        return new SecretKeySpec(pbkdf2WithHmacSHA256.generateSecret(new PBEKeySpec(password.toCharArray(), salt.getBytes(CHARSET), ITERATIONS, DERIVED_KEY_LENGTH_BITS)).getEncoded(), "AES");
    }

    /**
     * Encrypts a message
     * @param message The message to encrypt
     * @return The encrypted message in the format shown on the pushbullet api docs
     */
    @SneakyThrows
    public String encrypt(String message) {
        byte[] initVector = new byte[GCM_INIT_VECTOR_LENGTH_BITS / Byte.SIZE];
        secureRandom.nextBytes(initVector);
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH_BITS, initVector);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
        byte[] encrypted = cipher.doFinal(message.getBytes(CHARSET));
        byte[] tag = Arrays.copyOfRange(encrypted, encrypted.length - (GCM_TAG_LENGTH_BITS / Byte.SIZE), encrypted.length);
        encrypted = Arrays.copyOfRange(encrypted, 0, encrypted.length - (GCM_TAG_LENGTH_BITS / Byte.SIZE));
        return Base64.getEncoder().encodeToString(concatByteArrays(VERSION.getBytes(CHARSET), tag, initVector, encrypted));
    }

    /**
     * Decrypts an encoded encrypted message
     * @param encodedMessage The base64 encoded, encrypted message
     * @return The plaintext of the message
     */
    @SneakyThrows
    public String decrypt(String encodedMessage) {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        byte[] messageBytes = Base64.getDecoder().decode(encodedMessage.getBytes(CHARSET));
        byte[] version = Arrays.copyOfRange(messageBytes, 0, 1);
        if(!new String(version, CHARSET).equals(VERSION)) {
            throw new IllegalArgumentException("Version is wrong!");
        }
        int tagLastIndex = 1 + GCM_TAG_LENGTH_BITS / Byte.SIZE;
        byte[] tag = Arrays.copyOfRange(messageBytes, 1, tagLastIndex);
        int initVectorLastIndex = tagLastIndex + GCM_INIT_VECTOR_LENGTH_BITS / Byte.SIZE;
        byte[] initVector = Arrays.copyOfRange(messageBytes, tagLastIndex, initVectorLastIndex);
        byte[] realMessage = concatByteArrays(Arrays.copyOfRange(messageBytes, initVectorLastIndex, messageBytes.length), tag);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, initVector));
        byte[] decrypted = cipher.doFinal(realMessage);
        return new String(decrypted, CHARSET);
    }

    private byte[] concatByteArrays(byte[]... arrays) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Stream.of(arrays).forEach(array -> {
            try {
                byteArrayOutputStream.write(array);
            }
            catch (IOException ex) {

            }
        });
        return byteArrayOutputStream.toByteArray();
    }
} @RequiredArgsConstructor
public class EphemeralEncryptionHandler implements TypeAdapterFactory {

    private final Pushbullet pushbullet;

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if(Ephemeral.class.equals(type.getRawType())) {
            TypeAdapter<T> originalTypeAdapter = gson.getDelegateAdapter(this, type);
            return new EncryptionEphemeralTypeAdapter<>((Class<T>) type.getRawType(), originalTypeAdapter, gson);
        }
        return null;
    }

    @RequiredArgsConstructor
    private class EncryptionEphemeralTypeAdapter<T> extends TypeAdapter<T> {

        private final Class<T> rawType;

        private final TypeAdapter<T> originalAdapter;

        private final Gson gson;

        @Override
        public void write(JsonWriter out, T value) throws IOException {
            Encryption encryption = EphemeralEncryptionHandler.this.pushbullet.getEncryption();
            JsonElement tree = originalAdapter.toJsonTree(value);
            if (encryption != null) {
                String encrypted = encryption.encrypt(tree.toString());
                JsonObject object = new JsonObject();
                object.addProperty("ciphertext", encrypted);
                object.addProperty("encrypted", true);
                tree = object;
            }
            gson.toJson(tree, out);
        }

        @Override
        public T read(JsonReader in) throws IOException {
            Encryption encryption = EphemeralEncryptionHandler.this.pushbullet.getEncryption();
            if(encryption != null) {
                in.beginObject();
                String cipherText = null;
                boolean encrypted = false;
                while(in.hasNext()) {
                    switch (in.nextName()) {
                        case "ciphertext":
                            cipherText = in.nextString().replaceAll("\\n", "");
                            break;
                        case "encrypted":
                            encrypted = in.nextBoolean();
                            break;
                        default:
                            in.skipValue();
                    }
                }
                in.endObject();
                if(encrypted) {
                    String decrypted = encryption.decrypt(cipherText);
                    return originalAdapter.fromJson(decrypted);
                }
            }
            return originalAdapter.read(in);
        }
    }
} @Data
public class PushbulletApiError {

    /**
     * The type of this error
     */
    private final String type;

    /**
     * The message of this error
     */
    private final String message;

    /**
     * :3
     */
    private final String cat;

    public String toString() {
        return message;
    }

} public class PushbulletApiException extends RuntimeException {

    /**
     * The api error that occured after during an api request
     */
    @Getter
    private final PushbulletApiError error;

    public PushbulletApiException(PushbulletApiError error) {
        super(error.getMessage());
        this.error = error;
    }
} @Data
public class PushbulletContainerPostProcessor implements PostProcessor<PushbulletContainer> {

    private final Pushbullet pushbullet;

    @Override
    public void postDeserialize(PushbulletContainer result, JsonElement src, Gson gson) {
        if(result == null) return;
        result.setPushbullet(pushbullet);
    }

    @Override
    public void postSerialize(JsonElement result, PushbulletContainer src, Gson gson) {

    }
} public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
    private final Class<T> baseType;
    private TypeAdapter<T> baseDelegateAdapter;
    private final String typeFieldName;
    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();

    private RuntimeTypeAdapterFactory(Class<T> baseType, String typeFieldName) {
        if (typeFieldName == null || baseType == null) {
            throw new NullPointerException();
        }
        this.baseType = baseType;
        this.typeFieldName = typeFieldName;
    }

    /**
     * Creates a new runtime type adapter using for {@code baseType} using {@code
     * typeFieldName} as the type field name. Type field names are case sensitive.
     */
    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
        return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
    }

    /**
     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
     * the type field name.
     */
    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
        return new RuntimeTypeAdapterFactory<T>(baseType, "type");
    }

    /**
     * Registers {@code type} identified by {@code label}. Labels are case
     * sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or {@code label}
     *                                  have already been registered on this type adapter.
     */
    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
        if (type == null || label == null) {
            throw new NullPointerException();
        }
        if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
            throw new IllegalArgumentException("types and labels must be unique");
        }
        labelToSubtype.put(label, type);
        subtypeToLabel.put(type, label);
        return this;
    }

    /**
     * Registers {@code type} identified by its {@link Class#getSimpleName simple
     * name}. Labels are case sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or its simple name
     *                                  have already been registered on this type adapter.
     */
    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
        return registerSubtype(type, type.getSimpleName());
    }

    public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
        if (type.getRawType() != baseType) {
            return null;
        }

        if(baseDelegateAdapter == null) {
            baseDelegateAdapter = gson.getDelegateAdapter(RuntimeTypeAdapterFactory.this, TypeToken.get(baseType));
        }

        final Map<String, TypeAdapter<?>> labelToDelegate
                = new LinkedHashMap<String, TypeAdapter<?>>();
        final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
                = new LinkedHashMap<Class<?>, TypeAdapter<?>>();
        for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
            TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
            labelToDelegate.put(entry.getKey(), delegate);
            subtypeToDelegate.put(entry.getValue(), delegate);
        }

        return new TypeAdapter<R>() {
            @SuppressWarnings("unchecked")
            @Override
            public R read(JsonReader in) throws IOException {
                JsonElement jsonElement = Streams.parse(in);
                JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); //remove(typeFieldName) changed to get(typeFieldName)
                if (labelJsonElement == null) {
                    return (R) baseDelegateAdapter.fromJsonTree(jsonElement);
                }
                String label = labelJsonElement.getAsString();
                @SuppressWarnings("unchecked") // registration requires that subtype extends T
                        TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
                if (delegate == null) {
                    throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
                            + label + "; did you forget to register a subtype?");
                }
                return delegate.fromJsonTree(jsonElement);
            }

            @Override
            public void write(JsonWriter out, R value) throws IOException {
                Class<?> srcType = value.getClass();
                String label = subtypeToLabel.get(srcType);
                @SuppressWarnings("unchecked") // registration requires that subtype extends T
                        TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
                if (delegate == null) {
                    throw new JsonParseException("cannot serialize " + srcType.getName()
                            + "; did you forget to register a subtype?");
                }
                JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
                //if (jsonObject.has(typeFieldName)) {
                //    throw new JsonParseException("cannot serialize " + srcType.getName()
                //            + " because it already defines a field named " + typeFieldName); <-- Commented
                //}
                JsonObject clone = new JsonObject();
                clone.add(typeFieldName, new JsonPrimitive(label));
                for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
                    clone.add(e.getKey(), e.getValue());
                }
                Streams.write(clone, out);
            }
        }.nullSafe();
    }
} public abstract class EntityEnclosingRequest<TResult, TMessage extends HttpEntityEnclosingRequestBase> extends Request<TResult, TMessage> {

    public EntityEnclosingRequest(String relativePath) {
        super(relativePath);
    }

    public void applyBody(Gson gson, TMessage message) {}
} @Accessors(chain = true, fluent = true)
@Setter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ListRequestBuilder<T> {

    /**
     * The default API array length limit for the list requests
     */
    private final static int DEFAULT_LIMIT = 500;

    /**
     * The {@link Pushbullet} instance
     */
    private final Pushbullet pushbullet;

    /**
     * The request type
     */
    private final ListRequestType<T> request;

    /**
     * The cursor of the request
     */
    private String cursor;

    /**
     * Whether only active items should be included in the list
     */
    private boolean onlyShowActiveItems = true;

    /**
     * Whether only items that were modified after the specified timestamp should be included in the list
     */
    private double modifiedAfter;

    /**
     * The maximum length of the requested list
     */
    private int limit = DEFAULT_LIMIT;

    /**
     * Whether the list should automatically make new requests when the cursor is not null and include the items in the list
     */
    private boolean completeList = false;

    /**
     * The returned cursor if there is any (has to be used after {@link #execute()})
     */
    private String lastReturnedCursor;

    /**
     * Returns a new {@link ListRequestBuilder} instance
     * @param pushbullet      The {@link Pushbullet} instance to use
     * @param listRequestType The type of the request to be used
     * @param <T>             The generic type of the list
     * @return
     */
    public static <T> ListRequestBuilder<T> of(Pushbullet pushbullet, ListRequestType<T> listRequestType) {
        return new ListRequestBuilder<>(pushbullet, listRequestType);
    }

    /**
     * Sets the minimum timestamp for items
     * @param modifiedAfter The minimum timestamp
     * @return This {@link ListRequestBuilder}
     */
    public ListRequestBuilder<T> modifiedAfter(double modifiedAfter) {
        this.modifiedAfter = Math.max(modifiedAfter, 0);
        return this;
    }

    /**
     * Sets the maximum length of the requested list,
     * if completeList is true and cursor is not null,
     * new items will automatically be appended to the list when {@link #execute()} was used
     * @param limit The max limit, maximum = 500
     * @return This {@link ListRequestBuilder}
     */
    public ListRequestBuilder<T> limit(int limit) {
        this.limit = Math.min(Math.max(limit, 0), DEFAULT_LIMIT);
        return this;
    }

    /**
     * Executes the request and returns the list
     * @return The list of items
     */
    @SneakyThrows
    public List<T> execute() {
        ListItemsRequest itemRequest = request.getRequestType().newInstance();
        itemRequest
                .setCursor(cursor)
                .setModifiedAfter(modifiedAfter)
                .setLimit(limit)
                .setOnlyShowActiveItems(onlyShowActiveItems);
        if(completeList) {
            return ListUtil.fullList(pushbullet, itemRequest, request.getFunction());
        }
        else {
            ListResponse response = pushbullet.executeRequest(itemRequest);
            lastReturnedCursor = response.getCursor();
            return request.getFunction().apply(response);
        }
    }
} @Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ListRequestType<TResponseType> {

    /**
     * The request class
     */
    private final Class<? extends ListItemsRequest> requestType;

    /**
     * The function to use for the result
     */
    private final Function<ListResponse, List<TResponseType>> function;

    /**
     * Lists all accounts
     */
    public final static ListRequestType<Account> LIST_ACCOUNTS = new ListRequestType<>(ListAccountsRequest.class, ListResponse::getAccounts);

    /**
     * Lists all blocks
     */
    public final static ListRequestType<Block> LIST_BLOCKS = new ListRequestType<>(ListBlocksRequest.class, ListResponse::getBlocks);

    /**
     * Lists all own channels
     */
    public final static ListRequestType<OwnChannel> LIST_CHANNELS = new ListRequestType<>(ListChannelsRequest.class, ListResponse::getChannels);

    /**
     * Lists all chats
     */
    public final static ListRequestType<Chat> LIST_CHATS = new ListRequestType<>(ListChatsRequest.class, ListResponse::getChats);

    /**
     * Lists all oauth clients
     */
    public final static ListRequestType<OAuthClient> LIST_CLIENTS = new ListRequestType<>(ListClientsRequest.class, ListResponse::getClients);

    /**
     * Lists all contacts
     */
    public final static ListRequestType<Contact> LIST_CONTACTS = new ListRequestType<>(ListContactsRequest.class, ListResponse::getContacts);

    /**
     * Lists all devices
     */
    public final static ListRequestType<Device> LIST_DEVICES = new ListRequestType<>(ListDevicesRequest.class, ListResponse::getDevices);

    /**
     * Lists all grants
     */
    public final static ListRequestType<Grant> LIST_GRANTS = new ListRequestType<>(ListGrantsRequest.class, ListResponse::getGrants);

    /**
     * Lists all pushes
     */
    public final static ListRequestType<Push> LIST_PUSHES = new ListRequestType<>(ListPushesRequest.class, ListResponse::getPushes);

    /**
     * Lists all subscriptions
     */
    public final static ListRequestType<Subscription> LIST_SUBSCRIPTIONS = new ListRequestType<>(ListSubscriptionsRequest.class, ListResponse::getSubscriptions);

} @Getter
@RequiredArgsConstructor
public abstract class Request<TResult, TMessage extends HttpUriRequest> {

    /**
     * The relative path for the request
     */
    private final transient String relativePath;

    /**
     * Used to apply parameters to the {@link URIBuilder}
     * @param builder The builder to apply parameters to
     */
    public void applyParameters(URIBuilder builder) {}

    protected void addParam(String key, Object value, URIBuilder builder) {
        if(value != null) builder.addParameter(key, value.toString());
    }

    protected void setJsonBody(String jsonBody, TMessage message) {
        if(!(message instanceof HttpEntityEnclosingRequest)) return;
        StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON);
        HttpEntityEnclosingRequest request = ((HttpEntityEnclosingRequest) message);
        request.setEntity(entity);
    }

    protected void setJsonBody(@NonNull JsonElement element, TMessage message) {
        setJsonBody(element.toString(), message);
    }
} public class DeleteAllPushesRequest extends DeleteRequest<Void> {

    public DeleteAllPushesRequest() {
        super(Urls.PUSHES);
    }
} public interface Pushable {

    /**
     * Used to send a push to this object
     * @param push The push to send to
     * @return An instance of the sent push
     */
    Push push(SendablePush push);

    /**
     * Used to send a push to this object
     * @param pushbullet The pushbullet instance
     * @param push       The push to send
     * @return An instance of the sent push
     */
    default Push sendPush(Pushbullet pushbullet, SendablePush push) {
        return pushbullet.executeRequest(new SendPushRequest(push));
    }

    /**
     * Used to push a link to this object
     * @param title The title of the push
     * @param body  The body of the push
     * @param url   The link of the push
     * @return An instance of the sent push
     */
    default LinkPush pushLink(String title, String body, String url) {
        return (LinkPush) push(new SendableLinkPush(title, body, url));
    }

    /**
     * Used to push an address to this object
     * @param name    The name of the address
     * @param address The address
     * @return An instance of the sent push
     */
    @Deprecated
    default AddressPush pushAddress(String name, String address) {
        return (AddressPush) push(new SendableAddressPush(name, address));
    }

    /**
     * Used to push a note to this object
     * @param title The title of the note
     * @param body  The body of the note
     * @return An instance of the sent push
     */
    default NotePush pushNote(String title, String body) {
        return (NotePush) push(new SendableNotePush(title, body));
    }

    /**
     * Used to push a list to this object
     * @param title The title of the list
     * @param items The items of the list
     * @return An instance of the sent push
     */
    @Deprecated
    default ListPush pushList(String title, List<String> items) {
        return (ListPush) push(new SendableListPush(title, items));
    }

    /**
     * Used to push a file to this object
     * @param body The body of this push
     * @param file The file of this push
     * @return An instance of the sent push
     */
    default FilePush pushFile(String body, UploadFile file) {
        return (FilePush) push(new SendableFilePush(body, file));
    }
} @Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ListResponse {

    /**
     * The list of accounts
     */
    private List<Account> accounts;

    /**
     * The list of blocked users
     */
    private List<Block> blocks;

    /**
     * The list of own channels
     */
    private List<OwnChannel> channels;

    /**
     * The list of own chats
     */
    private List<Chat> chats;

    /**
     * The list of created oauth clients
     */
    private List<OAuthClient> clients;

    /**
     * The list of contacts
     */
    private List<Contact> contacts;

    /**
     * The list of devices
     */
    private List<Device> devices;

    /**
     * The list of granted clients
     */
    private List<Grant> grants;

    /**
     * The list of sent or received pushes
     */
    private List<Push> pushes;

    /**
     * The list of subscriptions
     */
    private List<Subscription> subscriptions;

    /**
     * Cursor for pagination
     */
    @Setter
    private String cursor;

    public ListResponse addAll(ListResponse other) {
        accounts.addAll(other.getAccounts());
        channels.addAll(other.getChannels());
        chats.addAll(other.getChats());
        clients.addAll(other.getClients());
        contacts.addAll(other.getContacts());
        devices.addAll(other.getDevices());
        grants.addAll(other.getGrants());
        pushes.addAll(other.getPushes());
        subscriptions.addAll(other.getSubscriptions());
        return this;
    }
} @Data
public abstract class PushbulletContainer {

    /**
     * The pushbullet instance that this object is linked to
     */
    private transient Pushbullet pushbullet;
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Account extends PushbulletObject implements Deletable {

    /**
     * The type of this account
     */
    private String type;

    /**
     * The id of this account
     */
    private String id;

    /**
     * The email of this account
     */
    private String email;

    /**
     * The normalized email of this account
     */
    @SerializedName("email_normalized")
    private String normalizedEmail;

    /**
     * The name of this account
     */
    private String name;

    /**
     * The url to the image of this account
     */
    @SerializedName("image_url")
    private String imageUrl;

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificAccountRequest(getIdentity()));
        setActive(false);
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Block extends PushbulletObject implements Deletable {

    private BlockedUser user;

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificBlockRequest(getIdentity()));
        setActive(false);
    }
} @Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChannelInfo extends PushbulletIdentifiable implements Subscribable {

    /**
     * The tag of this channel
     */
    private String tag;

    /**
     * The name of this channel
     */
    private String name;

    /**
     * The description of this channel
     */
    private String description;

    /**
     * The url to the image of this channel
     */
    @SerializedName("image_url")
    private String imageUrl;

    /**
     * The url to the website of this channel
     */
    @SerializedName("website_url")
    private String websiteUrl;

    /**
     * The subscriber count of this channel
     */
    @SerializedName("subscriber_count")
    private int subscriberCount;

    /**
     * The recent pushes of this channel
     */
    @SerializedName("recent_pushes")
    private List<Push> recentPushes;

    public void subscribe() {
        getPushbullet().executeRequest(new SubscribeToChannelRequest(tag));
        subscriberCount++;
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OwnChannel extends PushbulletObject implements Deletable {

    /**
     * The tag of this channel
     */
    private String tag;

    /**
     * The name of this channel
     */
    private String name;

    /**
     * The description of this channel
     */
    private String description;

    /**
     * The url to the image of this channel
     */
    @SerializedName("image_url")
    private String imageUrl;

    public void push(SendablePush push) {
        if(!isActive()) return;
        push = push.clone().setReceiver(ReceiverType.CHANNEL, tag);
        getPushbullet().executeRequest(new SendPushToChannelRequest(push));
    }

    public void sendPush(Pushbullet pushbullet, SendablePush push) {
        pushbullet.executeRequest(new SendPushRequest(push));
    }

    /**
     * Used to push a link to this channel
     * @param title The title of the push
     * @param body  The body of the push
     * @param url   The link of the push
     */
    public void pushLink(String title, String body, String url) {
        push(new SendableLinkPush(title, body, url));
    }

    /**
     * Used to push an address to this channel
     * @param name    The name of the address
     * @param address The address
     */
    @Deprecated
    public void pushAddress(String name, String address) {
        push(new SendableAddressPush(name, address));
    }

    /**
     * Used to push a note to this channel
     * @param title The title of the note
     * @param body  The body of the note
     */
    public void pushNote(String title, String body) {
        push(new SendableNotePush(title, body));
    }

    /**
     * Used to push a list to this channel
     * @param title The title of the list
     * @param items The items of the list
     */
    @Deprecated
    public void pushList(String title, List<String> items) {
        push(new SendableListPush(title, items));
    }

    /**
     * Used to push a file to this
     * @param body The body of this push
     * @param file The file of this push
     */
    public void pushFile(String body, UploadFile file) {
        push(new SendableFilePush(body, file));
    }

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificChannelRequest(getIdentity()));
        setActive(false);
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Subscription extends PushbulletObject implements Deletable, Mutable, Unsubscribable {

    /**
     * The channel info of this subscription
     */
    private ChannelInfo channel;

    /**
     * Whether this subscription is muted
     */
    private boolean muted;

    @Override
    public void unsubscribe() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificSubscriptionRequest(getIdentity()));
        setActive(false);
    }

    @Override
    public void delete() {
        unsubscribe();
    }

    public void mute() {
        updateMute(true);
    }

    public void unmute() {
        updateMute(false);
    }

    private void updateMute(boolean muteStatus) {
        getPushbullet().executeRequest(new UpdateSubscriptionMuteStatusRequest(getIdentity(), muteStatus));
        muted = muteStatus;
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Chat extends PushbulletObject implements Deletable, Mutable, Pushable, Blockable {

    /**
     * The user this chat is held with
     */
    private ChatUser with;

    /**
     * Whether this chat is muted
     */
    private boolean muted;

    @Override
    public void delete() {
        if(!isActive()) return;
        DeleteSpecificChatRequest chatRequest = new DeleteSpecificChatRequest(getIdentity());
        getPushbullet().executeRequest(chatRequest);
        setActive(false);
    }

    public void mute() {
        updateMute(true);
    }

    public void unmute() {
        updateMute(false);
    }

    /**
     * Used to block the user this chat is held with
     */
    @Override
    public void block() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new BlockUserRequest(getWith().getEmail()));
        setActive(false);
    }

    private void updateMute(boolean muteStatus) {
        getPushbullet().executeRequest(new UpdateChatMuteStatusRequest(getIdentity(), muteStatus));
        muted = muteStatus;
    }

    @Override
    public Push push(SendablePush push) {
        return with.push(push);
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Device extends PushbulletObject implements Deletable, Pushable, SmsSendable {

    /**
     * The nickname of this device
     */
    private String nickname;

    /**
     * Whether the nickname of this device has been generated
     */
    @SerializedName("generated_nickname")
    private boolean nicknameGenerated;

    /**
     * The type of this device
     */
    private String type;

    /**
     * The kind of this device
     */
    private String kind;

    /**
     * The icon of this device
     */
    private String icon;

    /**
     * Whether it is possible to push to this device
     */
    private boolean pushable;

    /**
     * The push token for this device
     */
    @SerializedName("push_token")
    private String pushToken;

    /**
     * The app version of the pushbullet app on this device
     */
    @SerializedName("app_version")
    private int appVersion;

    /**
     * The fingerprint of this device
     */
    @SerializedName("fingerprint")
    private String fingerPrint;

    /**
     * The manufacturer of this device
     */
    private String manufacturer;

    /**
     * The model of this device
     */
    private String model;

    @SerializedName("has_sms")
    @Accessors(fluent = true)
    private boolean hasSms;

    public Push push(SendablePush push) {
        if(!isPushable()) return null;
        push = push.clone();
        push.setReceiver(ReceiverType.DEVICE, getIdentity());
        return sendPush(getPushbullet(), push);
    }

    /**
     * Sent a text to the clipboard of the other registered devices that have universal copy paste enabled
     * @param content The text to
     * @param toTypes The types of the devices that should receive the text, leave empty for all
     */
    public void sendToOtherClipboards(String content, String... toTypes) {
        ClipEphemeral push = new ClipEphemeral();
        push.setBody(content)
                .setSourceDeviceIdentity(getIdentity());
        getPushbullet().pushEphemeral(push, toTypes);
    }

    /**
     * Returns all pushes that this device has received
     * @return A list of pushes that this device has received
     */
    public List<Push> getReceivedPushes() {
        return getPushbullet().getAllPushes().stream().filter(push -> getIdentity().equals(push.getTargetDeviceIdentity())).collect(Collectors.toList());
    }

    /**
     * Returns the phonebook of this device
     * @return The phonebook of this device
     */
    public Phonebook getPhonebook() {
        Phonebook phonebook = getPushbullet().executeRequest(new ListPhonebookRequest(getIdentity()));
        phonebook.setDevice(this);
        return phonebook;
    }

    @Override
    public void delete() {
        if(!isActive()) return;
        DeleteSpecificDeviceRequest deleteSpecificDeviceRequest = new DeleteSpecificDeviceRequest(getIdentity());
        getPushbullet().executeRequest(deleteSpecificDeviceRequest);
        setActive(false);
    }

    @Override
    public void sendSMS(String number, String body) {
        if(!isActive()) return;
        if(!hasSms()) return;
        SmsReplyEphemeral smsReplyEphemeralPush = new SmsReplyEphemeral();
        smsReplyEphemeralPush
                .setMessage(body)
                .setPackageName("com.pushbullet.android")
                .setPhoneNumber(number)
                .setTargetDeviceIdentity(getIdentity())
                .setSourceUserIdentity(getPushbullet().getCurrentUser().getIdentity());
        getPushbullet().pushEphemeral(smsReplyEphemeralPush);
    }
} @Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AwsAuthData {

    /**
     * The AWS access key id
     */
    @SerializedName("awsaccesskeyid")
    private String awsAccessKeyId;

    /**
     * The access control list for AWS
     */
    @SerializedName("acl")
    private String accessControlList;

    /**
     * The key for AWS
     */
    private String key;

    /**
     * The signature for AWS
     */
    private String signature;

    /**
     * The policy for AWS
     */
    private String policy;

    /**
     * The content type for AWS
     */
    @SerializedName("content-type")
    private String contentType;
} @Getter
public class UploadFile {

    /**
     * The mime type of this file
     */
    @SerializedName("file_type")
    private String fileType;

    /**
     * The name of this file
     */
    @SerializedName("file_name")
    private String fileName;

    /**
     * The url to this file
     */
    @SerializedName("file_url")
    private String fileUrl;

    /**
     * The upload url that this file should be uploaded to
     */
    @SerializedName("upload_url")
    private String uploadUrl;

    /**
     * The data for the upload to AWS
     */
    private AwsAuthData data;
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Grant extends PushbulletObject implements Deletable {

    /**
     * The "granted client" of this grant
     */
    private GrantClient client;

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificGrantRequest(getIdentity()));
        setActive(false);
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class GrantClient extends PushbulletIdentifiable {

    /**
     * The name of this granted client
     */
    private String name;

    /**
     * The url to the image of this granted client
     */
    @SerializedName("image_url")
    private String imageUrl;

    /**
     * The url to the website of this granted client
     */
    @SerializedName("website_url")
    private String websiteUrl;
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OAuthClient extends PushbulletObject implements Deletable {

    /**
     * The name of this client
     */
    private String name;

    /**
     * The url to the image of this client
     */
    @SerializedName("image_url")
    private String imageUrl;

    /**
     * The url to the website of this client
     */
    @SerializedName("website_url")
    private String websiteUrl;

    /**
     * The uri users should be redirected to
     */
    @SerializedName("redirect_uri")
    private String redirectUri;

    /**
     * The id of this client
     */
    @SerializedName("client_id")
    private String clientId;

    /**
     * The secret of this client
     */
    @SerializedName("client_secret")
    private String clientSecret;

    /**
     * The allowed origin of this client
     */
    @SerializedName("allowed_origin")
    private String allowedOrigin;

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificClientRequest(getIdentity()));
        setActive(false);
    }
} @Accessors(chain = true)
@Data
public abstract class SendablePush implements Cloneable {

    private final PushType type;

    /**
     * The identity of the device the push should be sent to
     */
    @SerializedName("device_iden")
    @Setter(AccessLevel.NONE)
    private String deviceIdentity;

    /**
     * The email of the user the push should be sent to
     */
    @SerializedName("email")
    @Setter(AccessLevel.NONE)
    private String email;

    /**
     * The tag of the channel the push should be sent to
     */
    @SerializedName("channel_tag")
    @Setter(AccessLevel.NONE)
    private String channelTag;

    /**
     * The identity of the oauth client the push should be sent to
     */
    @SerializedName("client_iden")
    @Setter(AccessLevel.NONE)
    private String clientIdentity;

    /**
     * A unique identifier for this push
     */
    private String guid;

    /**
     * Used to set a receiver for this push
     * @param type  The type of receiver this push should be sent to
     * @param value The channel tag / email / client identity or device identity
     * @return The object the method was executed on
     */
    public SendablePush setReceiver(ReceiverType type, String value) {
        clearReceivers();
        switch (type) {
            case DEVICE:
                deviceIdentity = value;
                break;
            case EMAIL:
                email = value;
                break;
            case CHANNEL:
                channelTag = value;
                break;
            case OAUTH_CLIENT:
                clientIdentity = value;
                break;
        }
        return this;
    }

    /**
     * Used to clear all receivers of this push
     */
    public void clearReceivers() {
        this.deviceIdentity = null;
        this.email = null;
        this.channelTag = null;
        this.clientIdentity = null;
    }

    /**
     * Clone this push
     * @return A clone of this object
     */
    @Override
    @SneakyThrows
    public SendablePush clone() {
        return (SendablePush) super.clone();
    }
} @Getter
@Setter(AccessLevel.PROTECTED)
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Push extends PushbulletObject implements Deletable, Dismissable {

    /**
     * The type of the push
     */
    private PushType type;

    /**
     * Whether the push has already been dimissed
     */
    private boolean dismissed;

    /**
     * The direction the push was sent to
     */
    private Direction direction;

    /**
     * The name of the sender
     */
    @SerializedName("sender_name")
    private String senderName;

    /**
     * The identity of the channel the push was sent to
     */
    @SerializedName("channel_iden")
    private String channelIdentity;

    /**
     * The client's identity
     */
    @SerializedName("client_iden")
    private String clientIdentity;

    /**
     * The identity of the device the push was sent to
     */
    @SerializedName("target_device_iden")
    private String targetDeviceIdentity;

    /**
     * The sender's identity
     */
    @SerializedName("sender_iden")
    private String senderIdentity;

    /**
     * The sender's email
     */
    @SerializedName("sender_email")
    private String senderEmail;

    /**
     * The sender's normalized email
     */
    @SerializedName("sender_email_normalized")
    private String normalizedSenderEmail;

    /**
     * The receiver's identity
     */
    @SerializedName("receiver_iden")
    private String receiverIdentity;

    /**
     * The receiver's email
     */
    @SerializedName("receiver_email")
    private String receiverEmail;

    /**
     * The receiver's normalized email
     */
    @SerializedName("receiver_email_normalized")
    private String normalizedReceiverEmail;

    /**
     * The list of awake apps the push was sent to when it was created
     */
    @SerializedName("awake_app_guids")
    private List<String> awakeAppGuids;

    /**
     * A unique identifier for the push, set by the client
     */
    private String guid;

    @Override
    public void delete() {
        if(!isActive()) return;
        DeleteSpecificPushRequest deleteSpecificPushRequest = new DeleteSpecificPushRequest(getIdentity());
        getPushbullet().executeRequest(deleteSpecificPushRequest);
        setActive(false);
    }

    /**
     * Show the push again if it was already dismissed
     */
    public void show() {
        if(!isDismissed()) return;
        getPushbullet().executeRequest(new ChangePushDismissStatusRequest(getIdentity(), false));
        setDismissed(false);
    }

    @Override
    public void dismiss() {
        if(isDismissed()) return;
        getPushbullet().executeRequest(new ChangePushDismissStatusRequest(getIdentity(), true));
        setDismissed(true);
    }

    /**
     * Get the title of this push
     * @return The title of this push
     */
    public String getTitle() {
        return null;
    }

    /**
     * Get the body of this push
     * @return The body of this push
     */
    public String getBody() {
        return null;
    }
} @Getter
@ToString(callSuper = true)
@Deprecated
public class AddressPush extends Push {

    private AddressPush() {
        setType(PushType.ADDRESS);
    }

    /**
     * The name of the address
     */
    private String name;

    /**
     * The address
     */
    private String address;

    @Override
    public String getTitle() {
        return name;
    }

    @Override
    public String getBody() {
        return address;
    }
} @Getter
@ToString(callSuper = true)
public class FilePush extends Push {

    private FilePush() {
        setType(PushType.FILE);
    }

    private String body;

    /**
     * The file's name
     */
    @SerializedName("file_name")
    private String fileName;

    /**
     * The file's mime type
     */
    @SerializedName("file_type")
    private String fileType;

    /**
     * The file's url
     */
    @SerializedName("file_url")
    private String fileUrl;

    /**
     * The url of the image<br>
     * Only set if the file's mime type is image/*
     */
    @SerializedName("image_url")
    private String imageUrl;

    /**
     * The width of the image<br>
     * Only set if the file's mime type is image/*
     */
    @SerializedName("image_width")
    private int imageWidth;

    /**
     * The height of the image<br>
     * Only set if the file's mime type is image/*
     */
    @SerializedName("image_height")
    private int imageHeight;

    /**
     * @return Whether the file of this push is an image file
     */
    public boolean hasImageFile() {
        return imageUrl != null;
    }

    @Override
    public String getTitle() {
        return null;
    }
} @Accessors(chain = true)
@Getter
@ToString(callSuper = true)
public class LinkPush extends Push {

    private LinkPush() {
        setType(PushType.LINK);
    }

    /**
     * The title of the push
     */
    private String title;

    /**
     * The body of the push
     */
    private String body;

    /**
     * The link of this push
     */
    private String url;
} @Getter
@ToString(callSuper = true)
@Deprecated
public class ListPush extends Push {

    private ListPush() {
        setType(PushType.LIST);
    }

    /**
     * The title of the push
     */
    private String title;

    /**
     * The items of the list
     */
    private List<ListItem> items;

    @Override
    public String getBody() {
        return null;
    }
} @Getter
@ToString(callSuper = true)
public class NotePush extends Push {

    private NotePush() {
        setType(PushType.NOTE);
    }

    /**
     * The title of the note
     */
    private String title;

    /**
     * The body of the note
     */
    private String body;
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BlockedUser extends PushbulletIdentifiable {

    /**
     * The email of this user
     */
    private String email;

    /**
     * The normalized email of this user
     */
    @SerializedName("email_normalized")
    private String normalizedEmail;

    /**
     * The name of this user
     */
    private String name;

    /**
     * The url to the image of this user
     */
    @SerializedName("image_url")
    private String imageUrl;
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatUser extends PushbulletIdentifiable implements Pushable, Blockable {

    /**
     * The type of this user
     */
    private String type;

    /**
     * The email of this user
     */
    private String email;

    /**
     * The normalized email of this user
     */
    @SerializedName("email_normalized")
    private String normalizedEmail;

    /**
     * The name of this user
     */
    private String name;

    /**
     * The url to the image of this user
     */
    @SerializedName("image_url")
    private String imageUrl;

    @Override
    public Push push(SendablePush push) {
        push = push.clone();
        push.setReceiver(ReceiverType.EMAIL, getEmail());
        return sendPush(getPushbullet(), push);
    }

    /**
     * Used to block this user
     */
    @Override
    public void block() {
        getPushbullet().executeRequest(new BlockUserRequest(getEmail()));
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Deprecated
public class Contact extends User implements Deletable {

    /**
     * The status of this contact
     */
    private String status;

    @Override
    public void delete() {
        if(!isActive()) return;
        getPushbullet().executeRequest(new DeleteSpecificContactRequest(getIdentity()));
        setActive(false);
    }
} @Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CurrentUser extends User {

    /**
     * The google user info of this user
     */
    @SerializedName("google_userinfo")
    private GoogleUserInfo googleUserInfo;

    /**
     * The preferences of this user
     */
    private JsonObject preferences;

    /**
     * The maximum allowed upload size for files allowed to this user
     */
    @SerializedName("max_upload_size")
    private long maxUploadSize;

    /**
     * Used to update the preferences of this user
     * @param preferences The preferences object
     */
    public void updatePreferences(JsonObject preferences) {
        getPushbullet().executeRequest(new UpdatePreferencesRequest(preferences));
        this.preferences = preferences;
    }

    @Override
    public void block() {
        throw new UnsupportedOperationException();
    }

    @Getter
    public class GoogleUserInfo {

        /**
         * The google name of this user
         */
        private String name;
    }
} @Getter
public final class PushbulletWebsocketClient {

    private final static String BASE_URL = "wss://stream.pushbullet.com:443/websocket/";

    private final Pushbullet pushbullet;

    protected final Collection<PushbulletWebsocketListener> websocketListeners = new ArrayList<>();

    private ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build();

    private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();

    private Session websocketSession;

    public PushbulletWebsocketClient(Pushbullet pushbullet) {
        this.pushbullet = pushbullet;
    }

    /**
     * Unregister a listener that listens to this websocket stream
     * @param listener The listener to unregister
     */
    public void registerListener(PushbulletWebsocketListener listener) {
        if(!websocketListeners.contains(listener)) {
            websocketListeners.add(listener);
        }
    }

    /**
     * Register a new listener that listens to this websocket stream
     * @param listener The listener to register
     */
    public void unregisterListener(PushbulletWebsocketListener listener) {
        if(websocketListeners.contains(listener)) {
            websocketListeners.remove(listener);
        }
    }

    /**
     * @return Whether the client is connected to the server
     */
    public synchronized boolean isConnected() {
        return websocketSession != null && websocketSession.isOpen();
    }

    /**
     * Connects the client to the server
     */
    @SneakyThrows
    public synchronized void connect() {
        if(websocketSession != null) return;
        websocketSession = webSocketContainer.connectToServer(new Endpoint() {
            @Override
            public void onOpen(Session session, EndpointConfig config) {
                //noinspection Convert2Lambda
                session.addMessageHandler(new MessageHandler.Whole<String>() {
                    @Override
                    public void onMessage(String message) {
                        websocketListeners.forEach(websocketListener -> websocketListener.handle(pushbullet, pushbullet.getGson().fromJson(message, StreamMessage.class)));
                    }
                });
            }
        }, config, new URI(BASE_URL + pushbullet.getAccessToken()));
    }

    /**
     * Disconnects the client from the server
     */
    public synchronized void disconnect() {
        if(websocketSession == null) return;
        try {
            websocketSession.close();
        } catch (IOException e) {

        }
        finally {
            websocketSession = null;
        }
    }
} @Data
@ToString(callSuper = true)
public class NopStreamMessage extends StreamMessage {

    public NopStreamMessage() {
        setType(StreamMessageType.NOP);
    }
} @Data
@ToString(callSuper = true)
public class PushStreamMessage extends StreamMessage {

    public PushStreamMessage() {
        setType(StreamMessageType.PUSH);
    }

    /**
     * The ephemeral that has been transferred with this message
     */
    private Ephemeral push;

    /**
     * The list of targets this message was or should be transferred to
     */
    private List<String> targets;
} @Data
public class StreamMessage {

    /**
     * The type of this stream message
     */
    @Setter(AccessLevel.PROTECTED)
    private StreamMessageType type;
} @Data
@ToString(callSuper = true)
public class TickleStreamMessage extends StreamMessage {

    public TickleStreamMessage() {
        setType(StreamMessageType.TICKLE);
    }

    /**
     * The sub type of this tickle message<br>
     * "push" if something at /v2/pushes has changed<br>
     * "device" if something at /v2/devices has changed
     */
    @SerializedName("subtype")
    private String subType;
} public class ClassUtil {

    /**
     * Searches for the request class with the response type
     * @param input The class to start with
     * @return The parameterized type it ended with or null
     */
    public static ParameterizedType searchForSuperclassWithResponseType(Class<? extends Request> input) {
        Type genericSuperType = input.getGenericSuperclass();
        while (!(genericSuperType instanceof ParameterizedType) && !genericSuperType.equals(Object.class)) {
            genericSuperType = ((Class<?>) genericSuperType).getGenericSuperclass();
        }
        if(genericSuperType instanceof ParameterizedType) {
            return ((ParameterizedType) genericSuperType);
        }
        return null;
    }

    /**
     * Searches for the request class with the {@link org.apache.http.client.methods.HttpUriRequest} type
     * @param input The class to start with
     * @return The parameterized type it ended with or null
     */
    public static ParameterizedType searchForSuperclassWithHttpUriRequestType(Class<? extends Request> input) {
        Type genericSuperType = input.getGenericSuperclass();
        ParameterizedType type = null;
        while (!(genericSuperType instanceof ParameterizedType) && !genericSuperType.equals(Object.class)) {
            genericSuperType = ((Class<?>) genericSuperType).getGenericSuperclass();
        }
        while((genericSuperType instanceof ParameterizedType) && ((type = (ParameterizedType) genericSuperType)).getActualTypeArguments().length < 2) {
            genericSuperType = ((Class<?>) ((ParameterizedType) genericSuperType).getRawType()).getGenericSuperclass();
        }
        return type;
    }
} public class FunctionUtil {

    /**
     * Returns an object of a list where at least one result of the functions matches with the value<br>
     * The U param of the functions is given by another function<br>
     * Yeah, I know, it's complicated
     * @param objects        The object list
     * @param mapperFunction The function
     * @param value          The value to match
     * @param functions      The functions array
     * @param <T>            The object type
     * @param <U>            The mapping type
     * @param <V>            The result type of the array's functions
     * @return An object of the list or null
     */
    @SafeVarargs
    public static <T, U, V> T getFirstWithFunctionWithCondition(List<T> objects, Function<T, U> mapperFunction, V value, Function<U, V>... functions) {
        Optional<T> first = objects.stream()
                .filter(object -> {
                    U methodVal = mapperFunction.apply(object);
                    boolean has = false;
                    for (Function<? super U, V> function : functions) {
                        has = has || value.equals(function.apply(methodVal));
                    }
                    return has;
                }).findFirst();
        return first.isPresent() ? first.get() : null;
    }

    /**
     * Returns an object of a list where at least one result of the functions matches with the value<br>
     * @param objects   The object list
     * @param value     The value to match
     * @param functions The functions array
     * @param <T>       The object type
     * @param <U>       The result type of the array's functions
     * @return An object of the list or null
     */
    @SafeVarargs
    public static <T, U> T getFirstWithCondition(List<T> objects, U value, Function<? super T, U>... functions) {
        Optional<T> first = objects.stream()
                .filter(object -> {
                    boolean has = false;
                    for (Function<? super T, U> function : functions) {
                        has = has || value.equals(function.apply(object));
                    }
                    return has;
                }).findFirst();
        return first.isPresent() ? first.get() : null;
    }
} public class HttpUtil {

    /**
     * Get a header value of {@link HttpResponse} or a default value
     * @param response     The response to get the header from
     * @param headerName   The name of the header
     * @param defaultValue The default value
     * @return The header value or the default value
     */
    public static String getHeaderValue(HttpResponse response, String headerName, String defaultValue) {
        Header header;
        return ((header = response.getFirstHeader(headerName)) != null) ? header.getValue() : defaultValue;
    }
} public class ListUtil {

    /**
     * Returns a distinct list from an array
     * @param input The input array
     * @param <T>   The array type
     * @return A distinct list
     */
    public static <T> List<T> distinctList(T[] input) {
        return distinctList(Arrays.asList(input));
    }

    /**
     * Returns a distinct list from a list
     * @param input The input list
     * @param <T>   The list type
     * @return A distinct list
     */
    public static <T> List<T> distinctList(List<T> input) {
        if(input.isEmpty()) return input;
        return input.stream().distinct().collect(Collectors.toList());
    }

    /**
     * Returns a full list of a request that calls /v2/*s (e.g. /v2/pushes, /v2/devices, etc.)
     * @param pushbullet The pushbullet instance
     * @param request    The request object
     * @param function   The function of {@link ListResponse}
     * @param <T>        The result type of the function
     * @return A result object with the result type {@link T}
     */
    public static <T> List<T> fullList(Pushbullet pushbullet, ListItemsRequest request, Function<ListResponse, List<T>> function) {
        ListResponse response = pushbullet.executeRequest(request);
        List<T> returnList = function.apply(response);
        while(response.getCursor() != null) {
            response = pushbullet.executeRequest(request.setCursor(response.getCursor()));
            returnList.addAll(function.apply(response));
        }
        return returnList;
    }

    /**
     * Returns a complete {@link ListResponse} by calling /v2/everything
     * @param pushbullet The pushbullet instance
     * @return A complete {@link ListResponse} object
     */
    public static ListResponse completeListResponse(Pushbullet pushbullet) {
        ListItemsRequest request = new ListEverythingRequest();
        ListResponse response = pushbullet.executeRequest(request);
        ListResponse tempResponse = response;
        while(tempResponse.getCursor() != null) {
            tempResponse = pushbullet.executeRequest(request.setCursor(tempResponse.getCursor()));
            response.addAll(tempResponse);
        }
        response.setCursor(null);
        return response;
    }
}
import com.github.sheigutn.pushbullet.cryptography.Encryption;
import com.github.sheigutn.pushbullet.cryptography.EphemeralEncryptionHandler;
import com.github.sheigutn.pushbullet.ephemeral.*;
import com.github.sheigutn.pushbullet.exception.PushbulletApiError;
import com.github.sheigutn.pushbullet.exception.PushbulletApiException;
import com.github.sheigutn.pushbullet.gson.PushbulletContainerPostProcessor;
import com.github.sheigutn.pushbullet.gson.RuntimeTypeAdapterFactory;
import com.github.sheigutn.pushbullet.http.EntityEnclosingRequest;
import com.github.sheigutn.pushbullet.http.ListRequestBuilder;
import com.github.sheigutn.pushbullet.http.ListRequestType;
import com.github.sheigutn.pushbullet.http.Request;
import com.github.sheigutn.pushbullet.http.defaults.delete.DeleteAllPushesRequest;
import com.github.sheigutn.pushbullet.http.defaults.get.*;
import com.github.sheigutn.pushbullet.http.defaults.post.*;
import com.github.sheigutn.pushbullet.interfaces.Pushable;
import com.github.sheigutn.pushbullet.items.ListResponse;
import com.github.sheigutn.pushbullet.items.PushbulletContainer;
import com.github.sheigutn.pushbullet.items.account.Account;
import com.github.sheigutn.pushbullet.items.block.Block;
import com.github.sheigutn.pushbullet.items.channel.ChannelInfo;
import com.github.sheigutn.pushbullet.items.channel.OwnChannel;
import com.github.sheigutn.pushbullet.items.channel.Subscription;
import com.github.sheigutn.pushbullet.items.chat.Chat;
import com.github.sheigutn.pushbullet.items.device.Device;
import com.github.sheigutn.pushbullet.items.file.AwsAuthData;
import com.github.sheigutn.pushbullet.items.file.UploadFile;
import com.github.sheigutn.pushbullet.items.grant.Grant;
import com.github.sheigutn.pushbullet.items.grant.GrantClient;
import com.github.sheigutn.pushbullet.items.oauth.OAuthClient;
import com.github.sheigutn.pushbullet.items.push.sendable.SendablePush;
import com.github.sheigutn.pushbullet.items.push.sendable.defaults.*;
import com.github.sheigutn.pushbullet.items.push.sent.Push;
import com.github.sheigutn.pushbullet.items.push.sent.defaults.AddressPush;
import com.github.sheigutn.pushbullet.items.push.sent.defaults.FilePush;
import com.github.sheigutn.pushbullet.items.push.sent.defaults.LinkPush;
import com.github.sheigutn.pushbullet.items.push.sent.defaults.ListPush;
import com.github.sheigutn.pushbullet.items.push.sent.defaults.NotePush;
import com.github.sheigutn.pushbullet.items.user.BlockedUser;
import com.github.sheigutn.pushbullet.items.user.ChatUser;
import com.github.sheigutn.pushbullet.items.user.Contact;
import com.github.sheigutn.pushbullet.items.user.CurrentUser;
import com.github.sheigutn.pushbullet.stream.PushbulletWebsocketClient;
import com.github.sheigutn.pushbullet.stream.message.NopStreamMessage;
import com.github.sheigutn.pushbullet.stream.message.PushStreamMessage;
import com.github.sheigutn.pushbullet.stream.message.StreamMessage;
import com.github.sheigutn.pushbullet.stream.message.TickleStreamMessage;
import com.github.sheigutn.pushbullet.util.ClassUtil;
import com.github.sheigutn.pushbullet.util.FunctionUtil;
import com.github.sheigutn.pushbullet.util.HttpUtil;
import com.github.sheigutn.pushbullet.util.ListUtil;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonReader;
import io.gsonfire.GsonFireBuilder;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
package com.github.sheigutn.pushbullet;



@Getter
public class Pushbullet implements Pushable {

    /**
     * Base api url for requests
     */
    private static final String API_BASE_URL = "https://api.pushbullet.com/v2";

    /**
     * Header name for the access token
     */
    private static final String ACCESS_TOKEN_HEADER = "Access-Token";

    /**
     * Header name for the API limit
     */
    private static final String RATELIMIT_LIMIT_HEADER = "X-Ratelimit-Limit";

    /**
     * Header name for the remaining API operations
     */
    private static final String RATELIMIT_REMAINING_HEADER = "X-Ratelimit-Remaining";

    /**
     * Header name for the API operation reset timestamp
     */
    private static final String RATELIMIT_RESET_HEADER = "X-Ratelimit-Reset";

    /**
     * Access token required for API requests
     */
    private final String accessToken;

    /**
     * Gson instance to use for serializing and deserializing
     */
    private final Gson gson = new GsonFireBuilder()
            .registerPostProcessor(PushbulletContainer.class, new PushbulletContainerPostProcessor(this))
            .createGsonBuilder()
            .registerTypeAdapterFactory(
                    RuntimeTypeAdapterFactory.of(StreamMessage.class)