diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..62814d098
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.gradle/
+.idea/
+build/
+*.iml
+*.ipr
+*.iws
+*.class
+bin/
+gen/
+out/
+/src/test/resources/config.properties
\ No newline at end of file
diff --git a/README.md b/README.md
index be4b7cc9d..1e051bcc9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,16 @@
-# kotlin
\ No newline at end of file
+
+# kotlin
+
+## Using your PubNub keys
+
+If you would like to run integration tests against your keys, Execute the following commands to add your publish, subscribe and secret keys to your local copy of the SDK:
+
+
+```bash
+cd src/test/resources/
+echo pub_key=YOUR_PUB_KEY >> config.properties
+echo sub_key=YOUR_SUB_KEY >> config.properties
+echo pam_pub_key=YOUR_PAM_PUB_KEY >> config.properties
+echo pam_sub_key=YOUR_PAM_SUB_KEY >> config.properties
+echo pam_sec_key=YOUR_PAM_SEC_KEY >> config.properties
+```
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..3f5318a3a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,101 @@
+plugins {
+ id 'org.jetbrains.kotlin.jvm' version '1.3.72'
+ id 'maven'
+ id 'com.github.johnrengelman.shadow' version '4.0.2'
+ id 'java-library'
+ id 'com.bmuschko.nexus' version '2.3.1'
+}
+
+group = 'com.pubnub'
+version = '4.0.0'
+
+repositories {
+ mavenCentral()
+ jcenter()
+}
+
+dependencies {
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
+
+ implementation("com.squareup.retrofit2:retrofit:2.6.2")
+
+ api group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.12.6'
+
+ api 'com.google.code.gson:gson:2.8.6'
+ implementation group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.6.2'
+
+ implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.28'
+ testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.28'
+
+ testImplementation 'org.awaitility:awaitility-kotlin:4.0.1'
+ testImplementation 'com.github.tomakehurst:wiremock:2.26.3'
+
+ implementation group: 'org.json', name: 'json', version: '20190722'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2'
+}
+
+shadowJar {
+ classifier = "all"
+}
+
+tasks.withType(Test) {
+ useJUnitPlatform {
+ includeEngines 'junit-jupiter'
+ }
+}
+
+compileKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+}
+compileTestKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+extraArchive {
+ sources = true
+ tests = true
+ javadoc = true
+}
+
+nexus {
+ sign = true
+ repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
+ snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots'
+}
+
+modifyPom {
+ project {
+ name 'PubNub Kotlin SDK'
+ description 'PubNub is a cross-platform client-to-client (1:1 and 1:many) push service in the cloud, capable of\n' +
+ ' broadcasting real-time messages to millions of web and mobile clients simultaneously, in less than a quarter\n' +
+ ' second!'
+ url 'https://github.com/pubnub/kotlin'
+ inceptionYear '2009'
+
+ scm {
+ url 'https://github.com/pubnub/kotlin'
+ }
+
+ licenses {
+ license {
+ name 'MIT License'
+ url 'https://github.com/pubnub/pubnub-api/blob/master/LICENSE'
+ distribution 'repo'
+ }
+ }
+
+ developers {
+ developer {
+ id 'PubNub'
+ name 'PubNub'
+ email 'support@pubnub.com'
+ }
+ }
+ }
+}
+
+build.finalizedBy(shadowJar)
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..29e08e8ca
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..87b738cbd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..950461335
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Oct 23 15:55:29 CEST 2019
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..af6708ff2
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..6d57edc70
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..29161764f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'pubnub-kotlin'
\ No newline at end of file
diff --git a/src/main/java/com/pubnub/api/vendor/Base64.java b/src/main/java/com/pubnub/api/vendor/Base64.java
new file mode 100644
index 000000000..901562636
--- /dev/null
+++ b/src/main/java/com/pubnub/api/vendor/Base64.java
@@ -0,0 +1,746 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pubnub.api.vendor;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs 2045 and 3548.
+ */
+public class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ /**
+ * Flag to pass to indicate that it
+ * should not close the output stream it is wrapping when it
+ * itself is closed.
+ */
+ public static final int NO_CLOSE = 16;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(str.getBytes(Charset.forName("UTF-8")), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Non-data values in the DECODE arrays.
+ */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 3 / 4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p + 4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p + 1] & 0xff] << 12) |
+ (alphabet[input[p + 2] & 0xff] << 6) |
+ (alphabet[input[p + 3] & 0xff]))) >= 0) {
+ output[op + 2] = (byte) value;
+ output[op + 1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op + 2] = (byte) value;
+ output[op + 1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op + 1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0:
+ break;
+ case 1:
+ output_len += 2;
+ break;
+ case 2:
+ output_len += 3;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ assert encoder.op == output_len;
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * RFC 2045).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 8 / 5 + 10;
+ }
+
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+ case 1:
+ if (p + 2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ case 2:
+ if (p + 1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p + 3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p + 1] & 0xff) << 8) |
+ (input[p + 2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op + 1] = alphabet[(v >> 12) & 0x3f];
+ output[op + 2] = alphabet[(v >> 6) & 0x3f];
+ output[op + 3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p - tailLen == len - 1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p - tailLen == len - 2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ assert tailLen == 0;
+ assert p == len;
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len - 1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len - 2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p + 1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ private Base64() {
+ } // don't instantiate
+}
diff --git a/src/main/java/com/pubnub/api/vendor/Crypto.java b/src/main/java/com/pubnub/api/vendor/Crypto.java
new file mode 100644
index 000000000..c63f95f40
--- /dev/null
+++ b/src/main/java/com/pubnub/api/vendor/Crypto.java
@@ -0,0 +1,149 @@
+package com.pubnub.api.vendor;
+
+import com.google.gson.Gson;
+import com.pubnub.api.PubNubError;
+import com.pubnub.api.PubNubException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.spec.AlgorithmParameterSpec;
+
+
+public class Crypto {
+
+ byte[] keyBytes = null;
+ byte[] ivBytes = null;
+ String initializationVector = "0123456789012345";
+ String cipherKey;
+ boolean INIT = false;
+
+ public Crypto(String cipherKey) {
+ this.cipherKey = cipherKey;
+ }
+
+ public Crypto(String cipherKey, String customInitializationVector) {
+ if (customInitializationVector != null) {
+ this.initializationVector = customInitializationVector;
+ }
+
+ this.cipherKey = cipherKey;
+ }
+
+ public void initCiphers() throws PubNubException {
+ if (INIT)
+ return;
+ try {
+
+ keyBytes = new String(hexEncode(sha256(this.cipherKey.getBytes("UTF-8"))), "UTF-8")
+ .substring(0, 32)
+ .toLowerCase().getBytes("UTF-8");
+ ivBytes = initializationVector.getBytes("UTF-8");
+ INIT = true;
+ } catch (UnsupportedEncodingException e) {
+ throw newCryptoError(11, e);
+ }
+ }
+
+ public static byte[] hexEncode(byte[] input) throws PubNubException {
+ StringBuffer result = new StringBuffer();
+ for (byte byt : input)
+ result.append(Integer.toString((byt & 0xff) + 0x100, 16).substring(1));
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw newCryptoError(12, e);
+ }
+ }
+
+ public static PubNubException newCryptoError(int code, Exception exception) {
+ PubNubException pubNubException = new PubNubException();
+ pubNubException.setPubnubError(PubNubError.CRYPTO_ERROR);
+ pubNubException.setErrorMessage(exception.getClass().getSimpleName() + " " + exception.getMessage() + " " + code);
+ return pubNubException;
+ }
+
+ public String encrypt(String input) throws PubNubException {
+ try {
+ initCiphers();
+ AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
+ SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES");
+ Cipher cipher = null;
+ cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);
+ return new String(Base64.encode(cipher.doFinal(input.getBytes("UTF-8")), 0), Charset.forName("UTF-8"));
+ } catch (Exception e) {
+ throw newCryptoError(0, e);
+ }
+
+ }
+
+ /**
+ * Decrypt
+ *
+ * @param cipher_text
+ * @return String
+ * @throws PubNubException
+ */
+ public String decrypt(String cipher_text) throws PubNubException {
+ try {
+ initCiphers();
+ AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
+ SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES");
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec);
+ return new String(cipher.doFinal(Base64.decode(cipher_text, 0)), "UTF-8");
+ } catch (Exception e) {
+ throw newCryptoError(0, e);
+ }
+ }
+
+ public static byte[] hexStringToByteArray(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
+ }
+ return data;
+ }
+
+ /**
+ * Get MD5
+ *
+ * @param input
+ * @return byte[]
+ * @throws PubNubException
+ */
+ public static byte[] md5(String input) throws PubNubException {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("MD5");
+ byte[] hashedBytes = digest.digest(input.getBytes("UTF-8"));
+ return hashedBytes;
+ } catch (Exception e) {
+ throw newCryptoError(0, e);
+ }
+ }
+
+ /**
+ * Get SHA256
+ *
+ * @param input
+ * @return byte[]
+ * @throws PubNubException
+ */
+ public static byte[] sha256(byte[] input) throws PubNubException {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashedBytes = digest.digest(input);
+ return hashedBytes;
+ } catch (Exception e) {
+ throw newCryptoError(0, e);
+ }
+ }
+
+}
diff --git a/src/main/kotlin/com/pubnub/api/Endpoint.kt b/src/main/kotlin/com/pubnub/api/Endpoint.kt
new file mode 100644
index 000000000..4442a5a5b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/Endpoint.kt
@@ -0,0 +1,425 @@
+package com.pubnub.api
+
+import com.google.gson.JsonElement
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNStatusCategory
+import com.pubnub.api.enums.PNStatusCategory.*
+import com.pubnub.api.models.consumer.PNStatus
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.io.IOException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+import java.net.UnknownHostException
+import java.util.*
+
+abstract class Endpoint(protected val pubnub: PubNub) {
+
+ companion object {
+ private const val SERVER_RESPONSE_SUCCESS = 200
+ private const val SERVER_RESPONSE_BAD_REQUEST = 400
+ private const val SERVER_RESPONSE_FORBIDDEN = 403
+ private const val SERVER_RESPONSE_NOT_FOUND = 404
+ }
+
+ private lateinit var cachedCallback: (result: Output?, status: PNStatus) -> Unit
+ private lateinit var call: Call
+ private var silenceFailures = false
+
+ var queryParam: Map = emptyMap()
+
+ fun sync(): Output? {
+ validateParams()
+
+ call = doWork(createBaseParams())
+
+ val response =
+ try {
+ call.execute()
+ } catch (e: Exception) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ }
+ }
+
+ when {
+ response.isSuccessful -> {
+ storeRequestLatency(response)
+ return checkAndCreateResponse(response)
+ }
+ else -> {
+ val (errorString, errorJson) = extractErrorBody(response)
+ throw PubNubException(PubNubError.HTTP_ERROR).apply {
+ errorMessage = errorString
+ jso = errorJson.toString()
+ statusCode = response.code()
+ affectedCall = call
+ }
+ }
+ }
+ }
+
+ fun async(callback: (result: Output?, status: PNStatus) -> Unit) {
+ cachedCallback = callback
+
+ try {
+ validateParams()
+ call = doWork(createBaseParams())
+ } catch (pubnubException: PubNubException) {
+ callback.invoke(
+ null,
+ createStatusResponse(
+ category = PNBadRequestCategory,
+ exception = pubnubException
+ )
+ )
+ return
+ }
+
+ call.enqueue(object : Callback {
+
+ override fun onResponse(call: Call, response: Response) {
+
+ when {
+ response.isSuccessful -> {
+ // query params
+ storeRequestLatency(response)
+ try {
+ Triple(PNAcknowledgmentCategory, checkAndCreateResponse(response), null)
+ } catch (e: PubNubException) {
+ Triple(PNMalformedResponseCategory, null, e)
+ }.let {
+ callback.invoke(
+ it.second,
+ createStatusResponse(
+ category = it.first,
+ response = response,
+ exception = it.third
+ )
+ )
+ }
+ }
+ else -> {
+ val (errorString, errorJson) = extractErrorBody(response)
+
+ val exception = PubNubException(PubNubError.HTTP_ERROR).apply {
+ errorMessage = errorString
+ jso = errorJson.toString()
+ statusCode = response.code()
+ affectedCall = call
+ }
+
+ val pnStatusCategory = when (response.code()) {
+ SERVER_RESPONSE_FORBIDDEN -> PNAccessDeniedCategory
+ SERVER_RESPONSE_BAD_REQUEST -> PNBadRequestCategory
+ SERVER_RESPONSE_NOT_FOUND -> PNNotFoundCategory
+ else -> PNUnknownCategory
+ }
+
+ callback.invoke(
+ null,
+ createStatusResponse(
+ category = pnStatusCategory,
+ response = response,
+ exception = exception,
+ errorBody = errorJson
+ )
+ )
+ return
+ }
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+
+ if (silenceFailures) {
+ return
+ }
+
+ lateinit var pnStatusCategory: PNStatusCategory
+
+ val pubnubException = PubNubException(t.toString())
+
+ try {
+ throw t
+ } catch (networkException: UnknownHostException) {
+ pubnubException.pubnubError = PubNubError.CONNECTION_NOT_SET
+ pnStatusCategory = PNUnexpectedDisconnectCategory
+ } catch (connectException: ConnectException) {
+ pubnubException.pubnubError = PubNubError.CONNECT_EXCEPTION
+ pnStatusCategory = PNUnexpectedDisconnectCategory
+ } catch (socketTimeoutException: SocketTimeoutException) {
+ pubnubException.pubnubError = PubNubError.SUBSCRIBE_TIMEOUT
+ pnStatusCategory = PNTimeoutCategory
+ } catch (ioException: IOException) {
+ pubnubException.pubnubError = PubNubError.PARSING_ERROR
+ pnStatusCategory = PNMalformedResponseCategory
+ } catch (ioException: IllegalStateException) {
+ pubnubException.pubnubError = PubNubError.PARSING_ERROR
+ pnStatusCategory = PNMalformedResponseCategory
+ } catch (throwable1: Throwable) {
+ pubnubException.pubnubError = PubNubError.HTTP_ERROR
+ pnStatusCategory = if (call.isCanceled) {
+ PNCancelledCategory
+ } else {
+ PNBadRequestCategory
+ }
+ }
+
+ callback.invoke(
+ null,
+ createStatusResponse(
+ category = pnStatusCategory,
+ exception = pubnubException
+ )
+ )
+ }
+ })
+ }
+
+ private fun storeRequestLatency(response: Response) {
+ pubnub.telemetryManager.storeLatency(
+ latency = with(response.raw()) {
+ receivedResponseAtMillis() - sentRequestAtMillis()
+ },
+ type = operationType()
+ )
+ }
+
+ private fun createBaseParams(): HashMap {
+ val map = hashMapOf()
+
+ map.putAll(queryParam)
+
+ map["pnsdk"] = "PubNub-Kotlin/${pubnub.version}"
+ map["uuid"] = pubnub.configuration.uuid
+
+ if (pubnub.configuration.includeInstanceIdentifier) {
+ map["instanceid"] = pubnub.instanceId
+ }
+
+ if (pubnub.configuration.includeRequestIdentifier) {
+ map["requestid"] = pubnub.requestId()
+ }
+
+ if (isAuthRequired() && pubnub.configuration.isAuthKeyValid()) {
+ map["auth"] = pubnub.configuration.authKey
+ }
+
+ map.putAll(pubnub.telemetryManager.operationsLatency())
+ return map
+ }
+
+ /**
+ * cancel the operation but do not alert anybody, useful for restarting the heartbeats and subscribe loops.
+ */
+ fun silentCancel() {
+ if (::call.isInitialized) {
+ if (!call.isCanceled) {
+ silenceFailures = true
+ call.cancel()
+ }
+ }
+ }
+
+
+ private fun createStatusResponse(
+ category: PNStatusCategory,
+ response: Response? = null,
+ exception: PubNubException? = null,
+ errorBody: JsonElement? = null
+ ): PNStatus {
+
+ val pnStatus = PNStatus(
+ category = category,
+ error = response == null || exception != null,
+ operation = operationType(),
+ exception = exception
+ )
+
+ pnStatus.executedEndpoint = this
+
+ response?.let {
+
+ with(pnStatus) {
+ statusCode = it.code()
+ tlsEnabled = it.raw().request().url().isHttps
+ origin = it.raw().request().url().host()
+ uuid = it.raw().request().url().queryParameter("uuid")
+ authKey = it.raw().request().url().queryParameter("auth")
+ clientRequest = it.raw().request()
+ }
+ }
+
+ val errorChannels = mutableListOf()
+ val errorGroups = mutableListOf()
+
+ if (errorBody != null) {
+ if (pubnub.mapper.isJsonObject(errorBody) && pubnub.mapper.hasField(errorBody, "payload")) {
+
+ val payloadBody = pubnub.mapper.getField(errorBody, "payload")!!
+
+ if (pubnub.mapper.hasField(payloadBody, "channels")) {
+ val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channels")
+ while (iterator.hasNext()) {
+ errorChannels.add(pubnub.mapper.elementToString(iterator.next())!!)
+ }
+ }
+
+ if (pubnub.mapper.hasField(payloadBody, "channel-groups")) {
+ val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channel-groups")
+ while (iterator.hasNext()) {
+ val node = iterator.next()
+
+ val channelGroupName = pubnub.mapper.elementToString(node)!!.let {
+ if (it.first().toString() == ":") {
+ it.substring(1)
+ } else {
+ it
+ }
+ }
+
+ errorGroups.add(channelGroupName)
+ }
+ }
+ }
+ }
+
+ pnStatus.affectedChannels =
+ if (errorChannels.isNotEmpty()) {
+ errorChannels
+ } else {
+ try {
+ getAffectedChannels()
+ } catch (e: UninitializedPropertyAccessException) {
+ emptyList()
+ }
+ }
+
+ pnStatus.affectedChannelGroups =
+ if (errorGroups.isNotEmpty()) {
+ errorGroups
+ } else {
+ try {
+ getAffectedChannelGroups()
+ } catch (e: UninitializedPropertyAccessException) {
+ emptyList()
+ }
+ }
+
+ return pnStatus
+ }
+
+ internal fun retry() {
+ silenceFailures = false
+ async(cachedCallback)
+ }
+
+ private fun extractErrorBody(response: Response): Pair {
+ val errorBodyString = try {
+ response.errorBody()?.string()
+ } catch (e: IOException) {
+ "N/A"
+ }
+
+ val errorBodyJson = try {
+ pubnub.mapper.fromJson(errorBodyString, JsonElement::class.java)
+ } catch (e: PubNubException) {
+ null
+ }
+
+ return errorBodyString to errorBodyJson
+ }
+
+ private fun checkAndCreateResponse(input: Response): Output? {
+ try {
+ return createResponse(input)
+ } catch (pubnubException: PubNubException) {
+ throw pubnubException.apply {
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ affectedCall = call
+ }
+ } catch (e: KotlinNullPointerException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: IllegalStateException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: IndexOutOfBoundsException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: NullPointerException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: IllegalArgumentException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: TypeCastException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: ClassCastException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ } catch (e: UninitializedPropertyAccessException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.toString()
+ affectedCall = call
+ statusCode = input.code()
+ jso = pubnub.mapper.toJson(input.body())
+ }
+ }
+ }
+
+ protected open fun getAffectedChannels() = emptyList()
+ protected open fun getAffectedChannelGroups(): List = emptyList()
+
+ protected open fun validateParams() {
+ if (isSubKeyRequired() && !pubnub.configuration.isSubscribeKeyValid()) {
+ throw PubNubException(PubNubError.SUBSCRIBE_KEY_MISSING)
+ }
+ if (isPubKeyRequired() && !pubnub.configuration.isPublishKeyValid()) {
+ throw PubNubException(PubNubError.PUBLISH_KEY_MISSING)
+ }
+ }
+
+ protected abstract fun doWork(queryParams: HashMap): Call
+ protected abstract fun createResponse(input: Response): Output?
+
+ protected abstract fun operationType(): PNOperationType
+
+ protected open fun isSubKeyRequired() = true
+ protected open fun isPubKeyRequired() = false
+ protected open fun isAuthRequired() = true
+}
+
diff --git a/src/main/kotlin/com/pubnub/api/PNConfiguration.kt b/src/main/kotlin/com/pubnub/api/PNConfiguration.kt
new file mode 100644
index 000000000..9c472cf93
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/PNConfiguration.kt
@@ -0,0 +1,99 @@
+package com.pubnub.api
+
+import com.pubnub.api.enums.PNHeartbeatNotificationOptions
+import com.pubnub.api.enums.PNLogVerbosity
+import com.pubnub.api.enums.PNReconnectionPolicy
+import okhttp3.Authenticator
+import okhttp3.CertificatePinner
+import okhttp3.ConnectionSpec
+import okhttp3.logging.HttpLoggingInterceptor
+import org.slf4j.LoggerFactory
+import java.net.Proxy
+import java.net.ProxySelector
+import java.util.*
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509ExtendedTrustManager
+
+class PNConfiguration {
+
+ private val log = LoggerFactory.getLogger("PNConfiguration")
+
+ private companion object Constants {
+ private const val DEFAULT_DEDUPE_SIZE = 100
+ private const val PRESENCE_TIMEOUT = 300
+ private const val MINIMUM_PRESENCE_TIMEOUT = 20
+ private const val NON_SUBSCRIBE_REQUEST_TIMEOUT = 10
+ private const val SUBSCRIBE_TIMEOUT = 310
+ private const val CONNECT_TIMEOUT = 5
+ }
+
+ lateinit var subscribeKey: String
+ lateinit var publishKey: String
+ lateinit var secretKey: String
+ lateinit var authKey: String
+ lateinit var cipherKey: String
+
+ var uuid: String = "pn-${UUID.randomUUID()}"
+
+ lateinit var origin: String
+ var secure = true
+
+ var logVerbosity = PNLogVerbosity.NONE
+ var heartbeatNotificationOptions = PNHeartbeatNotificationOptions.FAILURES
+ var reconnectionPolicy = PNReconnectionPolicy.NONE
+
+ var presenceTimeout = PRESENCE_TIMEOUT
+ set(value) {
+ field =
+ if (value < MINIMUM_PRESENCE_TIMEOUT) {
+ log.warn("Presence timeout is too low. Defaulting to: $MINIMUM_PRESENCE_TIMEOUT")
+ MINIMUM_PRESENCE_TIMEOUT
+ } else value
+ heartbeatInterval = (presenceTimeout / 2) - 1
+ }
+
+ var heartbeatInterval = 0
+
+ var subscribeTimeout = SUBSCRIBE_TIMEOUT
+ var connectTimeout = CONNECT_TIMEOUT
+ var nonSubscribeRequestTimeout =
+ NON_SUBSCRIBE_REQUEST_TIMEOUT
+ var maximumMessagesCacheSize = DEFAULT_DEDUPE_SIZE
+
+ var cacheBusting = false
+
+ var suppressLeaveEvents = false
+ var disableTokenManager = false
+ lateinit var filterExpression: String
+ var includeInstanceIdentifier = false
+ var includeRequestIdentifier = true
+ var maximumReconnectionRetries = -1
+ var maximumConnections: Int? = null
+ var requestMessageCountThreshold: Int? = null
+ var googleAppEngineNetworking = false
+ var startSubscriberThread = true
+ var dedupOnSubscribe = false
+
+ var proxy: Proxy? = null
+ var proxySelector: ProxySelector? = null
+ var proxyAuthenticator: Authenticator? = null
+ var certificatePinner: CertificatePinner? = null
+ var httpLoggingInterceptor: HttpLoggingInterceptor? = null
+ var sslSocketFactory: SSLSocketFactory? = null
+ var x509ExtendedTrustManager: X509ExtendedTrustManager? = null
+ var connectionSpec: ConnectionSpec? = null
+ var hostnameVerifier: HostnameVerifier? = null
+
+ internal fun isSubscribeKeyValid() = ::subscribeKey.isInitialized && !subscribeKey.isBlank()
+ internal fun isAuthKeyValid() = ::authKey.isInitialized && !authKey.isBlank()
+ internal fun isCipherKeyValid() = ::cipherKey.isInitialized && !cipherKey.isBlank()
+ internal fun isPublishKeyValid() = ::publishKey.isInitialized && !publishKey.isBlank()
+ internal fun isSecretKeyValid() = ::secretKey.isInitialized && !secretKey.isBlank()
+ internal fun isOriginValid() = ::origin.isInitialized && !origin.isBlank()
+ internal fun isFilterExpressionKeyValid(function: String.() -> Unit) {
+ if (::filterExpression.isInitialized && !filterExpression.isBlank()) {
+ function.invoke(filterExpression)
+ }
+ }
+}
diff --git a/src/main/kotlin/com/pubnub/api/PubNub.kt b/src/main/kotlin/com/pubnub/api/PubNub.kt
new file mode 100644
index 000000000..ebd25a80e
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/PubNub.kt
@@ -0,0 +1,168 @@
+package com.pubnub.api
+
+import com.pubnub.api.builder.PresenceBuilder
+import com.pubnub.api.builder.SubscribeBuilder
+import com.pubnub.api.builder.UnsubscribeBuilder
+import com.pubnub.api.callbacks.SubscribeCallback
+import com.pubnub.api.endpoints.*
+import com.pubnub.api.endpoints.access.Grant
+import com.pubnub.api.endpoints.channel_groups.*
+import com.pubnub.api.endpoints.message_actions.AddMessageAction
+import com.pubnub.api.endpoints.message_actions.GetMessageActions
+import com.pubnub.api.endpoints.message_actions.RemoveMessageAction
+import com.pubnub.api.endpoints.presence.GetState
+import com.pubnub.api.endpoints.presence.HereNow
+import com.pubnub.api.endpoints.presence.SetState
+import com.pubnub.api.endpoints.presence.WhereNow
+import com.pubnub.api.endpoints.pubsub.Publish
+import com.pubnub.api.endpoints.pubsub.Signal
+import com.pubnub.api.endpoints.push.AddChannelsToPush
+import com.pubnub.api.endpoints.push.ListPushProvisions
+import com.pubnub.api.endpoints.push.RemoveAllPushChannelsForDevice
+import com.pubnub.api.endpoints.push.RemoveChannelsFromPush
+import com.pubnub.api.managers.*
+import com.pubnub.api.vendor.Crypto
+import java.util.*
+
+class PubNub(val configuration: PNConfiguration) {
+
+ private companion object Constants {
+ private const val TIMESTAMP_DIVIDER = 1000
+ private const val SDK_VERSION = "4.0.0-dev"
+ private const val MAX_SEQUENCE = 65535;
+ }
+
+ private val basePathManager = BasePathManager(configuration)
+
+ val mapper = MapperManager()
+ internal val retrofitManager = RetrofitManager(this)
+ internal val publishSequenceManager = PublishSequenceManager(MAX_SEQUENCE)
+ internal val telemetryManager = TelemetryManager()
+ internal val subscriptionManager = SubscriptionManager(this)
+
+ val version = SDK_VERSION
+ val instanceId = UUID.randomUUID().toString()
+
+ fun baseUrl() = basePathManager.basePath()
+ fun requestId() = UUID.randomUUID().toString()
+ fun timestamp() = (Date().time / TIMESTAMP_DIVIDER).toInt()
+
+ fun publish() = Publish(this)
+ fun fire() = Publish(this).apply {
+ shouldStore = false
+ replicate = false
+ }
+
+ fun signal() = Signal(this)
+ fun subscribe() = SubscribeBuilder(subscriptionManager)
+ fun unsubscribe() = UnsubscribeBuilder(subscriptionManager)
+ fun presence() = PresenceBuilder(subscriptionManager)
+
+ fun addPushNotificationsOnChannels() = AddChannelsToPush(this)
+ fun removePushNotificationsFromChannels() =
+ RemoveChannelsFromPush(this)
+
+ fun removeAllPushNotificationsFromDeviceWithPushToken() =
+ RemoveAllPushChannelsForDevice(this)
+
+ fun auditPushChannelProvisions() = ListPushProvisions(this)
+
+ fun history() = History(this)
+ fun messageCounts() = MessageCounts(this)
+ fun fetchMessages() = FetchMessages(this)
+ fun deleteMessages() = DeleteMessages(this)
+ fun hereNow() = HereNow(this)
+ fun whereNow() = WhereNow(this)
+ fun setPresenceState() = SetState(this)
+ fun getPresenceState() = GetState(this)
+ fun time() = Time(this)
+ fun addMessageAction() = AddMessageAction(this)
+ fun getMessageActions() = GetMessageActions(this)
+ fun removeMessageAction() = RemoveMessageAction(this)
+
+ fun listAllChannelGroups() = ListAllChannelGroup(this)
+ fun listChannelsForChannelGroup() = AllChannelsChannelGroup(this)
+ fun addChannelsToChannelGroup() = AddChannelChannelGroup(this)
+ fun removeChannelsFromChannelGroup() = RemoveChannelChannelGroup(this)
+ fun deleteChannelGroup() = DeleteChannelGroup(this)
+
+ fun grant() = Grant(this)
+
+ fun addListener(listener: SubscribeCallback) {
+ subscriptionManager.addListener(listener)
+ }
+
+ fun removeListener(listener: SubscribeCallback) {
+ subscriptionManager.removeListener(listener)
+ }
+
+ fun getSubscribedChannels() = subscriptionManager.getSubscribedChannels()
+ fun getSubscribedChannelGroups() = subscriptionManager.getSubscribedChannelGroups()
+ fun unsubscribeAll() = subscriptionManager.unsubscribeAll()
+
+ /**
+ * Perform Cryptographic decryption of an input string using cipher key provided by PNConfiguration
+ *
+ * @param inputString String to be encrypted
+ * @return String containing the encryption of inputString using cipherKey
+ */
+ fun decrypt(inputString: String): String {
+ return decrypt(inputString, configuration.cipherKey)
+ }
+
+ /**
+ * Perform Cryptographic decryption of an input string using the cipher key
+ *
+ * @param inputString String to be encrypted
+ * @param cipherKey cipher key to be used for encryption
+ * @return String containing the encryption of inputString using cipherKey
+ * @throws PubNubException throws exception in case of failed encryption
+ */
+ fun decrypt(inputString: String, cipherKey: String): String {
+ return Crypto(cipherKey).decrypt(inputString)
+ }
+
+ /**
+ * Perform Cryptographic encryption of an input string and the cipher key provided by PNConfiguration
+ *
+ * @param inputString String to be encrypted
+ * @return String containing the encryption of inputString using cipherKey
+ */
+ fun encrypt(inputString: String): String {
+ return encrypt(inputString, configuration.cipherKey)
+ }
+
+ /**
+ * Perform Cryptographic encryption of an input string and the cipher key.
+ *
+ * @param inputString String to be encrypted
+ * @param cipherKey cipher key to be used for encryption
+ * @return String containing the encryption of inputString using cipherKey
+ * @throws PubNubException throws exception in case of failed encryption
+ */
+ @Throws(PubNubException::class)
+ fun encrypt(inputString: String, cipherKey: String): String {
+ return Crypto(cipherKey).encrypt(inputString)
+ }
+
+ fun reconnect() {
+ subscriptionManager.reconnect()
+ }
+
+ fun disconnect() {
+ subscriptionManager.disconnect()
+ }
+
+ fun destroy() {
+ subscriptionManager.destroy()
+ retrofitManager.destroy()
+ }
+
+ fun forceDestroy() {
+ subscriptionManager.destroy(true)
+ retrofitManager.destroy(true)
+ telemetryManager.stopCleanUpTimer()
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/PubNubError.kt b/src/main/kotlin/com/pubnub/api/PubNubError.kt
new file mode 100644
index 000000000..766607c9e
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/PubNubError.kt
@@ -0,0 +1,159 @@
+package com.pubnub.api
+
+enum class PubNubError(val code: Int, val message: String) {
+
+ TIMEOUT(
+ 100,
+ "Timeout Occurred"
+ ),
+
+ CONNECT_EXCEPTION(
+ 102,
+ "Connect Exception. Please verify if network is reachable"
+ ),
+
+ SECRET_KEY_MISSING(
+ 114,
+ "ULS configuration failed. Secret Key not configured"
+ ),
+
+ JSON_ERROR(
+ 121,
+ "JSON Error while processing API response"
+ ),
+
+ PARSING_ERROR(
+ 126,
+ "Parsing Error"
+ ),
+
+ CONNECTION_NOT_SET(
+ 133,
+ "PubNub Connection not set"
+ ),
+
+ GROUP_MISSING(
+ 136,
+ "Group Missing"
+ ),
+
+ SUBSCRIBE_KEY_MISSING(
+ 138,
+ "ULS configuration failed. Subscribe Key not configured."
+ ),
+
+ PUBLISH_KEY_MISSING(
+ 139,
+ "ULS configuration failed. Publish Key not configured."
+ ),
+
+ SUBSCRIBE_TIMEOUT(
+ 130,
+ "Subscribe Timeout"
+ ),
+
+ HTTP_ERROR(
+ 103,
+ "HTTP Error. Please check network connectivity. Please contact support with error details if the issue persists."
+ ),
+
+ MESSAGE_MISSING(
+ 142,
+ "Message Missing"
+ ),
+
+ CHANNEL_MISSING(
+ 132,
+ "Channel Missing"
+ ),
+
+ CRYPTO_ERROR(
+ 135,
+ "Error while encrypting/decrypting message. Please contact support with error details."
+ ),
+
+ STATE_MISSING(
+ 140,
+ "State Missing."
+ ),
+
+ CHANNEL_AND_GROUP_MISSING(
+ 141,
+ "Channel and Group Missing."
+ ),
+
+ PUSH_TYPE_MISSING(
+ 143,
+ "Push Type Missing."
+ ),
+
+ DEVICE_ID_MISSING(
+ 144,
+ "Device ID Missing"
+ ),
+
+ TIMETOKEN_MISSING(
+ 145,
+ "Timetoken Missing."
+ ),
+
+ CHANNELS_TIMETOKEN_MISMATCH(
+ 146,
+ "Channels and timetokens are not equal in size."
+ ),
+
+ USER_MISSING(
+ 147,
+ "User is missing"
+ ),
+
+ USER_ID_MISSING(
+ 148,
+ "User ID is missing"
+ ),
+
+ USER_NAME_MISSING(
+ 149,
+ "User name is missing"
+ ),
+
+ MESSAGE_ACTION_MISSING(
+ 158,
+ "Message action is missing."
+ ),
+
+ MESSAGE_ACTION_TYPE_MISSING(
+ 159,
+ "Message action type is missing."
+ ),
+
+ MESSAGE_ACTION_VALUE_MISSING(
+ 160,
+ "Message action value is missing."
+ ),
+
+ MESSAGE_TIMETOKEN_MISSING(
+ 161,
+ "Message timetoken is missing."
+ ),
+
+ MESSAGE_ACTION_TIMETOKEN_MISSING(
+ 162,
+ "Message action timetoken is missing."
+ ),
+
+ HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS(
+ 163,
+ "History can return message action data for a single channel only. Either pass a single channel or disable the includeMessageActions flag."
+ ),
+
+ PUSH_TOPIC_MISSING(
+ 164,
+ "Push notification topic is missing. Required only if push type is APNS2."
+ );
+
+ override fun toString(): String {
+ return "PubNubError(name=$name, code=$code, message='$message')"
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/PubNubException.kt b/src/main/kotlin/com/pubnub/api/PubNubException.kt
new file mode 100644
index 000000000..1bf62cf09
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/PubNubException.kt
@@ -0,0 +1,18 @@
+package com.pubnub.api
+
+import retrofit2.Call
+
+
+data class PubNubException(
+ var errorMessage: String? = null,
+ var pubnubError: PubNubError? = null,
+ var jso: String? = null,
+ var statusCode: Int = 0,
+ var affectedCall: Call<*>? = null
+) : Exception(errorMessage) {
+
+ constructor(pubnubError: PubNubError) : this(
+ errorMessage = pubnubError.message,
+ pubnubError = pubnubError
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/PubNubUtil.kt b/src/main/kotlin/com/pubnub/api/PubNubUtil.kt
new file mode 100644
index 000000000..b2ec58f84
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/PubNubUtil.kt
@@ -0,0 +1,213 @@
+package com.pubnub.api
+
+import com.pubnub.api.vendor.Base64
+import com.pubnub.api.vendor.Crypto
+import okhttp3.Request
+import okio.Buffer
+import org.slf4j.LoggerFactory
+import java.io.IOException
+import java.io.UnsupportedEncodingException
+import java.net.URLDecoder
+import java.net.URLEncoder
+import java.nio.charset.Charset
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.util.*
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+internal class PubNubUtil {
+
+ companion object A {
+
+ private val log = LoggerFactory.getLogger("PubNubUtil")
+
+ private const val CHARSET = "UTF-8"
+
+ fun replaceLast(string: String, toReplace: String, replacement: String): String {
+ val pos = string.lastIndexOf(toReplace)
+ return if (pos > -1) {
+ string.substring(0, pos) + replacement + string.substring(
+ pos + toReplace.length,
+ string.length
+ )
+ } else {
+ string
+ }
+ }
+
+ /**
+ * Returns decoded String
+ *
+ * @param stringToEncode , input string
+ * @return , decoded string
+ */
+ fun urlDecode(stringToEncode: String?): String? {
+ return try {
+ URLDecoder.decode(stringToEncode, CHARSET)
+ } catch (e: UnsupportedEncodingException) {
+ null
+ }
+ }
+
+ fun signRequest(
+ originalRequest: Request,
+ pnConfiguration: PNConfiguration,
+ timestamp: Int
+ ): Request {
+ // only sign if we have a secret key in place.
+ if (!pnConfiguration.isSecretKeyValid()) {
+ return originalRequest
+ }
+ val signature = generateSignature(pnConfiguration, originalRequest, timestamp)
+ val rebuiltUrl = originalRequest.url().newBuilder()
+ .addQueryParameter("timestamp", timestamp.toString())
+ .addQueryParameter("signature", signature)
+ .build()
+ return originalRequest.newBuilder().url(rebuiltUrl).build()
+ }
+
+ internal fun signSHA256(key: String, data: String): String {
+ val sha256HMAC: Mac
+ val hmacData: ByteArray
+ val secretKey = SecretKeySpec(key.toByteArray(charset(CHARSET)), "HmacSHA256")
+ sha256HMAC = try {
+ Mac.getInstance("HmacSHA256")
+ } catch (e: NoSuchAlgorithmException) {
+ throw Crypto.newCryptoError(0, e)
+ }
+ try {
+ sha256HMAC.init(secretKey)
+ } catch (e: InvalidKeyException) {
+ throw Crypto.newCryptoError(0, e)
+ }
+ hmacData = sha256HMAC.doFinal(data.toByteArray(charset(CHARSET)))
+ val signedd = String(Base64.encode(hmacData, 0), Charset.forName(CHARSET))
+ .replace('+', '-')
+ .replace('/', '_')
+ .replace("\n", "")
+ return signedd
+ }
+
+ private fun generateSignature(
+ configuration: PNConfiguration,
+ request: Request,
+ timestamp: Int
+ ): String? {
+ val isV2Signature: Boolean
+ val signatureBuilder = StringBuilder()
+ val requestURL = request.url().encodedPath()
+ val queryParams = mutableMapOf()
+ for (queryKey in request.url().queryParameterNames()) {
+ queryParams[queryKey] = request.url().queryParameter(queryKey)!!
+ // queryParams[queryKey] = request.url().encoded
+ }
+ queryParams["timestamp"] = timestamp.toString()
+
+ // todo AB testing
+ val classic = true
+ val encodedQueryString = if (classic) {
+ preparePamArguments(queryParams)
+ } else {
+ preparePamArguments("${request.url().encodedQuery()}×tamp=${timestamp}")
+ }
+
+ isV2Signature = !(requestURL.startsWith("/publish") && request.method().equals("post", ignoreCase = true))
+ if (!isV2Signature) {
+ signatureBuilder.append(configuration.subscribeKey).append("\n")
+ signatureBuilder.append(configuration.publishKey).append("\n")
+ signatureBuilder.append(requestURL).append("\n")
+ signatureBuilder.append(encodedQueryString)
+ } else {
+ signatureBuilder.append(request.method().toUpperCase()).append("\n")
+ signatureBuilder.append(configuration.publishKey).append("\n")
+ signatureBuilder.append(requestURL).append("\n")
+ signatureBuilder.append(encodedQueryString).append("\n")
+ signatureBuilder.append(PubNubUtil.requestBodyToString(request))
+ }
+
+ var signature = ""
+ try {
+ signature = signSHA256(configuration.secretKey, signatureBuilder.toString())
+ if (isV2Signature) {
+ signature = removeTrailingEqualSigns(signature)
+ signature = "v2.$signature"
+ }
+ } catch (e: PubNubException) {
+ log.warn("signature failed on SignatureInterceptor: $e")
+ } catch (e: UnsupportedEncodingException) {
+ log.warn("signature failed on SignatureInterceptor: $e")
+ }
+ return signature
+ }
+
+ fun removeTrailingEqualSigns(signature: String): String {
+ var cleanSignature = signature
+ while (cleanSignature[cleanSignature.length - 1] == '=') {
+ cleanSignature = cleanSignature.substring(0, cleanSignature.length - 1)
+ }
+ return cleanSignature
+ }
+
+ internal fun requestBodyToString(request: Request): String? {
+ if (request.body() == null) {
+ return ""
+ }
+ try {
+ val buffer = Buffer()
+ request.body()!!.writeTo(buffer)
+ return buffer.readUtf8()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ return ""
+ }
+
+ internal fun preparePamArguments(pamArgs: Map): String {
+ val pamKeys: Set = TreeSet(pamArgs.keys)
+ var stringifiedArguments = ""
+ var i = 0
+ for (pamKey in pamKeys) {
+ if (i != 0) {
+ stringifiedArguments = "$stringifiedArguments&"
+ }
+ stringifiedArguments = stringifiedArguments + pamKey + "=" + pamEncode(pamArgs[pamKey]!!)
+ i += 1
+ }
+ return stringifiedArguments
+ }
+
+ private fun preparePamArguments(encodedQueryString: String): String {
+ return encodedQueryString.split("&")
+ .toSortedSet()
+ .map { pamEncode(it, true) }
+ .joinToString("&")
+ }
+
+ /**
+ * Returns encoded String
+ *
+ * @param stringToEncode , input string
+ * @return , encoded string
+ */
+ internal fun pamEncode(stringToEncode: String, alreadyPercentEncoded: Boolean = false): String {
+ /* !'()*~ */
+
+ return if (alreadyPercentEncoded) {
+ stringToEncode
+ } else {
+ URLEncoder.encode(stringToEncode, "UTF-8")
+ .replace("+", "%20")
+ }.run {
+ replace("*", "%2A")
+ }
+ }
+
+ }
+}
+
+internal fun List.toCsv(): String {
+ if (this.isNotEmpty())
+ return this.joinToString(",")
+ return ","
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt b/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt
new file mode 100644
index 000000000..5a3b5ab23
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt
@@ -0,0 +1,58 @@
+package com.pubnub.api.builder
+
+import com.pubnub.api.managers.SubscriptionManager
+
+abstract class PubSubBuilder(
+ protected val subscriptionManager: SubscriptionManager,
+ var channels: List = emptyList(),
+ var channelGroups: List = emptyList()
+) {
+ abstract fun execute()
+}
+
+class PresenceBuilder(
+ subscriptionManager: SubscriptionManager,
+ channels: List = emptyList(),
+ channelGroups: List = emptyList(),
+ var connected: Boolean = false
+) : PubSubBuilder(subscriptionManager, channels, channelGroups) {
+
+ override fun execute() {
+ val presenceOperation = PresenceOperation().apply {
+ connected = this@PresenceBuilder.connected
+ channels = this@PresenceBuilder.channels
+ channelGroups = this@PresenceBuilder.channelGroups
+ }
+ subscriptionManager.adaptPresenceBuilder(presenceOperation)
+ }
+}
+
+class SubscribeBuilder(
+ subscriptionManager: SubscriptionManager,
+ var withPresence: Boolean = false,
+ var withTimetoken: Long = 0L
+) : PubSubBuilder(subscriptionManager) {
+
+ override fun execute() {
+ val subscribeOperation = SubscribeOperation().apply {
+ channels = this@SubscribeBuilder.channels
+ channelGroups = this@SubscribeBuilder.channelGroups
+ presenceEnabled = this@SubscribeBuilder.withPresence
+ timetoken = this@SubscribeBuilder.withTimetoken
+ }
+ this.subscriptionManager.adaptSubscribeBuilder(subscribeOperation)
+ }
+}
+
+class UnsubscribeBuilder(
+ subscriptionManager: SubscriptionManager
+) : PubSubBuilder(subscriptionManager) {
+
+ override fun execute() {
+ val unsubscribeOperation = UnsubscribeOperation().apply {
+ channels = this@UnsubscribeBuilder.channels
+ channelGroups = this@UnsubscribeBuilder.channelGroups
+ }
+ this.subscriptionManager.adaptUnsubscribeBuilder(unsubscribeOperation)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt b/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt
new file mode 100644
index 000000000..23259dfbc
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt
@@ -0,0 +1,25 @@
+package com.pubnub.api.builder
+
+// basic publish/subscribe class
+
+internal abstract class PubSubOperation(
+ internal var channels: List = emptyList(),
+ internal var channelGroups: List = emptyList()
+)
+
+// concrete publish/subscribe cases
+
+internal class SubscribeOperation(
+ internal var presenceEnabled: Boolean = false,
+ internal var timetoken: Long = 0L
+) : PubSubOperation()
+
+internal class UnsubscribeOperation : PubSubOperation()
+
+internal class PresenceOperation(
+ internal var connected: Boolean = false
+) : PubSubOperation()
+
+internal class StateOperation(
+ var state: Any? = null
+) : PubSubOperation()
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt b/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt
new file mode 100644
index 000000000..79f01bfda
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt
@@ -0,0 +1,6 @@
+package com.pubnub.api.callbacks
+
+internal abstract class ReconnectionCallback {
+ abstract fun onReconnection()
+ abstract fun onMaxReconnectionExhaustion()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt b/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt
new file mode 100644
index 000000000..0687d12b0
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt
@@ -0,0 +1,20 @@
+package com.pubnub.api.callbacks
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.models.consumer.PNStatus
+import com.pubnub.api.models.consumer.pubsub.PNMessageResult
+import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult
+import com.pubnub.api.models.consumer.pubsub.PNSignalResult
+import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult
+
+abstract class SubscribeCallback {
+ abstract fun status(pubnub: PubNub, pnStatus: PNStatus)
+
+ open fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {}
+
+ open fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {}
+
+ open fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) {}
+
+ open fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) {}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt b/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt
new file mode 100644
index 000000000..10cb19bdc
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt
@@ -0,0 +1,43 @@
+package com.pubnub.api.endpoints
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.history.PNDeleteMessagesResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class DeleteMessages(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channels: List
+ var start: Long? = null
+ var end: Long? = null
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ start?.let {
+ queryParams["start"] = it.toString().toLowerCase(Locale.US)
+ }
+ end?.let {
+ queryParams["end"] = it.toString().toLowerCase(Locale.US)
+ }
+
+ return pubnub.retrofitManager.historyService.deleteMessages(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNDeleteMessagesResult? {
+ return PNDeleteMessagesResult()
+ }
+
+ override fun operationType() = PNOperationType.PNDeleteMessagesOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt b/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt
new file mode 100644
index 000000000..e9961e539
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt
@@ -0,0 +1,139 @@
+package com.pubnub.api.endpoints
+
+import com.google.gson.JsonElement
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.history.PNFetchMessageItem
+import com.pubnub.api.models.consumer.history.PNFetchMessagesResult
+import com.pubnub.api.models.server.FetchMessagesEnvelope
+import com.pubnub.api.vendor.Crypto
+import org.slf4j.LoggerFactory
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class FetchMessages(pubnub: PubNub) : Endpoint(pubnub) {
+
+ private val log = LoggerFactory.getLogger("FetchMessages")
+
+ private companion object {
+ private const val DEFAULT_MESSAGES = 1
+ private const val MAX_MESSAGES = 25
+ }
+
+ lateinit var channels: List
+ var maximumPerChannel = 0
+ var start: Long? = null
+ var end: Long? = null
+ var includeMeta = false
+ var includeMessageActions = false
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+
+ if (!includeMessageActions) {
+ if (maximumPerChannel !in DEFAULT_MESSAGES..MAX_MESSAGES) {
+ when {
+ maximumPerChannel < DEFAULT_MESSAGES -> maximumPerChannel = DEFAULT_MESSAGES
+ maximumPerChannel > MAX_MESSAGES -> maximumPerChannel = MAX_MESSAGES
+ }
+ log.info("maximumPerChannel param defaulting to $maximumPerChannel")
+ }
+ } else {
+ if (maximumPerChannel !in DEFAULT_MESSAGES..MAX_MESSAGES) {
+ maximumPerChannel = MAX_MESSAGES
+ log.info("maximumPerChannel param defaulting to $maximumPerChannel")
+ }
+ }
+ }
+
+ override fun getAffectedChannels() = channels
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["max"] = maximumPerChannel.toString()
+
+ start?.let {
+ queryParams["start"] = it.toString().toLowerCase(Locale.US)
+ }
+ end?.let {
+ queryParams["end"] = it.toString().toLowerCase(Locale.US)
+ }
+
+ if (includeMeta) {
+ queryParams["include_meta"] = includeMeta.toString()
+ }
+
+ return if (!includeMessageActions) {
+ pubnub.retrofitManager.historyService.fetchMessages(
+ subKey = pubnub.configuration.subscribeKey,
+ channels = channels.toCsv(),
+ options = queryParams
+ )
+ } else {
+ if (channels.size > 1) {
+ throw PubNubException(PubNubError.HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS)
+ }
+ pubnub.retrofitManager.historyService.fetchMessagesWithActions(
+ subKey = pubnub.configuration.subscribeKey,
+ channel = channels.first(),
+ options = queryParams
+ )
+ }
+
+ }
+
+ override fun createResponse(input: Response): PNFetchMessagesResult? {
+ val channelsMap = hashMapOf>()
+
+ for (entry in input.body()!!.channels) {
+ val items = mutableListOf()
+ for (item in entry.value) {
+ items.add(
+ PNFetchMessageItem(
+ message = processMessage(item.message),
+ timetoken = item.timetoken,
+ meta = item.meta
+ ).apply {
+ if (includeMessageActions) {
+ actions = (item.actions) ?: mapOf()
+ }
+ }
+ )
+ }
+ channelsMap[entry.key] = items
+ }
+
+ return PNFetchMessagesResult(channelsMap)
+ }
+
+ private fun processMessage(message: JsonElement): JsonElement {
+ if (!pubnub.configuration.isCipherKeyValid())
+ return message
+
+ val crypto = Crypto(pubnub.configuration.cipherKey)
+
+ val inputText =
+ if (pubnub.mapper.isJsonObject(message) && pubnub.mapper.hasField(message, "pn_other")) {
+ pubnub.mapper.elementToString(message, "pn_other")
+ } else {
+ pubnub.mapper.elementToString(message)
+ }
+
+ val outputText = crypto.decrypt(inputText!!)
+
+ var outputObject = pubnub.mapper.fromJson(outputText, JsonElement::class.java)
+
+ pubnub.mapper.getField(message, "pn_other")?.let {
+ val objectNode = pubnub.mapper.getAsObject(message)
+ pubnub.mapper.putOnObject(objectNode, "pn_other", outputObject)
+ outputObject = objectNode
+ }
+
+ return outputObject
+ }
+
+ override fun operationType() = PNOperationType.PNFetchMessagesOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/History.kt b/src/main/kotlin/com/pubnub/api/endpoints/History.kt
new file mode 100644
index 000000000..dba53aff7
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/History.kt
@@ -0,0 +1,142 @@
+package com.pubnub.api.endpoints
+
+import com.google.gson.JsonElement
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.history.PNHistoryItemResult
+import com.pubnub.api.models.consumer.history.PNHistoryResult
+import com.pubnub.api.vendor.Crypto
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class History(pubnub: PubNub) : Endpoint(pubnub) {
+
+ private companion object {
+ private const val MAX_COUNT = 100
+ }
+
+ lateinit var channel: String
+ var start: Long? = null
+ var end: Long? = null
+ var count = MAX_COUNT
+ var reverse = false
+ var includeTimetoken = false
+ var includeMeta = false
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channel.isInitialized || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ count =
+ when (count) {
+ in 1..MAX_COUNT -> count
+ else -> MAX_COUNT
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["reverse"] = reverse.toString()
+ queryParams["include_token"] = includeTimetoken.toString()
+ queryParams["include_meta"] = includeMeta.toString()
+ queryParams["count"] = count.toString()
+
+ start?.let {
+ queryParams["start"] = it.toString().toLowerCase(Locale.US)
+ }
+ end?.let {
+ queryParams["end"] = it.toString().toLowerCase(Locale.US)
+ }
+
+ return pubnub.retrofitManager.historyService.fetchHistory(
+ pubnub.configuration.subscribeKey,
+ channel,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNHistoryResult? {
+ val startTimeToken = pubnub.mapper.elementToLong(pubnub.mapper.getArrayElement(input.body()!!, 1))
+ val endTimeToken = pubnub.mapper.elementToLong(pubnub.mapper.getArrayElement(input.body()!!, 2))
+
+ val messages = mutableListOf()
+
+ var historyData = PNHistoryResult(
+ messages = messages,
+ startTimetoken = startTimeToken,
+ endTimetoken = endTimeToken
+ )
+
+ if (pubnub.mapper.getArrayElement(input.body()!!, 0).isJsonArray) {
+ val iterator = pubnub.mapper.getArrayIterator(pubnub.mapper.getArrayElement(input.body()!!, 0))!!
+ while (iterator.hasNext()) {
+
+ val historyEntry = iterator.next()
+
+ var message: JsonElement
+ var timetoken: Long? = null
+ var meta: JsonElement? = null
+
+ if (includeTimetoken || includeMeta) {
+ message = processMessage(pubnub.mapper.getField(historyEntry, "message")!!)
+ if (includeTimetoken) {
+ timetoken = pubnub.mapper.elementToLong(historyEntry, "timetoken")
+ }
+ if (includeMeta) {
+ meta = pubnub.mapper.getField(historyEntry, "meta")
+ }
+ } else {
+ message = processMessage(historyEntry)
+ }
+
+ val historyItem = PNHistoryItemResult(
+ entry = message,
+ timetoken = timetoken,
+ meta = meta
+ )
+
+ messages.add(historyItem)
+ }
+ } else {
+ throw PubNubException(PubNubError.HTTP_ERROR).apply {
+ errorMessage = "History is disabled"
+ }
+ }
+
+ return historyData
+ }
+
+ private fun processMessage(message: JsonElement): JsonElement {
+ if (!pubnub.configuration.isCipherKeyValid())
+ return message
+
+ val crypto = Crypto(pubnub.configuration.cipherKey)
+
+ val inputText =
+ if (pubnub.mapper.isJsonObject(message) && pubnub.mapper.hasField(message, "pn_other")) {
+ pubnub.mapper.elementToString(message, "pn_other")
+ } else {
+ pubnub.mapper.elementToString(message)
+ }
+
+ val outputText = crypto.decrypt(inputText!!)
+
+ var outputObject = pubnub.mapper.fromJson(outputText, JsonElement::class.java)
+
+ pubnub.mapper.getField(message, "pn_other")?.let {
+ val objectNode = pubnub.mapper.getAsObject(message)
+ pubnub.mapper.putOnObject(objectNode, "pn_other", outputObject)
+ outputObject = objectNode
+ }
+
+ return outputObject
+ }
+
+ override fun operationType() = PNOperationType.PNHistoryOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt b/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt
new file mode 100644
index 000000000..9d92672f1
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt
@@ -0,0 +1,57 @@
+package com.pubnub.api.endpoints
+
+import com.google.gson.JsonElement
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.history.PNMessageCountResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class MessageCounts(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channels: List
+ lateinit var channelsTimetoken: List
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (!::channelsTimetoken.isInitialized || channelsTimetoken.isEmpty()) {
+ throw PubNubException(PubNubError.TIMETOKEN_MISSING)
+ }
+ if (channelsTimetoken.size != channels.size && channelsTimetoken.size > 1) {
+ throw PubNubException(PubNubError.CHANNELS_TIMETOKEN_MISMATCH)
+ }
+ }
+
+ override fun getAffectedChannels() = channels
+
+ override fun doWork(queryParams: HashMap): Call {
+ if (channelsTimetoken.size == 1) {
+ queryParams["timetoken"] = channelsTimetoken.toCsv()
+ } else {
+ queryParams["channelsTimetoken"] = channelsTimetoken.toCsv()
+ }
+
+ return pubnub.retrofitManager.historyService.fetchCount(
+ subKey = pubnub.configuration.subscribeKey,
+ channels = channels.toCsv(),
+ options = queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNMessageCountResult? {
+ val channelsMap = HashMap()
+
+ val it = pubnub.mapper.getObjectIterator(input.body()!!, "channels")
+ while (it.hasNext()) {
+ val entry = it.next()
+ channelsMap[entry.key] = entry.value.asLong
+ }
+ return PNMessageCountResult(channelsMap)
+ }
+
+ override fun operationType() = PNOperationType.PNMessageCountOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/Time.kt b/src/main/kotlin/com/pubnub/api/endpoints/Time.kt
new file mode 100644
index 000000000..9d3886d0d
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/Time.kt
@@ -0,0 +1,27 @@
+package com.pubnub.api.endpoints
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.PNTimeResult
+import retrofit2.Response
+
+class Time(pubnub: PubNub) : Endpoint, PNTimeResult>(pubnub) {
+
+ override fun getAffectedChannels() = emptyList()
+
+ override fun getAffectedChannelGroups() = emptyList()
+
+ override fun doWork(queryParams: HashMap) =
+ pubnub.retrofitManager.timeService.fetchTime(queryParams)
+
+ override fun createResponse(input: Response>): PNTimeResult? {
+ return PNTimeResult(input.body()!![0])
+ }
+
+ override fun operationType() = PNOperationType.PNTimeOperation
+
+ override fun isAuthRequired() = false
+ override fun isSubKeyRequired() = false
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt b/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt
new file mode 100644
index 000000000..316feb54e
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt
@@ -0,0 +1,117 @@
+package com.pubnub.api.endpoints.access
+
+import com.google.gson.JsonElement
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.access_manager.PNAccessManagerGrantResult
+import com.pubnub.api.models.consumer.access_manager.PNAccessManagerKeyData
+import com.pubnub.api.models.server.Envelope
+import com.pubnub.api.models.server.access_manager.AccessManagerGrantPayload
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class Grant(pubnub: PubNub) : Endpoint, PNAccessManagerGrantResult>(pubnub) {
+
+ var read = false
+ var write = false
+ var manage = false
+ var delete = false
+ var ttl: Int = -1
+
+ var authKeys = emptyList()
+ var channels = emptyList()
+ var channelGroups = emptyList()
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!pubnub.configuration.isSecretKeyValid()) {
+ throw PubNubException(PubNubError.SECRET_KEY_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun doWork(queryParams: HashMap): Call> {
+ channels.run {
+ if (isNotEmpty()) queryParams["channel"] = toCsv()
+ }
+ channelGroups.run {
+ if (isNotEmpty()) queryParams["channel-group"] = toCsv()
+ }
+ authKeys.run {
+ if (isNotEmpty()) queryParams["auth"] = toCsv()
+ }
+
+ if (ttl >= -1) {
+ queryParams["ttl"] = ttl.toString()
+ }
+
+ queryParams["r"] = if (read) "1" else "0"
+ queryParams["w"] = if (write) "1" else "0"
+ queryParams["m"] = if (manage) "1" else "0"
+ queryParams["d"] = if (delete) "1" else "0"
+
+ return pubnub.retrofitManager.accessManagerService
+ .grant(
+ subKey = pubnub.configuration.subscribeKey,
+ options = queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNAccessManagerGrantResult? {
+ val data = input.body()!!.payload!!
+
+ val constructedChannels = mutableMapOf?>()
+ val constructedGroups = mutableMapOf?>()
+
+ // we have a case of a singular channel.
+ data.channel?.let {
+ constructedChannels[it] = data.authKeys!!
+ }
+
+ if (channelGroups.size == 1) {
+ constructedGroups[pubnub.mapper.elementToString(data.channelGroups)!!] = data.authKeys!!
+ } else if (channelGroups.size > 1) {
+ val it = pubnub.mapper.getObjectIterator(data.channelGroups!!)
+ while (it.hasNext()) {
+ val (k, v) = it.next()
+ constructedGroups[k] = createKeyMap(v)
+ }
+ }
+
+ data.channels?.forEach {
+ constructedChannels[it.key] = data.channels[it.key]!!.authKeys
+ }
+
+ return PNAccessManagerGrantResult(
+ level = data.level!!,
+ ttl = data.ttl,
+ subscribeKey = data.subscribeKey!!,
+ channels = constructedChannels,
+ channelGroups = constructedGroups
+ )
+ }
+
+ private fun createKeyMap(input: JsonElement): Map {
+ val result: MutableMap =
+ HashMap()
+ val it: Iterator> =
+ pubnub.mapper.getObjectIterator(input, "auths")
+ while (it.hasNext()) {
+ val keyMap = it.next()
+ val pnAccessManagerKeyData = PNAccessManagerKeyData()
+ pnAccessManagerKeyData.manageEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "m"))
+ pnAccessManagerKeyData.writeEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "w"))
+ pnAccessManagerKeyData.readEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "r"))
+ pnAccessManagerKeyData.deleteEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "d"))
+ result[keyMap.key] = pnAccessManagerKeyData
+ }
+ return result
+ }
+
+ override fun operationType() = PNOperationType.PNAccessManagerGrant
+
+ override fun isAuthRequired() = false
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt
new file mode 100644
index 000000000..d6945b0b7
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt
@@ -0,0 +1,46 @@
+package com.pubnub.api.endpoints.channel_groups
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAddChannelResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class AddChannelChannelGroup(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channelGroup: String
+ lateinit var channels: List
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = listOf(channelGroup)
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channelGroup.isInitialized || channelGroup.isBlank()) {
+ throw PubNubException(PubNubError.GROUP_MISSING)
+ }
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ if (channels.isNotEmpty()) {
+ queryParams["add"] = channels.toCsv()
+ }
+
+ return pubnub.retrofitManager.channelGroupService
+ .addChannelChannelGroup(
+ pubnub.configuration.subscribeKey,
+ channelGroup,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNChannelGroupsAddChannelResult? {
+ return PNChannelGroupsAddChannelResult()
+ }
+
+ override fun operationType() = PNOperationType.PNAddChannelsToGroupOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt
new file mode 100644
index 000000000..e68448f52
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt
@@ -0,0 +1,44 @@
+package com.pubnub.api.endpoints.channel_groups
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAllChannelsResult
+import com.pubnub.api.models.server.Envelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class AllChannelsChannelGroup(pubnub: PubNub) :
+ Endpoint>, PNChannelGroupsAllChannelsResult>(pubnub) {
+
+ lateinit var channelGroup: String
+
+ override fun getAffectedChannelGroups() = listOf(channelGroup)
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channelGroup.isInitialized || channelGroup.isBlank()) {
+ throw PubNubException(PubNubError.GROUP_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call>> {
+ return pubnub.retrofitManager.channelGroupService
+ .allChannelsChannelGroup(
+ pubnub.configuration.subscribeKey,
+ channelGroup,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>>): PNChannelGroupsAllChannelsResult? {
+ return PNChannelGroupsAllChannelsResult(
+ input.body()!!.payload!!["channels"] as List
+ )
+ }
+
+ override fun operationType() = PNOperationType.PNChannelsForGroupOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt
new file mode 100644
index 000000000..f5203f33c
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt
@@ -0,0 +1,40 @@
+package com.pubnub.api.endpoints.channel_groups
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsDeleteGroupResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class DeleteChannelGroup(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channelGroup: String
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channelGroup.isInitialized || channelGroup.isBlank()) {
+ throw PubNubException(PubNubError.GROUP_MISSING)
+ }
+ }
+
+ override fun getAffectedChannelGroups() = listOf(channelGroup)
+
+ override fun doWork(queryParams: HashMap): Call {
+ return pubnub.retrofitManager.channelGroupService
+ .deleteChannelGroup(
+ pubnub.configuration.subscribeKey,
+ channelGroup,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNChannelGroupsDeleteGroupResult? {
+ return PNChannelGroupsDeleteGroupResult()
+ }
+
+ override fun operationType() = PNOperationType.PNRemoveGroupOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt
new file mode 100644
index 000000000..f993a7366
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt
@@ -0,0 +1,29 @@
+package com.pubnub.api.endpoints.channel_groups
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsListAllResult
+import com.pubnub.api.models.server.Envelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class ListAllChannelGroup(pubnub: PubNub) : Endpoint>, PNChannelGroupsListAllResult>(pubnub) {
+
+ override fun doWork(queryParams: HashMap): Call>> {
+ return pubnub.retrofitManager.channelGroupService
+ .listAllChannelGroup(
+ pubnub.configuration.subscribeKey,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>>): PNChannelGroupsListAllResult? {
+ return PNChannelGroupsListAllResult(
+ input.body()!!.payload!!["groups"] as List
+ )
+ }
+
+ override fun operationType() = PNOperationType.PNChannelGroupsOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt
new file mode 100644
index 000000000..494a488da
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt
@@ -0,0 +1,47 @@
+package com.pubnub.api.endpoints.channel_groups
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsRemoveChannelResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class RemoveChannelChannelGroup(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channelGroup: String
+ lateinit var channels: List
+
+ override fun getAffectedChannels() = channels
+
+ override fun getAffectedChannelGroups() = listOf(channelGroup)
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channelGroup.isInitialized || channelGroup.isBlank()) {
+ throw PubNubException(PubNubError.GROUP_MISSING)
+ }
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ if (channels.isNotEmpty()) {
+ queryParams["remove"] = channels.toCsv()
+ }
+
+ return pubnub.retrofitManager.channelGroupService
+ .removeChannel(
+ pubnub.configuration.subscribeKey,
+ channelGroup,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNChannelGroupsRemoveChannelResult? {
+ return PNChannelGroupsRemoveChannelResult()
+ }
+
+ override fun operationType() = PNOperationType.PNRemoveChannelsFromGroupOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt
new file mode 100644
index 000000000..876e78e8a
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt
@@ -0,0 +1,59 @@
+package com.pubnub.api.endpoints.message_actions
+
+import com.google.gson.JsonObject
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.message_actions.PNAddMessageActionResult
+import com.pubnub.api.models.consumer.message_actions.PNMessageAction
+import com.pubnub.api.models.server.objects_api.EntityEnvelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class AddMessageAction(pubnub: PubNub) : Endpoint, PNAddMessageActionResult>(pubnub) {
+
+ lateinit var channel: String
+ lateinit var messageAction: PNMessageAction
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channel.isInitialized || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (!::messageAction.isInitialized) {
+ throw PubNubException(PubNubError.MESSAGE_ACTION_MISSING)
+ }
+ if (messageAction.type.isBlank()) {
+ throw PubNubException(PubNubError.MESSAGE_ACTION_TYPE_MISSING)
+ }
+ if (messageAction.value.isBlank()) {
+ throw PubNubException(PubNubError.MESSAGE_ACTION_VALUE_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call> {
+ val body = JsonObject()
+ body.addProperty("type", messageAction.type)
+ body.addProperty("value", messageAction.value)
+
+ return pubnub.retrofitManager.messageActionService
+ .addMessageAction(
+ subKey = pubnub.configuration.subscribeKey,
+ channel = channel,
+ messageTimetoken = messageAction.messageTimetoken.toString().toLowerCase(),
+ body = body,
+ options = queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNAddMessageActionResult? {
+ return PNAddMessageActionResult(input.body()!!.data!!)
+ }
+
+ override fun operationType() = PNOperationType.PNAddMessageAction
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt
new file mode 100644
index 000000000..713f678e6
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt
@@ -0,0 +1,51 @@
+package com.pubnub.api.endpoints.message_actions
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.message_actions.PNGetMessageActionsResult
+import com.pubnub.api.models.consumer.message_actions.PNMessageAction
+import com.pubnub.api.models.server.objects_api.EntityEnvelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class GetMessageActions(pubnub: PubNub) :
+ Endpoint>, PNGetMessageActionsResult>(pubnub) {
+
+ lateinit var channel: String
+ var start: Long? = null
+ var end: Long? = null
+ var limit: Int? = null
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channel.isInitialized || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call>> {
+
+ start?.let { queryParams["start"] = it.toString().toLowerCase() }
+ end?.let { queryParams["end"] = it.toString().toLowerCase() }
+ limit?.let { queryParams["limit"] = it.toString().toLowerCase() }
+
+ return pubnub.retrofitManager.messageActionService
+ .getMessageActions(
+ pubnub.configuration.subscribeKey,
+ channel,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>>): PNGetMessageActionsResult? {
+ return PNGetMessageActionsResult(input.body()!!.data!!)
+ }
+
+ override fun operationType() = PNOperationType.PNGetMessageActions
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt
new file mode 100644
index 000000000..98fb42ce3
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt
@@ -0,0 +1,50 @@
+package com.pubnub.api.endpoints.message_actions
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class RemoveMessageAction(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var channel: String
+ var messageTimetoken: Long? = null
+ var actionTimetoken: Long? = null
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::channel.isInitialized || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (messageTimetoken == null) {
+ throw PubNubException(PubNubError.MESSAGE_TIMETOKEN_MISSING)
+ }
+ if (actionTimetoken == null) {
+ throw PubNubException(PubNubError.MESSAGE_ACTION_TIMETOKEN_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call {
+ return pubnub.retrofitManager.messageActionService
+ .deleteMessageAction(
+ subKey = pubnub.configuration.subscribeKey,
+ channel = channel,
+ messageTimetoken = messageTimetoken.toString().toLowerCase(),
+ actionTimetoken = actionTimetoken.toString().toLowerCase(),
+ options = queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): PNRemoveMessageActionResult? {
+ return PNRemoveMessageActionResult()
+ }
+
+ override fun operationType() = PNOperationType.PNDeleteMessageAction
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt
new file mode 100644
index 000000000..bf00f10e1
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt
@@ -0,0 +1,61 @@
+package com.pubnub.api.endpoints.presence
+
+import com.google.gson.JsonElement
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.presence.PNGetStateResult
+import com.pubnub.api.models.server.Envelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class GetState(pubnub: PubNub) : Endpoint, PNGetStateResult>(pubnub) {
+
+ var channels = listOf()
+ var channelGroups = listOf()
+ var uuid = pubnub.configuration.uuid
+
+ override fun getAffectedChannels() = channels
+
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun validateParams() {
+ super.validateParams()
+ if (channels.isNullOrEmpty() && channelGroups.isNullOrEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call> {
+ if (channelGroups.isNotEmpty()) {
+ queryParams["channel-group"] = channelGroups.toCsv()
+ }
+
+ return pubnub.retrofitManager.presenceService.getState(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ uuid,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNGetStateResult? {
+ val stateMappings = hashMapOf()
+ if (channels.size == 1 && channelGroups.isEmpty()) {
+ stateMappings[channels.first()] = input.body()!!.payload!!
+ } else {
+ val it = pubnub.mapper.getObjectIterator(input.body()!!.payload!!)
+ while (it.hasNext()) {
+ val stateMapping = it.next()
+ stateMappings[stateMapping.key] = stateMapping.value
+ }
+ }
+
+ return PNGetStateResult(stateMappings)
+ }
+
+ override fun operationType() = PNOperationType.PNGetState
+}
+
+
+
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt
new file mode 100644
index 000000000..0e365196a
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt
@@ -0,0 +1,54 @@
+package com.pubnub.api.endpoints.presence
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class Heartbeat internal constructor(
+ pubnub: PubNub,
+ val channels: List = listOf(),
+ val channelGroups: List = listOf()
+) : Endpoint(pubnub) {
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun validateParams() {
+ super.validateParams()
+ if (channels.isEmpty() && channelGroups.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["heartbeat"] = pubnub.configuration.presenceTimeout.toString()
+
+ if (channelGroups.isNotEmpty()) {
+ queryParams["channel-group"] = channelGroups.joinToString(",")
+ }
+
+ val channelsCsv =
+ if (channels.isNotEmpty())
+ channels.joinToString(",")
+ else
+ ","
+
+ return pubnub.retrofitManager.presenceService.heartbeat(
+ pubnub.configuration.subscribeKey,
+ channelsCsv,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): Boolean? {
+ return true
+ }
+
+ override fun operationType() = PNOperationType.PNHeartbeatOperation
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt
new file mode 100644
index 000000000..6c5d9b6e5
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt
@@ -0,0 +1,131 @@
+package com.pubnub.api.endpoints.presence
+
+import com.google.gson.JsonElement
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.presence.PNHereNowChannelData
+import com.pubnub.api.models.consumer.presence.PNHereNowOccupantData
+import com.pubnub.api.models.consumer.presence.PNHereNowResult
+import com.pubnub.api.models.server.Envelope
+import com.pubnub.api.toCsv
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class HereNow(pubnub: PubNub) : Endpoint, PNHereNowResult>(pubnub) {
+
+ var channels = listOf()
+ var channelGroups = listOf()
+ var includeState = false
+ var includeUUIDs = true
+
+ private fun isGlobalHereNow() = channels.isEmpty() && channelGroups.isEmpty()
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun doWork(queryParams: HashMap): Call> {
+ if (includeState) {
+ queryParams["state"] = "1"
+ }
+ if (!includeUUIDs) {
+ queryParams["disable_uuids"] = "1"
+ }
+ if (channelGroups.isNotEmpty()) {
+ queryParams["channel-group"] = channelGroups.toCsv()
+ }
+
+ return if (!isGlobalHereNow()) {
+ pubnub.retrofitManager.presenceService.hereNow(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ queryParams
+ )
+ } else {
+ pubnub.retrofitManager.presenceService.globalHereNow(
+ pubnub.configuration.subscribeKey,
+ queryParams
+ )
+ }
+ }
+
+ override fun createResponse(input: Response>): PNHereNowResult? {
+ return if (isGlobalHereNow()) {
+ parseMultipleChannelResponse(input.body()?.payload!!)
+ } else {
+ if (channels.size > 1 || channelGroups.isNotEmpty()) {
+ parseMultipleChannelResponse(input.body()?.payload!!)
+ } else {
+ parseSingleChannelResponse(input.body()!!)
+ }
+ }
+ }
+
+ private fun parseSingleChannelResponse(input: Envelope): PNHereNowResult {
+ val pnHereNowResult = PNHereNowResult(
+ totalChannels = 1,
+ totalOccupancy = input.occupancy
+ )
+
+ val pnHereNowChannelData = PNHereNowChannelData(
+ channelName = channels[0],
+ occupancy = input.occupancy
+ )
+
+ if (includeUUIDs) {
+ pnHereNowChannelData.occupants = prepareOccupantData(input.uuids!!)
+ pnHereNowResult.channels[channels[0]] = pnHereNowChannelData
+ }
+
+ return pnHereNowResult
+ }
+
+ private fun parseMultipleChannelResponse(input: JsonElement): PNHereNowResult {
+ val pnHereNowResult = PNHereNowResult(
+ totalChannels = pubnub.mapper.elementToInt(input, "total_channels"),
+ totalOccupancy = pubnub.mapper.elementToInt(input, "total_occupancy")
+ )
+
+ val it = pubnub.mapper.getObjectIterator(input, "channels")
+
+ while (it.hasNext()) {
+ val entry = it.next()
+ val pnHereNowChannelData = PNHereNowChannelData(
+ channelName = entry.key,
+ occupancy = pubnub.mapper.elementToInt(entry.value, "occupancy")
+ )
+ if (includeUUIDs) {
+ pnHereNowChannelData.occupants = prepareOccupantData(pubnub.mapper.getField(entry.value, "uuids")!!)
+ }
+ pnHereNowResult.channels[entry.key] = pnHereNowChannelData
+ }
+
+ return pnHereNowResult
+ }
+
+ private fun prepareOccupantData(input: JsonElement): MutableList {
+ val occupantsResults = mutableListOf()
+
+ val it = pubnub.mapper.getArrayIterator(input)
+ while (it?.hasNext()!!) {
+ val occupant = it.next()
+ occupantsResults.add(
+ if (includeState) {
+ PNHereNowOccupantData(
+ uuid = pubnub.mapper.elementToString(occupant, "uuid")!!,
+ state = pubnub.mapper.getField(occupant, "state")
+ )
+ } else {
+ PNHereNowOccupantData(
+ uuid = pubnub.mapper.elementToString(occupant)!!
+ )
+ }
+ )
+ }
+
+ return occupantsResults
+ }
+
+ override fun operationType() = PNOperationType.PNHereNowOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt
new file mode 100644
index 000000000..57d0e1e05
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt
@@ -0,0 +1,39 @@
+package com.pubnub.api.endpoints.presence
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class Leave internal constructor(pubnub: PubNub) : Endpoint(pubnub) {
+
+ var channels = emptyList()
+ var channelGroups = emptyList()
+
+ override fun validateParams() {
+ super.validateParams()
+ if (channels.isEmpty() && channelGroups.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["channel-group"] = channelGroups.toCsv()
+
+ return pubnub.retrofitManager.presenceService.leave(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response) = true
+
+ override fun operationType() = PNOperationType.PNUnsubscribeOperation
+}
+
+
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt
new file mode 100644
index 000000000..560923ceb
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt
@@ -0,0 +1,70 @@
+package com.pubnub.api.endpoints.presence
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.pubnub.api.*
+import com.pubnub.api.builder.StateOperation
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.presence.PNSetStateResult
+import com.pubnub.api.models.server.Envelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class SetState(pubnub: PubNub) : Endpoint, PNSetStateResult>(pubnub) {
+
+ var channels = emptyList()
+ var channelGroups = emptyList()
+ var uuid = pubnub.configuration.uuid
+ lateinit var state: Any
+
+ override fun getAffectedChannels() = channels
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun validateParams() {
+ super.validateParams()
+ if (channels.isNullOrEmpty() && channelGroups.isNullOrEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING)
+ }
+ if (!::state.isInitialized) {
+ throw PubNubException(PubNubError.STATE_MISSING)
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call> {
+ if (uuid == pubnub.configuration.uuid) {
+ pubnub.subscriptionManager.adaptStateBuilder(
+ StateOperation(
+ state = state
+ ).apply {
+ this.channels = this@SetState.channels
+ this.channelGroups = this@SetState.channelGroups
+ }
+ )
+ }
+
+ if (channelGroups.isNotEmpty()) {
+ queryParams["channel-group"] = channelGroups.toCsv()
+ }
+ queryParams["state"] = pubnub.mapper.toJson(state)
+
+ return pubnub.retrofitManager.presenceService.setState(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ uuid,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNSetStateResult? {
+ if (input.body()!!.payload!! is JsonNull) {
+ throw PubNubException(PubNubError.PARSING_ERROR)
+ }
+ return PNSetStateResult(input.body()!!.payload!!)
+ }
+
+ override fun operationType() = PNOperationType.PNSetStateOperation
+}
+
+
+
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt
new file mode 100644
index 000000000..a1099b24d
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt
@@ -0,0 +1,30 @@
+package com.pubnub.api.endpoints.presence
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.presence.PNWhereNowResult
+import com.pubnub.api.models.server.Envelope
+import com.pubnub.api.models.server.presence.WhereNowPayload
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class WhereNow(pubnub: PubNub) : Endpoint, PNWhereNowResult>(pubnub) {
+
+ var uuid = pubnub.configuration.uuid
+
+ override fun doWork(queryParams: HashMap): Call> {
+ return pubnub.retrofitManager.presenceService.whereNow(
+ pubnub.configuration.subscribeKey,
+ uuid,
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNWhereNowResult? {
+ return PNWhereNowResult(input.body()!!.payload!!.channels)
+ }
+
+ override fun operationType() = PNOperationType.PNWhereNowOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt
new file mode 100644
index 000000000..c33eca924
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt
@@ -0,0 +1,102 @@
+package com.pubnub.api.endpoints.pubsub
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.PNPublishResult
+import com.pubnub.api.vendor.Crypto
+import retrofit2.Call
+import retrofit2.Response
+
+class Publish(pubnub: PubNub) : Endpoint, PNPublishResult>(pubnub) {
+
+ lateinit var channel: String
+ lateinit var message: Any
+ lateinit var meta: Any
+ var shouldStore: Boolean? = null
+ var usePost = false
+ var replicate = true
+ var ttl: Int? = null
+
+ private fun isChannelValid() = ::channel.isInitialized
+ private fun isMessageValid() = ::message.isInitialized
+ private fun isMetaValid() = ::meta.isInitialized
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!isChannelValid() || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (!isMessageValid()) {
+ throw PubNubException(PubNubError.MESSAGE_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call> {
+
+ var stringifiedMessage = pubnub.mapper.toJson(message)
+
+ if (isMetaValid()) {
+ queryParams["meta"] = pubnub.mapper.toJson(meta)
+ }
+
+ shouldStore?.run { queryParams["store"] = if (this) "1" else "0" }
+
+ ttl?.let { queryParams["ttl"] = it.toString() }
+
+ if (!replicate) queryParams["norep"] = "true"
+
+ queryParams["seqn"] = pubnub.publishSequenceManager.nextSequence().toString()
+
+ if (pubnub.configuration.isCipherKeyValid()) {
+ stringifiedMessage = Crypto(pubnub.configuration.cipherKey)
+ .encrypt(stringifiedMessage)
+ .replace("\n", "")
+ }
+
+ if (usePost) {
+ var payload = message
+
+ if (pubnub.configuration.isCipherKeyValid()) {
+ payload = stringifiedMessage
+ }
+
+ return pubnub.retrofitManager.publishService.publishWithPost(
+ pubnub.configuration.publishKey,
+ pubnub.configuration.subscribeKey,
+ channel,
+ payload,
+ queryParams
+ )
+ } else {
+ // get request
+
+ if (pubnub.configuration.isCipherKeyValid()) {
+ stringifiedMessage = "\"$stringifiedMessage\""
+ }
+
+ return pubnub.retrofitManager.publishService.publish(
+ pubnub.configuration.publishKey,
+ pubnub.configuration.subscribeKey,
+ channel,
+ stringifiedMessage,
+ queryParams
+ )
+ }
+ }
+
+ override fun createResponse(input: Response>): PNPublishResult? {
+ return PNPublishResult(
+ timetoken = input.body()!![2].toString().toLong()
+ )
+ }
+
+ override fun operationType() = PNOperationType.PNPublishOperation
+
+ override fun isPubKeyRequired() = true
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt
new file mode 100644
index 000000000..87ec1ee67
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt
@@ -0,0 +1,52 @@
+package com.pubnub.api.endpoints.pubsub
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.consumer.PNPublishResult
+import retrofit2.Call
+import retrofit2.Response
+
+class Signal(pubnub: PubNub) : Endpoint, PNPublishResult>(pubnub) {
+
+ lateinit var channel: String
+ lateinit var message: Any
+
+ private fun isChannelValid() = ::channel.isInitialized
+ private fun isMessageValid() = ::message.isInitialized
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!isChannelValid() || channel.isBlank()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (!isMessageValid()) {
+ throw PubNubException(PubNubError.MESSAGE_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = listOf(channel)
+
+ override fun doWork(queryParams: HashMap): Call> {
+ return pubnub.retrofitManager.signalService.signal(
+ pubKey = pubnub.configuration.publishKey,
+ subKey = pubnub.configuration.subscribeKey,
+ channel = channel,
+ message = pubnub.mapper.toJson(message),
+ options = queryParams
+ )
+ }
+
+ override fun createResponse(input: Response>): PNPublishResult? {
+ return PNPublishResult(
+ timetoken = input.body()!![2].toString().toLong()
+ )
+ }
+
+ override fun operationType() = PNOperationType.PNSignalOperation
+
+ override fun isPubKeyRequired() = true
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt
new file mode 100644
index 000000000..7c61942eb
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt
@@ -0,0 +1,65 @@
+package com.pubnub.api.endpoints.pubsub
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.models.server.SubscribeEnvelope
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class Subscribe internal constructor(pubnub: PubNub) : Endpoint(pubnub) {
+
+ var channels = emptyList()
+ var channelGroups = emptyList()
+ var timetoken: Long? = null
+ var region: String? = null
+ var state: Any? = null
+ var filterExpression: String? = null
+
+ override fun validateParams() {
+ super.validateParams()
+ if (channels.isEmpty() && channelGroups.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING)
+ }
+ }
+
+ override fun getAffectedChannels() = channels
+
+ override fun getAffectedChannelGroups() = channelGroups
+
+ override fun doWork(queryParams: HashMap): Call {
+ if (channelGroups.isNotEmpty()) {
+ queryParams["channel-group"] = channelGroups.joinToString(",")
+ }
+
+ if (!filterExpression.isNullOrBlank()) {
+ queryParams["filter-expr"] = filterExpression!!
+ }
+
+ timetoken?.let {
+ queryParams["tt"] = it.toString()
+ }
+
+ region?.let {
+ queryParams["tr"] = it
+ }
+
+ queryParams["heartbeat"] = pubnub.configuration.presenceTimeout.toString()
+
+ state?.let {
+ queryParams["state"] = pubnub.mapper.toJson(it)
+ }
+
+ return pubnub.retrofitManager.subscribeService.subscribe(
+ pubnub.configuration.subscribeKey,
+ channels.toCsv(),
+ queryParams
+ )
+ }
+
+ override fun createResponse(input: Response): SubscribeEnvelope? {
+ return input.body()!!
+ }
+
+ override fun operationType() = PNOperationType.PNSubscribeOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt
new file mode 100644
index 000000000..31063213b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt
@@ -0,0 +1,71 @@
+package com.pubnub.api.endpoints.push
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNPushEnvironment
+import com.pubnub.api.enums.PNPushType
+import com.pubnub.api.models.consumer.push.PNPushAddChannelResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class AddChannelsToPush(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var pushType: PNPushType
+ lateinit var channels: List
+ lateinit var deviceId: String
+ var environment = PNPushEnvironment.DEVELOPMENT
+ lateinit var topic: String
+
+ override fun getAffectedChannels() = channels
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::pushType.isInitialized) {
+ throw PubNubException(PubNubError.PUSH_TYPE_MISSING)
+ }
+ if (!::deviceId.isInitialized || deviceId.isBlank()) {
+ throw PubNubException(PubNubError.DEVICE_ID_MISSING)
+ }
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (pushType == PNPushType.APNS2) {
+ if (!::topic.isInitialized || topic.isBlank()) {
+ throw PubNubException(PubNubError.PUSH_TOPIC_MISSING)
+ }
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["add"] = channels.toCsv()
+
+ if (pushType != PNPushType.APNS2) {
+ queryParams["type"] = pushType.toParamString()
+
+ return pubnub.retrofitManager.pushService
+ .modifyChannelsForDevice(
+ subKey = pubnub.configuration.subscribeKey,
+ pushToken = deviceId,
+ options = queryParams
+ )
+ }
+
+ queryParams["environment"] = environment.name.toLowerCase()
+ queryParams["topic"] = topic
+
+ return pubnub.retrofitManager.pushService
+ .modifyChannelsForDeviceApns2(
+ subKey = pubnub.configuration.subscribeKey,
+ deviceApns2 = deviceId,
+ options = queryParams
+ )
+
+ }
+
+ override fun createResponse(input: Response): PNPushAddChannelResult? {
+ return PNPushAddChannelResult()
+ }
+
+ override fun operationType() = PNOperationType.PNAddPushNotificationsOnChannelsOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt
new file mode 100644
index 000000000..04142acc3
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt
@@ -0,0 +1,66 @@
+package com.pubnub.api.endpoints.push
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNPushEnvironment
+import com.pubnub.api.enums.PNPushType
+import com.pubnub.api.models.consumer.push.PNPushListProvisionsResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class ListPushProvisions(pubnub: PubNub) : Endpoint, PNPushListProvisionsResult>(pubnub) {
+
+ lateinit var pushType: PNPushType
+ lateinit var deviceId: String
+ var environment = PNPushEnvironment.DEVELOPMENT
+ lateinit var topic: String
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::pushType.isInitialized) {
+ throw PubNubException(PubNubError.PUSH_TYPE_MISSING)
+ }
+ if (!::deviceId.isInitialized || deviceId.isBlank()) {
+ throw PubNubException(PubNubError.DEVICE_ID_MISSING)
+ }
+ if (pushType == PNPushType.APNS2) {
+ if (!::topic.isInitialized || topic.isBlank()) {
+ throw PubNubException(PubNubError.PUSH_TOPIC_MISSING)
+ }
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call> {
+ if (pushType != PNPushType.APNS2) {
+ queryParams["type"] = pushType.toParamString()
+
+ return pubnub.retrofitManager.pushService
+ .listChannelsForDevice(
+ subKey = pubnub.configuration.subscribeKey,
+ pushToken = deviceId,
+ options = queryParams
+ )
+ }
+
+ queryParams["environment"] = environment.name.toLowerCase()
+ queryParams["topic"] = topic
+
+ return pubnub.retrofitManager.pushService
+ .listChannelsForDeviceApns2(
+ subKey = pubnub.configuration.subscribeKey,
+ deviceApns2 = deviceId,
+ options = queryParams
+ )
+
+ }
+
+ override fun createResponse(input: Response>): PNPushListProvisionsResult? {
+ return PNPushListProvisionsResult(input.body()!!)
+ }
+
+ override fun operationType() = PNOperationType.PNPushNotificationEnabledChannelsOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt
new file mode 100644
index 000000000..99832820a
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt
@@ -0,0 +1,66 @@
+package com.pubnub.api.endpoints.push
+
+import com.pubnub.api.Endpoint
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNPushEnvironment
+import com.pubnub.api.enums.PNPushType
+import com.pubnub.api.models.consumer.push.PNPushRemoveAllChannelsResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class RemoveAllPushChannelsForDevice(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var pushType: PNPushType
+ lateinit var deviceId: String
+ var environment = PNPushEnvironment.DEVELOPMENT
+ lateinit var topic: String
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::pushType.isInitialized) {
+ throw PubNubException(PubNubError.PUSH_TYPE_MISSING)
+ }
+ if (!::deviceId.isInitialized || deviceId.isBlank()) {
+ throw PubNubException(PubNubError.DEVICE_ID_MISSING)
+ }
+ if (pushType == PNPushType.APNS2) {
+ if (!::topic.isInitialized || topic.isBlank()) {
+ throw PubNubException(PubNubError.PUSH_TOPIC_MISSING)
+ }
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ if (pushType != PNPushType.APNS2) {
+ queryParams["type"] = pushType.toParamString()
+
+ return pubnub.retrofitManager.pushService
+ .removeAllChannelsForDevice(
+ subKey = pubnub.configuration.subscribeKey,
+ pushToken = deviceId,
+ options = queryParams
+ )
+ }
+
+ queryParams["environment"] = environment.name.toLowerCase()
+ queryParams["topic"] = topic
+
+ return pubnub.retrofitManager.pushService
+ .removeAllChannelsForDeviceApns2(
+ subKey = pubnub.configuration.subscribeKey,
+ deviceApns2 = deviceId,
+ options = queryParams
+ )
+
+ }
+
+ override fun createResponse(input: Response): PNPushRemoveAllChannelsResult? {
+ return PNPushRemoveAllChannelsResult()
+ }
+
+ override fun operationType() = PNOperationType.PNRemoveAllPushNotificationsOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt
new file mode 100644
index 000000000..71d9ea98b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt
@@ -0,0 +1,71 @@
+package com.pubnub.api.endpoints.push
+
+import com.pubnub.api.*
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNPushEnvironment
+import com.pubnub.api.enums.PNPushType
+import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult
+import retrofit2.Call
+import retrofit2.Response
+import java.util.*
+
+class RemoveChannelsFromPush(pubnub: PubNub) : Endpoint(pubnub) {
+
+ lateinit var pushType: PNPushType
+ lateinit var channels: List
+ lateinit var deviceId: String
+ var environment = PNPushEnvironment.DEVELOPMENT
+ lateinit var topic: String
+
+ override fun getAffectedChannels() = channels
+
+ override fun validateParams() {
+ super.validateParams()
+ if (!::pushType.isInitialized) {
+ throw PubNubException(PubNubError.PUSH_TYPE_MISSING)
+ }
+ if (!::deviceId.isInitialized || deviceId.isBlank()) {
+ throw PubNubException(PubNubError.DEVICE_ID_MISSING)
+ }
+ if (!::channels.isInitialized || channels.isEmpty()) {
+ throw PubNubException(PubNubError.CHANNEL_MISSING)
+ }
+ if (pushType == PNPushType.APNS2) {
+ if (!::topic.isInitialized || topic.isBlank()) {
+ throw PubNubException(PubNubError.PUSH_TOPIC_MISSING)
+ }
+ }
+ }
+
+ override fun doWork(queryParams: HashMap): Call {
+ queryParams["remove"] = channels.toCsv()
+
+ if (pushType != PNPushType.APNS2) {
+ queryParams["type"] = pushType.toParamString()
+
+ return pubnub.retrofitManager.pushService
+ .modifyChannelsForDevice(
+ subKey = pubnub.configuration.subscribeKey,
+ pushToken = deviceId,
+ options = queryParams
+ )
+ }
+
+ queryParams["environment"] = environment.name.toLowerCase()
+ queryParams["topic"] = topic
+
+ return pubnub.retrofitManager.pushService
+ .modifyChannelsForDeviceApns2(
+ subKey = pubnub.configuration.subscribeKey,
+ deviceApns2 = deviceId,
+ options = queryParams
+ )
+
+ }
+
+ override fun createResponse(input: Response): PNPushRemoveChannelResult? {
+ return PNPushRemoveChannelResult()
+ }
+
+ override fun operationType() = PNOperationType.PNRemovePushNotificationsFromChannelsOperation
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt b/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt
new file mode 100644
index 000000000..322145904
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt
@@ -0,0 +1,7 @@
+package com.pubnub.api.enums
+
+enum class PNHeartbeatNotificationOptions {
+ NONE,
+ FAILURES,
+ ALL
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt b/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt
new file mode 100644
index 000000000..583a0e34b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt
@@ -0,0 +1,6 @@
+package com.pubnub.api.enums
+
+enum class PNLogVerbosity {
+ NONE,
+ BODY,
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt b/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt
new file mode 100644
index 000000000..6b7b7fadf
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt
@@ -0,0 +1,78 @@
+package com.pubnub.api.enums
+
+sealed class PNOperationType(open val queryParam: String? = null) {
+
+ open class PublishOperation : PNOperationType("pub")
+ open class HistoryOperation : PNOperationType("hist")
+ open class PresenceOperation : PNOperationType("pres")
+ open class ChannelGroupOperation : PNOperationType("cg")
+ open class PushNotificationsOperation : PNOperationType("push")
+ open class PAMOperation : PNOperationType("pam")
+ open class MessageCountsOperation : PNOperationType("mc")
+ open class SignalsOperation : PNOperationType("sig")
+ open class ObjectsOperation : PNOperationType("obj")
+ open class PAMV3Operation : PNOperationType("pamv3")
+ open class MessageActionsOperation : PNOperationType("msga")
+ open class TimeOperation : PNOperationType("time")
+
+ object PNSubscribeOperation : PNOperationType()
+
+ object PNPublishOperation : PublishOperation()
+
+ object PNHistoryOperation : HistoryOperation()
+ object PNFetchMessagesOperation : HistoryOperation()
+ object PNDeleteMessagesOperation : HistoryOperation()
+
+ object PNUnsubscribeOperation : PresenceOperation()
+ object PNWhereNowOperation : PresenceOperation()
+ object PNHereNowOperation : PresenceOperation()
+ object PNHeartbeatOperation : PresenceOperation()
+ object PNSetStateOperation : PresenceOperation()
+ object PNGetState : PresenceOperation()
+
+ object PNAddChannelsToGroupOperation : ChannelGroupOperation()
+ object PNRemoveChannelsFromGroupOperation : ChannelGroupOperation()
+ object PNChannelGroupsOperation : ChannelGroupOperation()
+ object PNRemoveGroupOperation : ChannelGroupOperation()
+ object PNChannelsForGroupOperation : ChannelGroupOperation()
+
+ object PNPushNotificationEnabledChannelsOperation : PushNotificationsOperation()
+ object PNAddPushNotificationsOnChannelsOperation : PushNotificationsOperation()
+ object PNRemovePushNotificationsFromChannelsOperation : PushNotificationsOperation()
+ object PNRemoveAllPushNotificationsOperation : PushNotificationsOperation()
+
+ object PNAccessManagerAudit : PAMOperation()
+ object PNAccessManagerGrant : PAMOperation()
+
+ object PNMessageCountOperation : MessageCountsOperation()
+
+ object PNSignalOperation : SignalsOperation()
+
+ object PNCreateUserOperation : ObjectsOperation()
+ object PNGetUserOperation : ObjectsOperation()
+ object PNGetUsersOperation : ObjectsOperation()
+ object PNUpdateUserOperation : ObjectsOperation()
+ object PNDeleteUserOperation : ObjectsOperation()
+ object PNCreateSpaceOperation : ObjectsOperation()
+ object PNGetSpaceOperation : ObjectsOperation()
+ object PNGetSpacesOperation : ObjectsOperation()
+ object PNUpdateSpaceOperation : ObjectsOperation()
+ object PNDeleteSpaceOperation : ObjectsOperation()
+ object PNGetMembers : ObjectsOperation()
+ object PNManageMembers : ObjectsOperation()
+ object PNGetMemberships : ObjectsOperation()
+ object PNManageMemberships : ObjectsOperation()
+
+ object PNAccessManagerGrantToken : PAMV3Operation()
+
+ object PNAddMessageAction : MessageActionsOperation()
+ object PNGetMessageActions : MessageActionsOperation()
+ object PNDeleteMessageAction : MessageActionsOperation()
+
+ object PNTimeOperation : TimeOperation()
+
+ override fun toString(): String {
+ return this.javaClass.simpleName
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt b/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt
new file mode 100644
index 000000000..1a14b6a80
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt
@@ -0,0 +1,6 @@
+package com.pubnub.api.enums
+
+enum class PNPushEnvironment {
+ DEVELOPMENT,
+ PRODUCTION
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt b/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt
new file mode 100644
index 000000000..73429ccf3
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt
@@ -0,0 +1,17 @@
+package com.pubnub.api.enums
+
+import java.util.*
+
+enum class PNPushType(s: String) {
+
+ APNS("apns"),
+ MPNS("mpns"),
+ FCM("gcm"),
+ APNS2("apns2");
+
+ private val value: String = s
+
+ fun toParamString(): String {
+ return value.toLowerCase(Locale.US)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt b/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt
new file mode 100644
index 000000000..bc0bce39a
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt
@@ -0,0 +1,7 @@
+package com.pubnub.api.enums
+
+enum class PNReconnectionPolicy {
+ NONE,
+ LINEAR,
+ EXPONENTIAL
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt b/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt
new file mode 100644
index 000000000..bb46a7695
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt
@@ -0,0 +1,24 @@
+package com.pubnub.api.enums
+
+enum class PNStatusCategory {
+ PNUnknownCategory,
+ PNAcknowledgmentCategory,
+ PNAccessDeniedCategory,
+ PNTimeoutCategory,
+ PNNetworkIssuesCategory,
+ PNConnectedCategory,
+ PNReconnectedCategory,
+ PNDisconnectedCategory,
+ PNUnexpectedDisconnectCategory,
+ PNCancelledCategory,
+ PNBadRequestCategory,
+ PNMalformedFilterExpressionCategory,
+ PNMalformedResponseCategory,
+ PNDecryptionErrorCategory,
+ PNTLSConnectionFailedCategory,
+ PNTLSUntrustedCertificateCategory,
+
+ PNRequestMessageCountExceededCategory,
+ PNReconnectionAttemptsExhausted,
+ PNNotFoundCategory
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt b/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt
new file mode 100644
index 000000000..9ee09ba6a
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt
@@ -0,0 +1,17 @@
+package com.pubnub.api.interceptor
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubUtil
+import okhttp3.Interceptor
+import okhttp3.Response
+
+class SignatureInterceptor(val pubnub: PubNub) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val request = PubNubUtil.signRequest(originalRequest, pubnub.configuration, pubnub.timestamp())
+ return chain.proceed(request)
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt b/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt
new file mode 100644
index 000000000..3a4640ac2
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt
@@ -0,0 +1,66 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PNConfiguration
+
+internal class BasePathManager(private val config: PNConfiguration) {
+
+ /**
+ * for cache busting, the current subdomain number used.
+ */
+ private var currentSubdomain = 1
+
+ /**
+ * if using cache busting, this is the max number of subdomains that are supported.
+ */
+ private val MAX_SUBDOMAIN = 20
+
+ /**
+ * default subdomain used if cache busting is disabled.
+ */
+
+ private val DEFAULT_SUBDOMAIN = "ps"
+
+ /**
+ * default base path if a custom one is not provided.
+ */
+
+ private val DEFAULT_BASE_PATH = "pndsn.com"
+
+ fun basePath(): String {
+ val basePathBuilder = StringBuilder("http")
+ .append(if (config.secure) "s" else "")
+ .append("://")
+
+ when {
+ config.isOriginValid() -> {
+ basePathBuilder.append(config.origin)
+ }
+ config.cacheBusting -> {
+ basePathBuilder
+ .append("ps")
+ .append(currentSubdomain)
+ .append(".")
+ .append(DEFAULT_BASE_PATH)
+
+ incrementSubdomain()
+ }
+ else -> {
+ basePathBuilder
+ .append(DEFAULT_SUBDOMAIN)
+ .append(".")
+ .append(DEFAULT_BASE_PATH)
+ }
+ }
+
+ return basePathBuilder.toString()
+ }
+
+ private fun incrementSubdomain() {
+ if (currentSubdomain == MAX_SUBDOMAIN) {
+ currentSubdomain = 1
+ } else {
+ currentSubdomain++
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt b/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt
new file mode 100644
index 000000000..16d1e90f3
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt
@@ -0,0 +1,26 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PNConfiguration
+import com.pubnub.api.models.server.SubscribeMessage
+
+internal class DuplicationManager(private val config: PNConfiguration) {
+
+ private val hashHistory: ArrayList = ArrayList()
+
+ private fun getKey(message: SubscribeMessage) =
+ with(message) {
+ "${publishMetaData?.publishTimetoken}-${payload.hashCode()}"
+ }
+
+ fun isDuplicate(message: SubscribeMessage) = hashHistory.contains(getKey(message))
+
+ fun addEntry(message: SubscribeMessage) {
+ if (hashHistory.size >= config.maximumMessagesCacheSize) {
+ hashHistory.removeAt(0)
+ }
+ hashHistory.add(getKey(message))
+ }
+
+ fun clearHistory() = hashHistory.clear()
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt b/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt
new file mode 100644
index 000000000..8a70b7459
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt
@@ -0,0 +1,57 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.callbacks.SubscribeCallback
+import com.pubnub.api.models.consumer.PNStatus
+import com.pubnub.api.models.consumer.pubsub.PNMessageResult
+import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult
+import com.pubnub.api.models.consumer.pubsub.PNSignalResult
+import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult
+import java.util.*
+
+internal class ListenerManager(val pubnub: PubNub) {
+ private val listeners = mutableListOf()
+
+ fun addListener(listener: SubscribeCallback) {
+ synchronized(listeners) {
+ listeners.add(listener)
+ }
+ }
+
+ fun removeListener(listener: SubscribeCallback) {
+ synchronized(listeners) {
+ listeners.remove(listener)
+ }
+ }
+
+ private fun getListeners(): List {
+ val tempCallbackList = ArrayList()
+ synchronized(listeners) {
+ tempCallbackList.addAll(listeners)
+ }
+ return tempCallbackList
+ }
+
+ @Synchronized
+ fun announce(status: PNStatus) {
+ getListeners().forEach { it.status(pubnub, status) }
+ }
+
+ fun announce(message: PNMessageResult) {
+ getListeners().forEach { it.message(pubnub, message) }
+ }
+
+ fun announce(presence: PNPresenceEventResult) {
+ getListeners().forEach { it.presence(pubnub, presence) }
+ }
+
+ fun announce(signal: PNSignalResult) {
+ getListeners().forEach { it.signal(pubnub, signal) }
+ }
+
+ fun announce(messageAction: PNMessageActionResult) {
+ getListeners().forEach { it.messageAction(pubnub, messageAction) }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt b/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt
new file mode 100644
index 000000000..4d9c424c5
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt
@@ -0,0 +1,200 @@
+package com.pubnub.api.managers
+
+import com.google.gson.*
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonToken
+import com.google.gson.stream.JsonWriter
+import com.pubnub.api.PubNubError
+import com.pubnub.api.PubNubException
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import retrofit2.Converter
+import retrofit2.converter.gson.GsonConverterFactory
+import java.lang.reflect.Type
+
+class MapperManager {
+
+ private val objectMapper: Gson
+ internal val converterFactory: Converter.Factory
+
+ init {
+ val booleanAsIntAdapter = object : TypeAdapter() {
+ override fun write(out: JsonWriter?, value: Boolean?) {
+ if (value == null) {
+ out?.nullValue()
+ } else {
+ out?.value(value)
+ }
+ }
+
+ override fun read(_in: JsonReader): Boolean? {
+ val peek: JsonToken = _in.peek()
+ return when (peek) {
+ JsonToken.BOOLEAN -> _in.nextBoolean()
+ JsonToken.NUMBER -> _in.nextInt() != 0
+ JsonToken.STRING -> java.lang.Boolean.parseBoolean(_in.nextString())
+ else -> throw IllegalStateException("Expected BOOLEAN or NUMBER but was $peek")
+ }
+ }
+ }
+
+ objectMapper = GsonBuilder()
+ .registerTypeAdapter(Boolean::class.javaObjectType, booleanAsIntAdapter)
+ .registerTypeAdapter(Boolean::class.javaPrimitiveType, booleanAsIntAdapter)
+ .registerTypeAdapter(Boolean::class.java, booleanAsIntAdapter)
+ .registerTypeAdapter(JSONObject::class.java, JSONObjectAdapter())
+ .registerTypeAdapter(JSONArray::class.java, JSONArrayAdapter())
+ .create()
+ converterFactory = GsonConverterFactory.create(objectMapper)
+ }
+
+ fun hasField(element: JsonElement, field: String) = element.asJsonObject.has(field)
+
+ fun getField(element: JsonElement?, field: String): JsonElement? {
+ if (element?.isJsonObject!!) {
+ return element.asJsonObject.get(field)
+ }
+ return null
+ }
+
+ fun getArrayIterator(element: JsonElement?) = element?.asJsonArray?.iterator()
+
+ fun getArrayIterator(element: JsonElement, field: String) = element.asJsonObject.get(field).asJsonArray.iterator()
+
+ fun getObjectIterator(element: JsonElement) = element.asJsonObject.entrySet().iterator()
+
+ fun getObjectIterator(element: JsonElement, field: String) =
+ element.asJsonObject.get(field).asJsonObject.entrySet().iterator()
+
+ fun elementToString(element: JsonElement?) = element?.asString
+
+ fun elementToString(element: JsonElement?, field: String) = element?.asJsonObject?.get(field)?.asString
+
+ fun elementToInt(element: JsonElement, field: String) = element.asJsonObject.get(field).asInt
+
+ fun isJsonObject(element: JsonElement) = element.isJsonObject
+
+ fun getAsObject(element: JsonElement) = element.asJsonObject
+
+ fun getAsBoolean(element: JsonElement, field: String) = element.asJsonObject.get(field)?.asBoolean
+ .run { this != null }
+
+ fun putOnObject(element: JsonObject, key: String, value: JsonElement) = element.add(key, value)
+
+ fun getArrayElement(element: JsonElement, index: Int) = element.asJsonArray.get(index)
+
+ fun elementToLong(element: JsonElement) = element.asLong
+
+ fun elementToLong(element: JsonElement, field: String) = element.asJsonObject.get(field).asLong
+
+ fun getAsArray(element: JsonElement) = element.asJsonArray
+
+ fun fromJson(input: String?, clazz: Class): T {
+ return try {
+ this.objectMapper.fromJson(input, clazz)
+ } catch (e: JsonParseException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.message
+ }
+ }
+ }
+
+ fun fromJson(input: String?, typeOfT: Type): T {
+ return try {
+ this.objectMapper.fromJson(input, typeOfT)
+ } catch (e: JsonParseException) {
+ throw PubNubException(PubNubError.PARSING_ERROR).apply {
+ errorMessage = e.message
+ }
+ }
+ }
+
+ fun convertValue(input: JsonElement?, clazz: Class): T {
+ return this.objectMapper.fromJson(input, clazz) as T
+ }
+
+ fun convertValue(o: Any?, clazz: Class?): T {
+ return this.objectMapper.fromJson(toJson(o), clazz) as T
+ }
+
+ fun toJson(input: Any?): String {
+ return try {
+ this.objectMapper.toJson(input)
+ } catch (e: JsonParseException) {
+ throw PubNubException(PubNubError.JSON_ERROR).apply {
+ errorMessage = e.message
+ }
+ }
+ }
+
+ private class JSONObjectAdapter : JsonSerializer, JsonDeserializer {
+ override fun serialize(
+ src: JSONObject?,
+ typeOfSrc: Type?,
+ context: JsonSerializationContext
+ ): JsonElement? {
+ if (src == null) {
+ return null
+ }
+ val jsonObject = JsonObject()
+ val keys: Iterator = src.keys()
+ while (keys.hasNext()) {
+ val key = keys.next()
+ val value: Any = src.opt(key)
+ val jsonElement = context.serialize(value, value.javaClass)
+ jsonObject.add(key, jsonElement)
+ }
+ return jsonObject
+ }
+
+ override fun deserialize(
+ json: JsonElement?,
+ typeOfT: Type?,
+ context: JsonDeserializationContext?
+ ): JSONObject? {
+ return if (json == null) {
+ null
+ } else try {
+ JSONObject(json.toString())
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ throw JsonParseException(e)
+ }
+ }
+ }
+
+ private class JSONArrayAdapter : JsonSerializer, JsonDeserializer {
+ override fun serialize(
+ src: JSONArray?,
+ typeOfSrc: Type?,
+ context: JsonSerializationContext
+ ): JsonElement? {
+ if (src == null) {
+ return null
+ }
+ val jsonArray = JsonArray()
+ for (i in 0 until src.length()) {
+ val `object`: Any = src.opt(i)
+ val jsonElement = context.serialize(`object`, `object`.javaClass)
+ jsonArray.add(jsonElement)
+ }
+ return jsonArray
+ }
+
+ override fun deserialize(
+ json: JsonElement?,
+ typeOfT: Type?,
+ context: JsonDeserializationContext?
+ ): JSONArray? {
+ return if (json == null) {
+ null
+ } else try {
+ JSONArray(json.toString())
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ throw JsonParseException(e)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt b/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt
new file mode 100644
index 000000000..589d0473b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt
@@ -0,0 +1,17 @@
+package com.pubnub.api.managers
+
+internal class PublishSequenceManager(private val maxSequence: Int) {
+
+ private var nextSequence = 0
+
+ internal fun nextSequence(): Int {
+ synchronized(nextSequence) {
+ if (maxSequence == nextSequence) {
+ nextSequence = 1
+ } else {
+ nextSequence++
+ }
+ return nextSequence
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt b/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt
new file mode 100644
index 000000000..2b71e118d
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt
@@ -0,0 +1,111 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PNConfiguration
+import com.pubnub.api.PubNub
+import com.pubnub.api.callbacks.ReconnectionCallback
+import com.pubnub.api.enums.PNReconnectionPolicy
+import org.slf4j.LoggerFactory
+import java.util.*
+import kotlin.math.pow
+
+internal class ReconnectionManager(val pubnub: PubNub) {
+
+ private val log = LoggerFactory.getLogger("ReconnectionManager")
+
+ private companion object {
+ private const val LINEAR_INTERVAL = 3
+ private const val MIN_EXPONENTIAL_BACKOFF = 1
+ private const val MAX_EXPONENTIAL_BACKOFF = 32
+
+ private const val MILLISECONDS = 1000
+ }
+
+ internal lateinit var reconnectionCallback: ReconnectionCallback
+
+ private var exponentialMultiplier = 1
+ private var failedCalls = 0
+
+ private lateinit var pnReconnectionPolicy: PNReconnectionPolicy
+ private var maxConnectionRetries = -1
+
+ private val timer = Timer()
+
+ internal fun startPolling(pnConfiguration: PNConfiguration) {
+ pnReconnectionPolicy = pnConfiguration.reconnectionPolicy
+ maxConnectionRetries = pnConfiguration.maximumReconnectionRetries
+
+ if (isReconnectionPolicyUndefined())
+ return
+
+ exponentialMultiplier = 1
+ failedCalls = 0
+
+ registerHeartbeatTimer()
+ }
+
+ private fun registerHeartbeatTimer() {
+ // make sure only one timer is running at a time.
+ stopHeartbeatTimer()
+
+ if (isReconnectionPolicyUndefined()) {
+ return
+ }
+
+ if (maxConnectionRetries != -1 && failedCalls >= maxConnectionRetries) {
+ reconnectionCallback.onMaxReconnectionExhaustion()
+ return
+ }
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ callTime()
+ }
+ }, getBestInterval() * MILLISECONDS.toLong())
+ }
+
+ private fun getBestInterval(): Int {
+ var timerInterval = LINEAR_INTERVAL
+ if (pnReconnectionPolicy == PNReconnectionPolicy.EXPONENTIAL) {
+ timerInterval = (2.0.pow(exponentialMultiplier.toDouble()) - 1).toInt()
+ if (timerInterval > MAX_EXPONENTIAL_BACKOFF) {
+ timerInterval = MIN_EXPONENTIAL_BACKOFF
+ exponentialMultiplier = 1
+ log.info("timerInterval > MAXEXPONENTIALBACKOFF at: " + Calendar.getInstance().time.toString())
+ } else if (timerInterval < 1) {
+ timerInterval = MIN_EXPONENTIAL_BACKOFF
+ }
+ log.info("timerInterval = " + timerInterval + " at: " + Calendar.getInstance().time.toString())
+ }
+ if (pnReconnectionPolicy == PNReconnectionPolicy.LINEAR) {
+ timerInterval = LINEAR_INTERVAL
+ }
+ return timerInterval
+ }
+
+ private fun stopHeartbeatTimer() {
+ timer.cancel()
+ }
+
+ private fun callTime() {
+ pubnub.time()
+ .async { _, status ->
+ if (!status.error) {
+ stopHeartbeatTimer()
+ reconnectionCallback.onReconnection()
+ } else {
+ log.info("callTime at ${System.currentTimeMillis()}")
+ exponentialMultiplier++
+ failedCalls++
+ registerHeartbeatTimer()
+ }
+ }
+ }
+
+ private fun isReconnectionPolicyUndefined(): Boolean {
+ if (pnReconnectionPolicy == PNReconnectionPolicy.NONE) {
+ log.info("reconnection policy is disabled, please handle reconnection manually.")
+ return true
+ }
+ return false
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt b/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt
new file mode 100644
index 000000000..04ec1042b
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt
@@ -0,0 +1,113 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.enums.PNLogVerbosity
+import com.pubnub.api.interceptor.SignatureInterceptor
+import com.pubnub.api.services.*
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import java.util.concurrent.TimeUnit
+
+internal class RetrofitManager(val pubnub: PubNub) {
+
+ private val transactionClientInstance: OkHttpClient by lazy {
+ createOkHttpClient(pubnub.configuration.nonSubscribeRequestTimeout)
+ }
+
+ private val subscriptionClientInstance: OkHttpClient by lazy {
+ createOkHttpClient(pubnub.configuration.subscribeTimeout)
+ }
+
+ private val signatureInterceptor: SignatureInterceptor
+
+ internal val timeService: TimeService
+ internal val publishService: PublishService
+ internal val historyService: HistoryService
+ internal val presenceService: PresenceService
+ internal val messageActionService: MessageActionService
+ internal val signalService: SignalService
+ internal val channelGroupService: ChannelGroupService
+ internal val pushService: PushService
+ internal val accessManagerService: AccessManagerService
+
+ internal val subscribeService: SubscribeService
+
+
+ init {
+ signatureInterceptor = SignatureInterceptor(pubnub)
+
+ val transactionInstance = createRetrofit(transactionClientInstance)
+ val subscriptionInstance = createRetrofit(subscriptionClientInstance)
+
+ timeService = transactionInstance.create(TimeService::class.java)
+ publishService = transactionInstance.create(PublishService::class.java)
+ historyService = transactionInstance.create(HistoryService::class.java)
+ presenceService = transactionInstance.create(PresenceService::class.java)
+ messageActionService = transactionInstance.create(MessageActionService::class.java)
+ signalService = transactionInstance.create(SignalService::class.java)
+ channelGroupService = transactionInstance.create(ChannelGroupService::class.java)
+ pushService = transactionInstance.create(PushService::class.java)
+ accessManagerService = transactionInstance.create(AccessManagerService::class.java)
+
+ subscribeService = subscriptionInstance.create(SubscribeService::class.java)
+ }
+
+ private fun createOkHttpClient(readTimeout: Int): OkHttpClient {
+ val okHttpBuilder = OkHttpClient.Builder()
+ .retryOnConnectionFailure(true)
+ .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
+ .connectTimeout(pubnub.configuration.connectTimeout.toLong(), TimeUnit.SECONDS)
+
+ with(pubnub.configuration) {
+ if (logVerbosity == PNLogVerbosity.BODY) {
+ okHttpBuilder.addInterceptor(HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ })
+ }
+ if (sslSocketFactory != null && x509ExtendedTrustManager != null) {
+ okHttpBuilder.sslSocketFactory(
+ pubnub.configuration.sslSocketFactory!!,
+ pubnub.configuration.x509ExtendedTrustManager!!
+ )
+ }
+ connectionSpec?.let { okHttpBuilder.connectionSpecs(listOf(it)) }
+ hostnameVerifier?.let { okHttpBuilder.hostnameVerifier(it) }
+ proxy?.let { okHttpBuilder.proxy(it) }
+ proxySelector?.let { okHttpBuilder.proxySelector(it) }
+ proxyAuthenticator?.let { okHttpBuilder.proxyAuthenticator(it) }
+ certificatePinner?.let { okHttpBuilder.certificatePinner(it) }
+ }
+
+ okHttpBuilder.addInterceptor(signatureInterceptor)
+
+ val okHttpClient = okHttpBuilder.build()
+
+ pubnub.configuration.maximumConnections?.let { okHttpClient.dispatcher().maxRequestsPerHost = it }
+
+ return okHttpClient
+ }
+
+ private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
+ val retrofitBuilder = Retrofit.Builder()
+ .client(okHttpClient)
+ .baseUrl(pubnub.baseUrl())
+ .addConverterFactory(pubnub.mapper.converterFactory)
+
+ return retrofitBuilder.build()
+ }
+
+ fun destroy(force: Boolean = false) {
+ closeExecutor(transactionClientInstance, force)
+ closeExecutor(subscriptionClientInstance, force)
+ }
+
+ private fun closeExecutor(client: OkHttpClient, force: Boolean) {
+ client.dispatcher().cancelAll()
+ if (force) {
+ client.connectionPool().evictAll()
+ val executorService = client.dispatcher().executorService()
+ executorService.shutdown()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/StateManager.kt b/src/main/kotlin/com/pubnub/api/managers/StateManager.kt
new file mode 100644
index 000000000..861966ca8
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/StateManager.kt
@@ -0,0 +1,170 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.builder.PresenceOperation
+import com.pubnub.api.builder.StateOperation
+import com.pubnub.api.builder.SubscribeOperation
+import com.pubnub.api.builder.UnsubscribeOperation
+import com.pubnub.api.models.SubscriptionItem
+
+internal class StateManager {
+
+ /**
+ * Contains a list of subscribed channels
+ */
+ private val channels: HashMap = hashMapOf()
+
+ /**
+ * Contains a list of subscribed presence channels.
+ */
+ private val presenceChannels: HashMap = hashMapOf()
+
+ /**
+ * Contains a list of subscribed channel groups.
+ */
+ private val groups: HashMap = hashMapOf()
+
+ /**
+ * Contains a list of subscribed presence channel groups.
+ */
+ private val presenceGroups: HashMap = hashMapOf()
+
+ private val heartbeatChannels: HashMap = hashMapOf()
+ private val heartbeatGroups: HashMap = hashMapOf()
+
+ @Synchronized
+ internal fun adaptSubscribeBuilder(subscribeOperation: SubscribeOperation) {
+ for (channel in subscribeOperation.channels) {
+ if (channel.isEmpty()) {
+ continue
+ }
+ val subscriptionItem = SubscriptionItem(channel)
+
+ channels[channel] = subscriptionItem
+
+ if (subscribeOperation.presenceEnabled) {
+ val presenceSubscriptionItem = SubscriptionItem(channel)
+ presenceChannels[channel] = presenceSubscriptionItem
+ }
+ }
+ for (channelGroup in subscribeOperation.channelGroups) {
+ if (channelGroup.isEmpty()) {
+ continue
+ }
+ val subscriptionItem = SubscriptionItem(channelGroup)
+ groups.put(channelGroup, subscriptionItem)
+ if (subscribeOperation.presenceEnabled) {
+ val presenceSubscriptionItem = SubscriptionItem(channelGroup)
+ presenceGroups[channelGroup] = presenceSubscriptionItem
+ }
+ }
+ }
+
+ @Synchronized
+ internal fun adaptStateBuilder(stateOperation: StateOperation) {
+ for (channel in stateOperation.channels) {
+ val subscribedChannel = channels[channel]
+ subscribedChannel?.state = stateOperation.state
+ }
+ for (channelGroup in stateOperation.channelGroups) {
+ val subscribedChannelGroup = groups[channelGroup]
+ subscribedChannelGroup?.state = stateOperation.state
+ }
+ }
+
+ @Synchronized
+ internal fun adaptUnsubscribeBuilder(unsubscribeOperation: UnsubscribeOperation) {
+ for (channel in unsubscribeOperation.channels) {
+ channels.remove(channel)
+ presenceChannels.remove(channel)
+ }
+ for (channelGroup in unsubscribeOperation.channelGroups) {
+ groups.remove(channelGroup)
+ presenceGroups.remove(channelGroup)
+ }
+ }
+
+ @Synchronized
+ internal fun adaptPresenceBuilder(presenceOperation: PresenceOperation) {
+ for (channel in presenceOperation.channels) {
+ if (channel.isEmpty()) {
+ continue
+ }
+ if (presenceOperation.connected) {
+ val subscriptionItem = SubscriptionItem(channel)
+ heartbeatChannels[channel] = subscriptionItem
+ } else {
+ heartbeatChannels.remove(channel)
+ }
+ }
+ for (channelGroup in presenceOperation.channelGroups) {
+ if (channelGroup.isEmpty()) {
+ continue
+ }
+ if (presenceOperation.connected) {
+ val subscriptionItem = SubscriptionItem(channelGroup)
+ heartbeatGroups[channelGroup] = subscriptionItem
+ } else {
+ heartbeatGroups.remove(channelGroup)
+ }
+ }
+ }
+
+ @Synchronized
+ fun createStatePayload(): Map {
+ val stateResponse: HashMap = hashMapOf()
+ for (channel in channels.values) {
+ if (channel.state != null) {
+ stateResponse[channel.name] = channel.state
+ }
+ }
+ for (channelGroup in groups.values) {
+ if (channelGroup.state != null) {
+ stateResponse[channelGroup.name] = channelGroup.state
+ }
+ }
+ return stateResponse
+ }
+
+ @Synchronized
+ fun prepareChannelList(includePresence: Boolean): List {
+ return prepareMembershipList(channels, presenceChannels, includePresence)
+ }
+
+ @Synchronized
+ fun prepareChannelGroupList(includePresence: Boolean): List {
+ return prepareMembershipList(groups, presenceGroups, includePresence)
+ }
+
+ @Synchronized
+ fun prepareHeartbeatChannelList(includePresence: Boolean): List {
+ return prepareMembershipList(heartbeatChannels, presenceChannels, includePresence)
+ }
+
+ @Synchronized
+ fun prepareHeartbeatChannelGroupList(includePresence: Boolean): List {
+ return prepareMembershipList(heartbeatGroups, presenceGroups, includePresence)
+ }
+
+ @Synchronized
+ fun isEmpty(): Boolean {
+ return channels.isEmpty() && presenceChannels.isEmpty() && groups.isEmpty() && presenceGroups.isEmpty()
+ }
+
+ @Synchronized
+ private fun prepareMembershipList(
+ dataStorage: Map,
+ presenceStorage: Map,
+ includePresence: Boolean
+ ): List {
+ val response: MutableList = ArrayList()
+ for (channelGroupItem in dataStorage.values) {
+ response.add(channelGroupItem.name)
+ }
+ if (includePresence) {
+ for (presenceChannelGroupItem in presenceStorage.values) {
+ response.add(presenceChannelGroupItem.name + "-pnpres")
+ }
+ }
+ return response
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt b/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt
new file mode 100644
index 000000000..9a646c270
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt
@@ -0,0 +1,394 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.PubNub
+import com.pubnub.api.builder.PresenceOperation
+import com.pubnub.api.builder.StateOperation
+import com.pubnub.api.builder.SubscribeOperation
+import com.pubnub.api.builder.UnsubscribeOperation
+import com.pubnub.api.callbacks.ReconnectionCallback
+import com.pubnub.api.callbacks.SubscribeCallback
+import com.pubnub.api.endpoints.presence.Heartbeat
+import com.pubnub.api.endpoints.presence.Leave
+import com.pubnub.api.endpoints.pubsub.Subscribe
+import com.pubnub.api.enums.PNHeartbeatNotificationOptions
+import com.pubnub.api.enums.PNOperationType
+import com.pubnub.api.enums.PNStatusCategory
+import com.pubnub.api.models.consumer.PNStatus
+import com.pubnub.api.models.server.SubscribeMessage
+import com.pubnub.api.workers.SubscribeMessageWorker
+import java.util.*
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.concurrent.timerTask
+
+class SubscriptionManager(val pubnub: PubNub) {
+
+ private companion object {
+ private const val HEARTBEAT_INTERVAL_MULTIPLIER = 1000L
+ }
+
+ private var subscribeCall: Subscribe? = null
+ private var heartbeatCall: Heartbeat? = null
+
+ private var messageQueue: LinkedBlockingQueue = LinkedBlockingQueue()
+
+ private var duplicationManager: DuplicationManager = DuplicationManager(pubnub.configuration)
+
+ /**
+ * Store the latest timetoken to subscribe with, null by default to get the latest timetoken.
+ */
+ private var timetoken = 0L
+ private var storedTimetoken: Long? = null // when changing the channel mix, store the timetoken for a later date.
+
+ /**
+ * Keep track of Region to support PSV2 specification.
+ */
+ private var region: String? = null
+
+ /**
+ * Timer for heartbeat operations.
+ */
+ private var heartbeatTimer: Timer? = null
+
+ private val subscriptionState = StateManager()
+ internal val listenerManager = ListenerManager(pubnub)
+ private val reconnectionManager = ReconnectionManager(pubnub)
+
+ private var consumerThread: Thread? = null
+
+ /**
+ * lever to indicate if an announcement to the user about the subscription should be made.
+ * the announcement happens only after the channel mix has been changed.
+ */
+ private var subscriptionStatusAnnounced: Boolean = false
+
+ init {
+
+ reconnectionManager.reconnectionCallback = object : ReconnectionCallback() {
+ override fun onReconnection() {
+ reconnect()
+ listenerManager.announce(
+ PNStatus(
+ category = PNStatusCategory.PNReconnectedCategory,
+ operation = PNOperationType.PNSubscribeOperation,
+ error = false,
+ affectedChannels = subscriptionState.prepareChannelList(true),
+ affectedChannelGroups = subscriptionState.prepareChannelGroupList(true)
+ )
+ )
+ subscriptionStatusAnnounced = true
+ }
+
+ override fun onMaxReconnectionExhaustion() {
+ listenerManager.announce(
+ PNStatus(
+ category = PNStatusCategory.PNReconnectionAttemptsExhausted,
+ operation = PNOperationType.PNSubscribeOperation,
+ error = false,
+ affectedChannels = subscriptionState.prepareChannelList(true),
+ affectedChannelGroups = subscriptionState.prepareChannelGroupList(true)
+ )
+ )
+
+ disconnect()
+ }
+ }
+
+ if (pubnub.configuration.startSubscriberThread) {
+ consumerThread = Thread(
+ SubscribeMessageWorker(
+ pubnub,
+ listenerManager,
+ messageQueue,
+ duplicationManager
+ )
+ )
+ consumerThread?.name = "Subscription Manager Consumer Thread"
+ consumerThread?.start()
+ }
+ }
+
+ @Synchronized
+ fun getSubscribedChannels(): List {
+ return subscriptionState.prepareChannelList(false)
+ }
+
+ @Synchronized
+ fun getSubscribedChannelGroups(): List {
+ return subscriptionState.prepareChannelGroupList(false)
+ }
+
+ @Synchronized
+ internal fun adaptStateBuilder(stateOperation: StateOperation?) {
+ subscriptionState.adaptStateBuilder(stateOperation!!)
+ reconnect()
+ }
+
+ @Synchronized
+ internal fun adaptSubscribeBuilder(subscribeOperation: SubscribeOperation) {
+ subscriptionState.adaptSubscribeBuilder(subscribeOperation)
+
+ // the channel mix changed, on the successful subscribe, there is going to be announcement.
+ subscriptionStatusAnnounced = false
+ duplicationManager.clearHistory()
+
+ timetoken = subscribeOperation.timetoken
+
+ // if the timetoken is not at starting position, reset the timetoken to get a connected event
+ // and store the old timetoken to be reused later during subscribe.
+ if (timetoken != 0L) {
+ storedTimetoken = timetoken
+ }
+ timetoken = 0L
+ reconnect()
+ }
+
+ @Synchronized
+ fun reconnect() {
+ startSubscribeLoop()
+ registerHeartbeatTimer()
+ }
+
+ @Synchronized
+ fun disconnect() {
+ heartbeatTimer?.cancel()
+ stopSubscribeLoop()
+ }
+
+ private fun registerHeartbeatTimer() {
+ // make sure only one timer is running at a time.
+ heartbeatTimer?.cancel()
+
+ // if the interval is 0 or less, do not start the timer
+ if (pubnub.configuration.heartbeatInterval <= 0) {
+ return
+ }
+
+ heartbeatTimer = Timer()
+ heartbeatTimer?.schedule(
+ timerTask {
+ performHeartbeatLoop()
+ },
+ 0,
+ pubnub.configuration.heartbeatInterval * HEARTBEAT_INTERVAL_MULTIPLIER
+ )
+ }
+
+ private fun performHeartbeatLoop() {
+ heartbeatCall?.silentCancel()
+ val presenceChannels = subscriptionState.prepareChannelList(false)
+ val presenceChannelGroups =
+ subscriptionState.prepareChannelGroupList(false)
+ val heartbeatChannels =
+ subscriptionState.prepareHeartbeatChannelList(false)
+ val heartbeatChannelGroups =
+ subscriptionState.prepareHeartbeatChannelGroupList(false)
+ // do not start the loop if we do not have any presence channels or channel groups enabled.
+ if (presenceChannels.isEmpty()
+ && presenceChannelGroups.isEmpty()
+ && heartbeatChannels.isEmpty()
+ && heartbeatChannelGroups.isEmpty()
+ ) {
+ return
+ }
+ val channels: MutableList = ArrayList()
+ channels.addAll(presenceChannels)
+ channels.addAll(heartbeatChannels)
+ val groups: MutableList = ArrayList()
+ groups.addAll(presenceChannelGroups)
+ groups.addAll(heartbeatChannelGroups)
+ heartbeatCall = Heartbeat(pubnub, channels, groups)
+ heartbeatCall?.async { _, status ->
+ val heartbeatVerbosity = pubnub.configuration.heartbeatNotificationOptions
+
+ if (status.error) {
+ if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL
+ || heartbeatVerbosity == PNHeartbeatNotificationOptions.FAILURES
+ ) {
+ listenerManager.announce(status)
+ }
+ // stop the heartbeating logic since an error happened.
+ heartbeatTimer?.cancel()
+ } else {
+ if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL) {
+ listenerManager.announce(status)
+ }
+ }
+ }
+ }
+
+ private fun startSubscribeLoop() {
+ // this function can be called from different points, make sure any old loop is closed
+ stopSubscribeLoop()
+
+ val combinedChannels = subscriptionState.prepareChannelList(true)
+ val combinedChannelGroups = subscriptionState.prepareChannelGroupList(true)
+ val stateStorage = subscriptionState.createStatePayload()
+
+ // do not start the subscribe loop if we have no channels to subscribe to.
+ if (combinedChannels.isEmpty() && combinedChannelGroups.isEmpty()) {
+ return
+ }
+
+ subscribeCall = Subscribe(pubnub).apply {
+ channels = combinedChannels
+ channelGroups = combinedChannelGroups
+ timetoken = this@SubscriptionManager.timetoken
+ region = this@SubscriptionManager.region
+ pubnub.configuration.isFilterExpressionKeyValid {
+ filterExpression = this
+ }
+ state = stateStorage
+ }
+
+ subscribeCall?.async { result, status ->
+ if (status.error) {
+ if (status.category == PNStatusCategory.PNTimeoutCategory) {
+ startSubscribeLoop()
+ return@async
+ }
+
+ disconnect()
+ listenerManager.announce(status)
+
+ if (status.category == PNStatusCategory.PNUnexpectedDisconnectCategory) {
+ // stop all announcements and ask the reconnection manager to start polling for connection restoration..
+ reconnectionManager.startPolling(pubnub.configuration)
+ }
+
+ return@async
+ }
+
+ if (!subscriptionStatusAnnounced) {
+ val pnStatus = createPublicStatus(status).apply {
+ category = PNStatusCategory.PNConnectedCategory
+ error = false
+ }
+ subscriptionStatusAnnounced = true
+ listenerManager.announce(pnStatus)
+ }
+
+ pubnub.configuration.requestMessageCountThreshold?.let {
+ // todo default value of size if all ?s are null
+ if (it <= result!!.messages.size) {
+ listenerManager.announce(
+ createPublicStatus(status).apply {
+ category = PNStatusCategory.PNRequestMessageCountExceededCategory
+ error = false
+ }
+ )
+ }
+ }
+
+ if (result!!.messages.isNotEmpty()) {
+ messageQueue.addAll(result.messages)
+ }
+
+ if (storedTimetoken != null) {
+ timetoken = storedTimetoken!!
+ storedTimetoken = null
+ } else {
+ timetoken = result.metadata.timetoken
+ }
+
+ region = result.metadata.region
+ startSubscribeLoop()
+
+ }
+
+ }
+
+ private fun stopSubscribeLoop() {
+ subscribeCall?.silentCancel()
+ }
+
+ private fun stopHeartbeatLoop() {
+ heartbeatCall?.silentCancel()
+ }
+
+ internal fun adaptPresenceBuilder(presenceOperation: PresenceOperation) {
+ subscriptionState.adaptPresenceBuilder(presenceOperation)
+
+ if (!pubnub.configuration.suppressLeaveEvents && !presenceOperation.connected) {
+ Leave(pubnub).apply {
+ channels = presenceOperation.channels
+ channelGroups = presenceOperation.channelGroups
+ }.async { _, status ->
+ listenerManager.announce(status)
+ }
+ }
+
+ registerHeartbeatTimer()
+ }
+
+ @Synchronized
+ internal fun adaptUnsubscribeBuilder(unsubscribeOperation: UnsubscribeOperation) {
+ subscriptionState.adaptUnsubscribeBuilder(unsubscribeOperation)
+ subscriptionStatusAnnounced = false
+
+ if (!pubnub.configuration.suppressLeaveEvents) {
+ Leave(pubnub).apply {
+ channels = unsubscribeOperation.channels
+ channelGroups = unsubscribeOperation.channelGroups
+ }.async { _, status ->
+ listenerManager.announce(status)
+ }
+ }
+
+ // if we unsubscribed from all the channels, reset the timetoken back to zero and remove the region.
+ if (subscriptionState.isEmpty()) {
+ region = null
+ storedTimetoken = null
+ timetoken = 0L
+ } else {
+ storedTimetoken = timetoken
+ timetoken = 0L
+ }
+
+ reconnect()
+ }
+
+ @Synchronized
+ fun unsubscribeAll() {
+ adaptUnsubscribeBuilder(
+ UnsubscribeOperation().apply {
+ channels = subscriptionState.prepareChannelList(false)
+ channelGroups = subscriptionState.prepareChannelGroupList(false)
+ }
+ )
+ }
+
+ private fun createPublicStatus(privateStatus: PNStatus): PNStatus {
+ with(privateStatus) {
+ return PNStatus(
+ category = category,
+ error = this.error,
+ operation = operation,
+ exception = exception,
+ statusCode = statusCode,
+ tlsEnabled = tlsEnabled,
+ origin = origin,
+ uuid = uuid,
+ authKey = authKey,
+ affectedChannels = affectedChannels,
+ affectedChannelGroups = affectedChannelGroups
+ )
+ }
+ }
+
+ fun addListener(listener: SubscribeCallback) {
+ listenerManager.addListener(listener)
+ }
+
+ fun removeListener(listener: SubscribeCallback) {
+ listenerManager.removeListener(listener)
+ }
+
+ @Synchronized
+ fun destroy(forceDestroy: Boolean = false) {
+ disconnect()
+ if (forceDestroy && consumerThread != null) {
+ consumerThread!!.interrupt()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt b/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt
new file mode 100644
index 000000000..f959c1ac9
--- /dev/null
+++ b/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt
@@ -0,0 +1,120 @@
+package com.pubnub.api.managers
+
+import com.pubnub.api.enums.PNOperationType
+import java.math.RoundingMode
+import java.text.NumberFormat
+import java.util.*
+import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
+import kotlin.collections.set
+
+internal class TelemetryManager {
+
+ companion object {
+ private const val MAX_FRACTION_DIGITS = 3
+ private const val TIMESTAMP_DIVIDER = 1000
+ private const val MAXIMUM_LATENCY_DATA_AGE = 60.0
+ private const val CLEAN_UP_INTERVAL = 1
+ private const val CLEAN_UP_INTERVAL_MULTIPLIER = 1000
+ }
+
+ private var timer: Timer? = Timer()
+ private val latencies: HashMap>> = HashMap()
+ private val numberFormat by lazy {
+ NumberFormat.getNumberInstance(Locale.US).apply {
+ maximumFractionDigits = MAX_FRACTION_DIGITS
+ roundingMode = RoundingMode.HALF_UP
+ isGroupingUsed = false
+ }
+ }
+
+ init {
+ startCleanUpTimer()
+ }
+
+ @Synchronized
+ fun operationsLatency(): Map {
+ val operationLatencies = HashMap()
+ latencies.entries.forEach {
+ val latencyKey = "l_${it.key}"
+ val endpointAverageLatency = averageLatencyFromData(it.value)
+ if (endpointAverageLatency > 0.0f) {
+ operationLatencies[latencyKey] = numberFormat.format(endpointAverageLatency)
+ }
+ }
+ return operationLatencies
+ }
+
+ private fun startCleanUpTimer() {
+ val interval = (CLEAN_UP_INTERVAL * CLEAN_UP_INTERVAL_MULTIPLIER).toLong()
+
+ stopCleanUpTimer()
+ timer = Timer()
+ timer?.schedule(object : TimerTask() {
+ override fun run() {
+ cleanUpTelemetryData()
+ }
+ }, interval, interval)
+ }
+
+
+ internal fun stopCleanUpTimer() {
+ this.timer?.cancel()
+ }
+
+ @Synchronized
+ private fun cleanUpTelemetryData() {
+ val currentDate = Date().time / (TIMESTAMP_DIVIDER.toDouble())
+ val endpoints = latencies.keys.toList()
+ endpoints.forEach {
+ val outdatedLatencies = ArrayList