diff --git a/.travis.yml b/.travis.yml index 0fc7d88..bb114c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,23 +12,23 @@ cache: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ -before_install: chmod +x ./gradlew +install: chmod +x ./gradlew before_script: - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build -script: ./gradlew check +script: ./gradlew test after_script: ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT jobs: include: - stage: deploy - name: "Documentation" + name: "Documentation Deploy" jdk: openjdk11 script: ./gradlew publishDoc if: tag IS present - deploy: &pages + deploy: provider: pages repo: GlitchLib/docs target-branch: master @@ -41,17 +41,19 @@ jobs: on: tags: true - stage: deploy - name: "Upload to Bintray" + name: "Bintray Upload" jdk: openjdk8 - install: skip - script: ./gradlew bintrayUpload publishRelease -x test + script: ./gradlew bintrayUpload -x test if: tag IS present - deploy: &releases + - stage: deploy + name: "Create Release" + jdk: openjdk8 + script: ./gradlew publishRelease + if: tag IS present + deploy: provider: releases api_key: $GITHUB_TOKEN file_glob: true - file: ./build/release/*.jar - prerelease: true - skip_cleanup: true - on: - tags: true \ No newline at end of file + file: build/release/* + draft: true + skip_cleanup: true \ No newline at end of file diff --git a/README.md b/README.md index 1486baa..d3db4fa 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,6 @@ A hybrid-reactive Java wrapper for the Twitch. Supports using: * [Gson](https://github.com/google/gson) - JSON * [Project Reactor](http://projectreactor.io/) -# Module Supports -| Name | JVM | Android | -|:---:|:---:|:---:| -| [glitch-core](core) | 1.8+ | ✔ SDK 26 | -| [glitch-kraken](kraken) | 1.8+ | ✔ SDK 26 | -| [glitch-helix](helix) | 1.8+ | ✔ SDK 26 | -| [glitch-auth](auth) | 1.8+ | ✔ SDK 26 | -| [glitch-chat](chat) | 1.8+ | ❌ | -| [glitch-pubsub](pubsub) | 1.8+ | ❌ | - # Getting started Please introduce the [wiki page](https://glitchlib.github.io/wiki/getting-started/welcome/) diff --git a/all/build.gradle.kts b/all/build.gradle.kts index bc850f6..3c60e5a 100644 --- a/all/build.gradle.kts +++ b/all/build.gradle.kts @@ -37,7 +37,7 @@ tasks { sourceDirs = files(projectList.flatMap { it.sourceSets.main.get().allSource }.filter { it.name.endsWith(".kt") }.toList()) classpath = files(projectList.flatMap { it.tasks.getByName("dokka").classpath }.toList()) } - + withType { enabled = true } diff --git a/build.gradle.kts b/build.gradle.kts index ed7079f..9b6f182 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,17 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import com.jfrog.bintray.gradle.BintrayExtension import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.text.SimpleDateFormat import java.util.* plugins { + jacoco `maven-publish` + kotlin("jvm") version "1.3.21" id("com.jfrog.bintray") version "1.8.4" id("org.jetbrains.dokka") version "0.9.17" - id("org.jetbrains.kotlin.jvm") version "1.3.21" id("com.github.ben-manes.versions") version "0.20.0" - id("com.github.johnrengelman.shadow") version "4.0.3" + id("com.github.johnrengelman.shadow") version "4.0.4" id("com.gorylenko.gradle-git-properties") version "2.0.0" apply false } @@ -34,6 +34,7 @@ allprojects { subprojects { apply(plugin = "java") + apply(plugin = "jacoco") apply(plugin = "maven-publish") apply(plugin = "com.jfrog.bintray") apply(plugin = "org.jetbrains.dokka") @@ -50,7 +51,7 @@ subprojects { dependencies { if (!arrayOf("bom", "all", "auth", rootProject.name).contains(project.name)) { // https://docs.gradle.org/5.0/userguide/managing_transitive_dependencies.html#sec:bom_import - compile(enforcedPlatform("io.projectreactor:reactor-bom:Californium-SR4")) + compile(enforcedPlatform("io.projectreactor:reactor-bom:Californium-SR5")) compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") compile("org.jetbrains.kotlin:kotlin-reflect") diff --git a/chat/build.gradle.kts b/chat/build.gradle.kts index 0f1ffcd..69e40a6 100644 --- a/chat/build.gradle.kts +++ b/chat/build.gradle.kts @@ -1,7 +1,5 @@ dependencies { compileOnly(project(":core")) - compileOnly(project(":kraken")) testCompile(project(":core")) - testCompile(project(":kraken")) } \ No newline at end of file diff --git a/chat/src/main/java/glitch/chat/Colors.java b/chat/src/main/java/glitch/chat/Colors.java index 2ef750b..6600cc9 100644 --- a/chat/src/main/java/glitch/chat/Colors.java +++ b/chat/src/main/java/glitch/chat/Colors.java @@ -3,24 +3,71 @@ import java.awt.Color; /** + * Default Twitch Colors + * * @author Damian Staszewski [damian@stachuofficial.tv] * @version %I%, %G% * @since 1.0 */ public class Colors { - public static Color Red = Color.decode("#FF0000"); - public static Color Blue = Color.decode("#0000FF"); - public static Color Green = Color.decode("#00FF00"); - public static Color FireBrick = Color.decode("#B22222"); - public static Color Coral = Color.decode("#FF7F50"); - public static Color YellowGreen = Color.decode("#9ACD32"); - public static Color OrangeRed = Color.decode("#FF4500"); - public static Color SeaGreen = Color.decode("#2E8B57"); - public static Color GoldenRod = Color.decode("#DAA520"); - public static Color Chocolate = Color.decode("#D2691E"); - public static Color CadetBlue = Color.decode("#5F9EA0"); - public static Color DodgerBlue = Color.decode("#1E90FF"); - public static Color HotPink = Color.decode("#FF69B4"); - public static Color BlueViolet = Color.decode("#8A2BE2"); - public static Color SpringGreen = Color.decode("#00FF7F"); + /** + * Red + */ + public static Color RED = Color.decode("#FF0000"); + /** + * Blue + */ + public static Color BLUE = Color.decode("#0000FF"); + /** + * Green + */ + public static Color GREEN = Color.decode("#00FF00"); + /** + * Fire Brick + */ + public static Color FIRE_BRICK = Color.decode("#B22222"); + /** + * Coral + */ + public static Color CORAL = Color.decode("#FF7F50"); + /** + * Yellow Green + */ + public static Color YELLOW_GREEN = Color.decode("#9ACD32"); + /** + * Orange Red + */ + public static Color ORANGE_RED = Color.decode("#FF4500"); + /** + * Sea Green + */ + public static Color SEA_GREEN = Color.decode("#2E8B57"); + /** + * Golden Rod + */ + public static Color GOLDEN_ROD = Color.decode("#DAA520"); + /** + * Chocolate + */ + public static Color CHOCOLATE = Color.decode("#D2691E"); + /** + * Cadet Blue + */ + public static Color CADET_BLUE = Color.decode("#5F9EA0"); + /** + * Dodger Blue + */ + public static Color DODGER_BLUE = Color.decode("#1E90FF"); + /** + * Hot Pink + */ + public static Color HOT_PINK = Color.decode("#FF69B4"); + /** + * Blue Violet + */ + public static Color BLUE_VIOLET = Color.decode("#8A2BE2"); + /** + * Spring Green + */ + public static Color SPRING_GREEN = Color.decode("#00FF7F"); } \ No newline at end of file diff --git a/chat/src/main/java/glitch/chat/GlitchChat.java b/chat/src/main/java/glitch/chat/GlitchChat.java index d39b65a..82c645b 100644 --- a/chat/src/main/java/glitch/chat/GlitchChat.java +++ b/chat/src/main/java/glitch/chat/GlitchChat.java @@ -9,10 +9,10 @@ import glitch.chat.events.IRCEvent; import glitch.chat.exceptions.AlreadyJoinedChannelException; import glitch.chat.exceptions.NotJoinedChannelException; +import glitch.chat.irc.TmiConverter; import glitch.service.ISocketService; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import okhttp3.logging.HttpLoggingInterceptor; @@ -57,7 +57,7 @@ private GlitchChat( this.client = client; this.ws = WebSocket.builder(this) .addInterceptor(new HttpLoggingInterceptor(LOG::debug).setLevel(HttpLoggingInterceptor.Level.BASIC)) - .setEventConverter(TmiConverter.create()) + .setEventConverter(new TmiConverter()) .setEventProcessor(eventProcessor) .setEventScheduler(scheduler) .build((secure) ? URI_SECURE : URI); @@ -69,7 +69,7 @@ private GlitchChat( this.ws.onEvent(PingEvent.class) .subscribe(ping -> { if (!configuration.isDisableAutoPing()) { - this.ws.send(Mono.just("PONG :tmi.twitch.tv")); + this.ws.send(Mono.just("PONG :tmi.twitch.tv")).subscribe(); } }); } @@ -89,10 +89,7 @@ public GlitchClient getClient() { @Override public Mono login() { - // TODO: Whispers -// if (!channels.contains("jtv")) { -// channels.add("jtv"); -// } +// channels.add("jtv"); return this.ws.connect() .then(doInit()); @@ -128,6 +125,11 @@ public > Flux onEvent(Class type) { return this.ws.onEvent(type); } + @Override + public Flux> onEvents() { + return this.ws.onEvents(); + } + public Mono sendChannel(String channel, Publisher message) throws NotJoinedChannelException { final String fchannel = (channel.startsWith("#")) ? channel.substring(1) : channel; @@ -246,12 +248,12 @@ public Mono buildAsync() { }); } - public GlitchChat build() throws ExecutionException, InterruptedException, RuntimeException { + public GlitchChat build() { return new CompletableFuture() { { buildAsync().subscribe(this::complete); } - }.get(); + }.join(); } private Mono createBotConfig() { diff --git a/chat/src/main/java/glitch/chat/TmiConverter.java b/chat/src/main/java/glitch/chat/TmiConverter.java deleted file mode 100644 index 1595b6f..0000000 --- a/chat/src/main/java/glitch/chat/TmiConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -package glitch.chat; - -import glitch.api.ws.IEventConverter; -import glitch.api.ws.events.IEvent; -import glitch.chat.events.IRCEvent; - -/** - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 1.0 - */ -public class TmiConverter implements IEventConverter { - - private TmiConverter() { - } - - static TmiConverter create() { - return new TmiConverter(); - } - - @Override - public IEvent convert(GlitchChat client, String raw) { - if (raw.contains(System.lineSeparator())) { - for (String r : raw.split(System.lineSeparator())) { - return convert(client, r); - } - } - - return new IRCEvent(client, raw); - - } -} diff --git a/chat/src/main/kotlin/glitch/chat/ChatUtils.kt b/chat/src/main/kotlin/glitch/chat/ChatUtils.kt index 7be932a..23a1583 100644 --- a/chat/src/main/kotlin/glitch/chat/ChatUtils.kt +++ b/chat/src/main/kotlin/glitch/chat/ChatUtils.kt @@ -4,6 +4,8 @@ import glitch.api.ws.events.IEvent import glitch.api.ws.events.PingEvent import glitch.api.ws.events.PongEvent import glitch.chat.events.* +import glitch.chat.irc.Command +import glitch.chat.events.IRCEvent import java.util.* /** @@ -82,7 +84,7 @@ internal object ChatUtils { val username = event.prefix.nick!! val displayName = event.tags["display-name"]!! val timestamp = event.tags.sentTimestamp - val id = UUID.fromString(event.tags["id"]!!) + val id = event.tags.getInteger("id") val userId = event.tags.getLong("user-id") event.client.ws.dispatch(PrivateMessageEvent( diff --git a/chat/src/main/kotlin/glitch/chat/events/IRCEvent.kt b/chat/src/main/kotlin/glitch/chat/events/IRCEvent.kt new file mode 100644 index 0000000..0948fcc --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/events/IRCEvent.kt @@ -0,0 +1,59 @@ +package glitch.chat.events + +import glitch.api.ws.events.IEvent +import glitch.chat.GlitchChat +import glitch.chat.irc.Command +import glitch.chat.irc.Prefix +import glitch.chat.irc.Tags + +/** + * + * @param raw Raw Message, formatted into IRC + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +data class IRCEvent( + override val client: GlitchChat, + /** + * Specific IRC Command. If some commands isn't match of [commands][Command] will be return [{][Command.UNKNOWN] + * + * @return IRC Command + */ + val command: Command, + + /** + * Prefix of IRC Message + * + * @return IRC Prefix + */ + val prefix: Prefix, + + /** + * Tags parameters. Will be empty if some [Tags] doesn't even support it. + * + * @return Tag object + */ + val tags: Tags = Tags(emptyMap()), + + /** + * Middle parameters. Usually contains channel name. + * + * @return Middle parameters in [Immutable List][java.util.List] + */ + val middle: List = emptyList(), + + /** + * The leftover parameters after [Middle Parameters][middle] splitted with double dot colon (`:`). Generally it is a message of the channel, user or server. + * + * @return the leftover middle parameters splitted with double dot colon (`:`) + */ + val trailing: String? = null + +) : IEvent { + + val isActionMessage = trailing != null && trailing.matches(Regex("^\\001ACTION(.*)\\001$")) + + val formattedTrailing = trailing?.replace("\u0001ACTION", "")?.replace("\u0001", "") +} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/events/_irc.kt b/chat/src/main/kotlin/glitch/chat/events/_irc.kt deleted file mode 100644 index ad24ddd..0000000 --- a/chat/src/main/kotlin/glitch/chat/events/_irc.kt +++ /dev/null @@ -1,334 +0,0 @@ -package glitch.chat.events - -import glitch.api.objects.enums.UserType -import glitch.api.objects.json.Badge -import glitch.api.objects.json.interfaces.IDObject -import glitch.api.ws.events.IEvent -import glitch.chat.GlitchChat -import java.awt.Color -import java.time.Instant -import java.util.* - -private val booleanKeys = arrayOf("turbo", "mod", "subscriber", "emote-only", "r9k", "subs-only", "msg-param-should-share-streak") - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 0.1.0 - */ -data class Prefix(val raw: String, val nick: String?, val user: String?, val host: String) { - override fun toString(): String = raw - - companion object { - /** - * @param rawPrefix - * @return - */ - @JvmStatic - fun fromRaw(rawPrefix: String): Prefix { - if (!rawPrefix.matches(":(?:.*)tmi.twitch.tv".toRegex())) { - throw IllegalArgumentException("The RAW Prefix is invalid! PREFIX: $rawPrefix") - } - val prefix = rawPrefix.substring(1) - var nick: String? = null - var user: String? = null - val host: String - - - if (prefix.contains("@")) { - val nh = rawPrefix.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - host = nh[1] - if (nh[0].contains("!")) { - val nu = nh[0].split("!".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - nick = nu[0] - user = nu[1] - } else { - nick = nh[0] - } - } else { - host = prefix - } - - return Prefix(rawPrefix, nick, user, host) - } - } -} - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 0.1.0 - */ -class Tags(tags: Map) : Map by tags { - - fun getOrDefault(key: String, defaultValue: String) = get(key) ?: defaultValue - - fun getBoolean(key: String) = booleanKeys.any { it.equals(key, true) } - && getOrDefault(key, "0") == "1" - - fun getInteger(key: String): Int = getOrDefault(key, "0").toInt() - fun getLong(key: String): Long = getOrDefault(key, "0").toLong() - - val badges = get("badges")?.split(',') - ?.map { Badge(it.split('/')[0], it.split('/')[1].toInt()) } - ?.toSet() - .orEmpty() - - val emotes = get("emotes")?.split('/') - ?.map { - val index = it.split(':') - val pairSet = index[1].split(',').map { it.split('-') } - .map { it[0].toInt() to it[1].toInt() }.toSet() - return@map Emote(index[0].toInt(), pairSet) - }.orEmpty() - - val emoteSets = get("emote-sets")?.split(',') - ?.map { it.toInt() } - .orEmpty() - - val sentTimestamp = if (containsKey("tmi-sent-ts") && get("tmi-sent-ts") != null) Date(get("tmi-sent-ts")!!.toLong()).toInstant() else Instant.now() - - val broadcasterLanguage = if (containsKey("broadcast-lang") && get("broadcast-lang") != null) - Locale.forLanguageTag(get("broadcast-lang")) else null - - val userType: UserType = if (containsKey("user-type") && get("user-type") != null) - UserType.from(get("broadcast-lang")) else UserType.USER - - val color = if (containsKey("color") && get("color") != null) - Color.decode(get("color")) else null -} - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 0.1.0 - */ -enum class Command { - UNKNOWN, - PRIV_MSG, - NOTICE, - PING, - PONG, - JOIN, - PART, - HOST_TARGET, - CLEAR_CHAT, - CLEAR_MESSAGE, - USER_STATE, - GLOBAL_USER_STATE, - NICK, - PASS, - CAP, - RPL_WELCOME, - RPL_YOURHOST, - RPL_CREATED, - RPL_MYINFO, - RPL_NAMREPLY, - RPL_ENDOFNAMES, - RPL_MOTD, - RPL_MOTDSTART, - RPL_ENDOFMOTD, - ERR_UNKNOWNCOMMAND, - WHISPER, - ROOM_STATE, - RECONNECT, - SERVER_CHANGE, - USER_NOTICE; - - internal var value = if (name.startsWith("RPL_")) name.substring(4) else name - - override fun toString(): String { - return value - } - - companion object { - - fun of(cmd: String): Command { - when (cmd) { - "PRIVMSG" -> return PRIV_MSG - "NOTICE" -> return NOTICE - "PING" -> return PING - "PONG" -> return PONG - "HOSTTARGET" -> return HOST_TARGET - "CLEARCHAT" -> return CLEAR_CHAT - "USERSTATE" -> return USER_STATE - "GLOBALUSERSTATE" -> return GLOBAL_USER_STATE - "NICK" -> return NICK - "JOIN" -> return JOIN - "PART" -> return PART - "PASS" -> return PASS - "CAP" -> return CAP - "001" -> return RPL_WELCOME - "002" -> return RPL_YOURHOST - "003" -> return RPL_CREATED - "004" -> return RPL_MYINFO - "353" -> return RPL_NAMREPLY - "366" -> return RPL_ENDOFNAMES - "372" -> return RPL_MOTD - "375" -> return RPL_MOTDSTART - "376" -> return RPL_ENDOFMOTD - "421" -> return ERR_UNKNOWNCOMMAND - "WHISPER" -> return WHISPER - "SERVERCHANGE" -> return SERVER_CHANGE - "RECONNECT" -> return RECONNECT - "ROOMSTATE" -> return ROOM_STATE - "USERNOTICE" -> return USER_NOTICE - "CLEARMSG" -> return CLEAR_MESSAGE - else -> { - val com = UNKNOWN - com.value = cmd - return com - } - } - } - } -} - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 0.1.0 - */ -data class Emote( - override val id: Int, - val indexRanges: Set> -) : IDObject { - - fun getEmoteUrl(emoteSize: Emote.Size): String { - return String.format("http://static-cdn.jtvnw.net/emoticons/v1/%d/%s", id, emoteSize.value) - } - - enum class Size private constructor(internal val value: Double) { - X1(1.0), - X2(2.0), - X3(3.0) - } -} - -/** - * - * @param raw Raw Message, formatted into IRC - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 1.0 - */ -data class IRCEvent( - override val client: GlitchChat, - val raw: String -) : IEvent { - - /** - * Tags parameters. Will be empty if some [Tags] doesn't even support it. - * - * @return Tag object - */ - val tags: Tags - - /** - * Prefix of IRC Message - * - * @return IRC Prefix - */ - val prefix: Prefix - - /** - * Specific IRC Command. If some commands isn't match of [commands][Command] will be return [{][Command.UNKNOWN] - * - * @return IRC Command - */ - val command: Command - - /** - * Middle parameters. Usually contains channel name. - * - * @return Middle parameters in [Immutable List][java.util.List] - */ - val middle: List - - /** - * The leftover parameters after [Middle Parameters][middle] splitted with double dot colon (`:`). Generally it is a message of the channel, user or server. - * - * @return the leftover middle parameters splitted with double dot colon (`:`) - */ - val trailing: String? - - init { - val split = raw.split(Regex("\\s:"), 3) - .map { if (it.startsWith(':')) it.substring(1).trim() else it } - .toMutableList() - - - tags = Tags(if (split[0].startsWith('@')) { - split[0].substring(1).trim().split(';').map { - val p = it.split('=') - return@map Pair(p[0], valueParse(p[1])) - }.toMap() - } else { - emptyMap() - }) - - split[0] = split[1].trim() - - if (split.size > 2) { - split[1] = split[2].trim() - } - - trailing = split[1] - - val mid = split[0].split(Regex("\\s"), 3) - .map { if (it.startsWith(":")) it.substring(1) else it } - - prefix = Prefix.fromRaw(":" + mid.first { it.matches(Regex("^(.+)(!.+)*?(@.+)*?$")) }) - command = Command.of(mid.first { it.matches(Regex("^([A-Z]+|[0-9]{1,3})")) }) - middle = mid.joinToString(" ").replace(prefix.raw, "") - .replace(command.toString(), "").replace(":", "").trim() - .split(' ') - } - - val isActionMessage = trailing != null && trailing.matches(Regex("^\\001ACTION(.*)\\001$")) - - val formattedTrailing = trailing?.replace("\u0001ACTION", "")?.replace("\u0001", "") - - private fun valueParse(value: String?): String? { - var v = value - if (v != null) { - if (v == "") return null - if (v.contains("\\r")) v = v.replace("\\r", "\r") - if (v.contains("\\n")) v = v.replace("\\n", "\n") - if (v.contains("\\\\")) v = v.replace("\\\\", "\\") - if (v.contains("\\s")) v = v.replace("\\s", " ") - if (v.contains("\\:")) v = v.replace("\\:", ":") - } - - return value - } - - override fun toString(): String = raw -} - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 1.0 - */ -data class RoomState( - override val id: Long, - var broadcasterLanguage: Locale?, - var isEmoteOnly: Boolean, - var follow: Long, - var isR9k: Boolean, - var slow: Long, - var isSubsOnly: Boolean -) : IDObject { - val isFollowersOnly: Boolean - get() = follow != -1L - - val isSlowMode: Boolean - get() = slow > 0 -} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/events/ordinal.kt b/chat/src/main/kotlin/glitch/chat/events/ordinal.kt index adcde77..283ab40 100644 --- a/chat/src/main/kotlin/glitch/chat/events/ordinal.kt +++ b/chat/src/main/kotlin/glitch/chat/events/ordinal.kt @@ -7,7 +7,6 @@ import glitch.api.ws.events.IEvent import glitch.chat.GlitchChat import java.awt.Color import java.time.Instant -import java.util.* /** * @@ -17,7 +16,7 @@ import java.util.* */ data class PrivateMessageEvent( override val client: GlitchChat, - override val id: UUID, + override val id: Int, override val badges: Set, override val color: Color, override val username: String, @@ -26,7 +25,7 @@ data class PrivateMessageEvent( override val userType: UserType, override val message: String?, override val createdAt: Instant -) : IUser, IMessage, GlobalUserState, IEvent, IDObject { +) : IUser, IMessage, GlobalUserState, IEvent, IDObject { // fun reply(message: String) = user.send(message) } diff --git a/chat/src/main/kotlin/glitch/chat/irc/Command.kt b/chat/src/main/kotlin/glitch/chat/irc/Command.kt new file mode 100644 index 0000000..1bb2102 --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/Command.kt @@ -0,0 +1,84 @@ +package glitch.chat.irc + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 0.1.0 + */ +enum class Command { + UNKNOWN, + PRIV_MSG, + NOTICE, + PING, + PONG, + JOIN, + PART, + HOST_TARGET, + CLEAR_CHAT, + CLEAR_MESSAGE, + USER_STATE, + GLOBAL_USER_STATE, + NICK, + PASS, + CAP, + RPL_WELCOME, + RPL_YOURHOST, + RPL_CREATED, + RPL_MYINFO, + RPL_NAMREPLY, + RPL_ENDOFNAMES, + RPL_MOTD, + RPL_MOTDSTART, + RPL_ENDOFMOTD, + ERR_UNKNOWNCOMMAND, + WHISPER, + ROOM_STATE, + RECONNECT, + SERVER_CHANGE, + USER_NOTICE; + + internal var value = if (name.startsWith("RPL_")) name.substring(4) else name + + override fun toString(): String { + return value + } + + companion object { + + fun of(cmd: String) = + when (cmd) { + "PRIVMSG" -> PRIV_MSG + "NOTICE" -> NOTICE + "PING" -> PING + "PONG" -> PONG + "HOSTTARGET" -> HOST_TARGET + "CLEARCHAT" -> CLEAR_CHAT + "USERSTATE" -> USER_STATE + "GLOBALUSERSTATE" -> GLOBAL_USER_STATE + "NICK" -> NICK + "JOIN" -> JOIN + "PART" -> PART + "PASS" -> PASS + "CAP" -> CAP + "001" -> RPL_WELCOME + "002" -> RPL_YOURHOST + "003" -> RPL_CREATED + "004" -> RPL_MYINFO + "353" -> RPL_NAMREPLY + "366" -> RPL_ENDOFNAMES + "372" -> RPL_MOTD + "375" -> RPL_MOTDSTART + "376" -> RPL_ENDOFMOTD + "421" -> ERR_UNKNOWNCOMMAND + "WHISPER" -> WHISPER + "SERVERCHANGE" -> SERVER_CHANGE + "RECONNECT" -> RECONNECT + "ROOMSTATE" -> ROOM_STATE + "USERNOTICE" -> USER_NOTICE + "CLEARMSG" -> CLEAR_MESSAGE + else -> UNKNOWN.apply { value = cmd } + + } + } +} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/irc/Emote.kt b/chat/src/main/kotlin/glitch/chat/irc/Emote.kt new file mode 100644 index 0000000..e3a8272 --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/Emote.kt @@ -0,0 +1,25 @@ +package glitch.chat.irc + +import glitch.api.objects.json.interfaces.IDObject + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 0.1.0 + */ +data class Emote( + override val id: Int, + val indexRanges: Set> +) : IDObject { + + fun getEmoteUrl(emoteSize: Emote.Size): String { + return String.format("http://static-cdn.jtvnw.net/emoticons/v1/%d/%s", id, emoteSize.value) + } + + enum class Size private constructor(internal val value: Double) { + X1(1.0), + X2(2.0), + X3(3.0) + } +} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/irc/Prefix.kt b/chat/src/main/kotlin/glitch/chat/irc/Prefix.kt new file mode 100644 index 0000000..009e56a --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/Prefix.kt @@ -0,0 +1,46 @@ +package glitch.chat.irc + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 0.1.0 + */ +data class Prefix(val raw: String, val nick: String? = null, val user: String? = null, val host: String) { + override fun toString(): String = raw + + companion object { + /** + * @param rawPrefix + * @return + */ + fun fromRaw(rawPrefix: String): Prefix { + if (!rawPrefix.matches(":(?:.*)tmi.twitch.tv".toRegex())) { + throw IllegalArgumentException("The RAW Prefix is invalid! PREFIX: $rawPrefix") + } + val prefix = rawPrefix.substring(1) + var nick: String? = null + var user: String? = null + val host: String + + + if (prefix.contains("@")) { + val nh = rawPrefix.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + host = nh[1] + if (nh[0].contains("!")) { + val nu = nh[0].split("!".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + nick = nu[0] + user = nu[1] + } else { + nick = nh[0] + } + } else { + host = prefix + } + + return Prefix(rawPrefix, nick, user, host) + } + + fun empty() = Prefix(":tmi.twitch.tv", host = "tmi.twitch.tv") + } +} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/irc/RoomState.kt b/chat/src/main/kotlin/glitch/chat/irc/RoomState.kt new file mode 100644 index 0000000..b26eaf8 --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/RoomState.kt @@ -0,0 +1,26 @@ +package glitch.chat.irc + +import glitch.api.objects.json.interfaces.IDObject +import java.util.* + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +data class RoomState( + override val id: Long, + var broadcasterLanguage: Locale?, + var isEmoteOnly: Boolean, + var follow: Long, + var isR9k: Boolean, + var slow: Long, + var isSubsOnly: Boolean +) : IDObject { + val isFollowersOnly: Boolean + get() = follow != -1L + + val isSlowMode: Boolean + get() = slow > 0 +} \ No newline at end of file diff --git a/chat/src/main/kotlin/glitch/chat/irc/Tags.kt b/chat/src/main/kotlin/glitch/chat/irc/Tags.kt new file mode 100644 index 0000000..f1d3299 --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/Tags.kt @@ -0,0 +1,64 @@ +package glitch.chat.irc + +import glitch.api.objects.enums.UserType +import glitch.api.objects.json.Badge +import java.awt.Color +import java.time.Instant +import java.util.* + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 0.1.0 + */ +class Tags(private val tags: Map) : Map by tags { + + private val booleanKeys = arrayOf("turbo", "mod", "subscriber", "emote-only", "r9k", "subs-only", "msg-param-should-share-streak") + + fun getOrDefault(key: String, defaultValue: String) = get(key) ?: defaultValue + + fun getBoolean(key: String) = booleanKeys.any { it.equals(key, true) } + && getOrDefault(key, "0") == "1" + + fun getInteger(key: String): Int = getOrDefault(key, "0").toInt() + fun getLong(key: String): Long = getOrDefault(key, "0").toLong() + + val badges + get() = get("badges")?.split(',') + ?.map { Badge(it.split('/')[0], it.split('/')[1].toInt()) } + ?.toSet() + .orEmpty() + + val emotes + get() = get("emotes")?.split('/') + ?.map { + val index = it.split(';') + val pairSet = index[1].split(',').map { it.split('-') } + .map { it.first().toInt() to it.last().toInt() }.toSet() + return@map Emote(index[0].toInt(), pairSet) + }.orEmpty() + + val emoteSets + get() = get("emote-sets")?.split(',') + ?.map { it.toInt() } + .orEmpty() + + val sentTimestamp + get() = if (containsKey("tmi-sent-ts") && get("tmi-sent-ts") != null) Date(get("tmi-sent-ts")!!.toLong()).toInstant() else Instant.now() + + val broadcasterLanguage + get() = if (containsKey("broadcast-lang") && get("broadcast-lang") != null) + Locale.forLanguageTag(get("broadcast-lang")) else null + + val userType: UserType + get() = if (containsKey("user-type") && get("user-type") != null) + UserType.from(get("broadcast-lang")) else UserType.USER + + val color + get() = if (containsKey("color") && get("color") != null) + Color.decode(get("color")) else null + + override fun toString() = tags.toString() +} + diff --git a/chat/src/main/kotlin/glitch/chat/irc/TmiConverter.kt b/chat/src/main/kotlin/glitch/chat/irc/TmiConverter.kt new file mode 100644 index 0000000..1b34d56 --- /dev/null +++ b/chat/src/main/kotlin/glitch/chat/irc/TmiConverter.kt @@ -0,0 +1,62 @@ +package glitch.chat.irc + +import glitch.api.ws.IEventConverter +import glitch.api.ws.events.IEvent +import glitch.chat.GlitchChat +import glitch.chat.events.IRCEvent + +class TmiConverter : IEventConverter { + + override fun convert(client: GlitchChat, raw: String): IEvent { + if (raw.contains(System.lineSeparator())) { + for (line in raw.split(System.lineSeparator())) { + return convert(client, line) + } + } + + return parse(client, raw) + } + + private fun parse(client: GlitchChat, line: String): IRCEvent { + + var pairLine = line.split(" :", limit = 3).mapIndexed { i, string -> if (i == 0) string else ":$string" } + + val tags = Tags(if (pairLine[0].startsWith('@')) pairLine[0].substring(1).split(";").map { + val p = it.split('=') + return@map Pair(p.first(), p.last() + .replace("\\:", ";") + .replace("\\s", " ") + .replace("\\\\", "\\") + .replace("\\r", "\r") + .replace("\\n", "\n")) + }.toMap() else emptyMap()) + + if (tags.isNotEmpty()) { + pairLine = pairLine.drop(1) + } + + val trailing = if (pairLine.size > 1) pairLine[1] else null + + val mid = pairLine[0].split(' ') + + var prefix = Prefix.empty() + var command = Command.UNKNOWN + var middle = if (mid.isEmpty()) emptyList() else mid + + mid.forEach { s -> + when { + s.startsWith(':') -> { + prefix = Prefix.fromRaw(s) + middle = middle.filter { it != s } + } + s.matches(Regex("^([A-Z]+|[0-9]{3}|(^ACK))$")) -> { + command = Command.of(s) + middle = middle.filter { it != s } + } + } + } + + return IRCEvent(client, command, prefix, tags, middle, trailing) + } + +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 06594f7..8313c31 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } dependencies { - compile("org.apache.commons:commons-collections4:4.2") + compile("org.apache.commons:commons-collections4:4.3") compile("com.squareup.okhttp3:okhttp:3.13.1") compile("com.squareup.okhttp3:logging-interceptor:3.13.1") @@ -16,14 +16,14 @@ dependencies { compile("io.projectreactor:reactor-core") - testCompile("com.squareup.okhttp3:mockwebserver:3.12.1") + testCompile("com.squareup.okhttp3:mockwebserver:3.13.1") } extensions.getByType().apply { keys = listOf( - "git.branch", - "git.commit.id", - "git.commit.id.abbrev", - "git.commit.id.describe" + "git.branch", + "git.commit.id", + "git.commit.id.abbrev", + "git.commit.id.describe" ) dateFormatTimeZone = "GMT" customProperty("application.name", rootProject.name) diff --git a/core/src/main/java/glitch/GitProperty.java b/core/src/main/java/glitch/GitProperty.java index 4b7890e..c10187c 100644 --- a/core/src/main/java/glitch/GitProperty.java +++ b/core/src/main/java/glitch/GitProperty.java @@ -20,12 +20,12 @@ public enum GitProperty { GIT_COMMIT_ID_ABBREV("git.commit.id.abbrev"), GIT_COMMIT_ID_DESCRIBE("git.commit.id.describe"); - private static final Properties properties = new Properties(); + private static final Properties PROPERTIES = new Properties(); static { try (InputStream inputStream = GitProperty.class.getClassLoader().getResourceAsStream("git.properties")) { if (inputStream != null) { - properties.load(inputStream); + PROPERTIES.load(inputStream); } } catch (IOException ignore) { } @@ -38,6 +38,6 @@ public enum GitProperty { } public static String get(GitProperty key) { - return properties.getProperty(key.value); + return PROPERTIES.getProperty(key.value); } } diff --git a/core/src/main/java/glitch/api/http/HttpClient.java b/core/src/main/java/glitch/api/http/HttpClient.java index fb41173..7ee9d03 100644 --- a/core/src/main/java/glitch/api/http/HttpClient.java +++ b/core/src/main/java/glitch/api/http/HttpClient.java @@ -19,9 +19,7 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; +import okhttp3.*; import okhttp3.logging.HttpLoggingInterceptor; import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; @@ -95,13 +93,19 @@ public final HttpRequest create(HttpMethod method, String endpoint) { * @return Non throwing response if any subjects are be fulfilled. If it throws exceptions will be return {@code null} */ public final Mono exchange(HttpRequest request) { - return Mono.create(sink -> { - try { - sink.success(doResponse(request, httpClient.newCall(doRequest(request)).execute())); - } catch (IOException e) { - sink.error(e); - } - }); + return Mono.create(sink -> + httpClient.newCall(doRequest(request)).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + sink.error(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + sink.success(doResponse(request, response)); + } + }) + ); } /** @@ -265,7 +269,7 @@ public HttpClient build() { }); } - HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(LOG::debug).setLevel(HttpLoggingInterceptor.Level.HEADERS); + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(LOG::debug).setLevel(HttpLoggingInterceptor.Level.BASIC); interceptor.redactHeader("Client-ID"); interceptor.redactHeader("Authorization"); diff --git a/core/src/main/java/glitch/api/http/HttpRequest.java b/core/src/main/java/glitch/api/http/HttpRequest.java index b5f91b0..ad82f32 100644 --- a/core/src/main/java/glitch/api/http/HttpRequest.java +++ b/core/src/main/java/glitch/api/http/HttpRequest.java @@ -16,14 +16,20 @@ import org.apache.commons.collections4.MultiValuedMap; /** - * HTTP Request created by {@code {@link HttpClient#create(HttpMethod, String, Class)}}. + * HTTP Request created by {@code {@link Routes#newRequest(Object...)}}. * This is a ordinal Request Builder. * * @author Damian Staszewski [damian@stachuofficial.tv] * @version %I%, %G% + * @see glitch.api.http.Routes#get(String) + * @see glitch.api.http.Routes#post(String) + * @see glitch.api.http.Routes#put(String) + * @see glitch.api.http.Routes#patch(String) + * @see glitch.api.http.Routes#delete(String) + * @see glitch.api.http.Routes#options(String) * @since 1.0 */ -public class HttpRequest extends Object { +public class HttpRequest { final HttpMethod method; final String endpoint; final MultiValuedMap headers = MultiMapUtils.newSetValuedHashMap(); diff --git a/core/src/main/java/glitch/api/http/Routes.java b/core/src/main/java/glitch/api/http/Routes.java index d313c59..801d5ab 100644 --- a/core/src/main/java/glitch/api/http/Routes.java +++ b/core/src/main/java/glitch/api/http/Routes.java @@ -14,31 +14,31 @@ private Routes(HttpMethod method, String endpoint) { this.endpoint = endpoint; } - public static Routes create(HttpMethod method, String endpoint) { + public static Routes create(HttpMethod method, String endpoint) { return new Routes(method, endpoint); } - public static Routes get(String endpoint) { + public static Routes get(String endpoint) { return create(HttpMethod.GET, endpoint); } - public static Routes post(String endpoint) { + public static Routes post(String endpoint) { return create(HttpMethod.POST, endpoint); } - public static Routes put(String endpoint) { + public static Routes put(String endpoint) { return create(HttpMethod.PUT, endpoint); } - public static Routes patch(String endpoint) { + public static Routes patch(String endpoint) { return create(HttpMethod.PATCH, endpoint); } - public static Routes delete(String endpoint) { + public static Routes delete(String endpoint) { return create(HttpMethod.DELETE, endpoint); } - public static Routes options(String endpoint) { + public static Routes options(String endpoint) { return create(HttpMethod.OPTIONS, endpoint); } diff --git a/core/src/main/java/glitch/api/objects/enums/VideoType.java b/core/src/main/java/glitch/api/objects/enums/VideoType.java index cad5198..b56739b 100644 --- a/core/src/main/java/glitch/api/objects/enums/VideoType.java +++ b/core/src/main/java/glitch/api/objects/enums/VideoType.java @@ -1,5 +1,5 @@ package glitch.api.objects.enums; public enum VideoType { - ARCHIVE, HIGHLIGHT, UPLOAD, All + ARCHIVE, HIGHLIGHT, UPLOAD, ALL } \ No newline at end of file diff --git a/core/src/main/java/glitch/api/ws/WebSocket.java b/core/src/main/java/glitch/api/ws/WebSocket.java index 8982e61..42634d1 100644 --- a/core/src/main/java/glitch/api/ws/WebSocket.java +++ b/core/src/main/java/glitch/api/ws/WebSocket.java @@ -134,6 +134,12 @@ public Mono send(Publisher message) { .then(); } + public Flux> onEvents() { + return eventProcessor.publishOn(eventScheduler) + .log("glitch.ws.events.ALL", Level.FINE, + SignalType.ON_NEXT, SignalType.ON_SUBSCRIBE, SignalType.ON_ERROR, SignalType.CANCEL); + } + public static class Builder> { private final S service; @@ -169,8 +175,8 @@ public Builder setEventConverter(IEventConverter eventConverter) { return this; } - public WebSocket build(String url) { - if (!Objects.requireNonNull(url, "url == null").matches("^[wS][sS]{1,2}://(.+)")) { + public WebSocket build(String url) { + if (!Objects.requireNonNull(url, "url == null").matches("^ws(s)?://(.+)")) { throw new RequestException("URL it is not be a matched pattern!!! (ws / wss)"); } diff --git a/core/src/main/java/glitch/auth/CredentialManager.java b/core/src/main/java/glitch/auth/CredentialManager.java index 9b28c05..c81da12 100644 --- a/core/src/main/java/glitch/auth/CredentialManager.java +++ b/core/src/main/java/glitch/auth/CredentialManager.java @@ -3,16 +3,16 @@ import glitch.GlitchClient; import glitch.api.http.HttpClient; import glitch.api.http.Routes; +import glitch.api.http.Unofficial; import glitch.auth.objects.adapters.AccessTokenAdapter; -import glitch.auth.objects.adapters.ExpireInstantAdapter; import glitch.auth.objects.adapters.ValidateAdapter; import glitch.auth.objects.json.AccessToken; import glitch.auth.objects.json.Credential; +import glitch.auth.objects.json.Kraken; import glitch.auth.objects.json.Validate; import glitch.auth.store.Storage; import glitch.service.AbstractHttpService; import java.lang.reflect.Type; -import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -38,7 +38,6 @@ public CredentialManager(GlitchClient client, Storage credentialStorage) { private static Map authAdapters() { Map adapters = new LinkedHashMap<>(); - adapters.put(Instant.class, new ExpireInstantAdapter()); adapters.put(AccessToken.class, new AccessTokenAdapter()); adapters.put(Validate.class, new ValidateAdapter()); @@ -62,6 +61,25 @@ private Mono valid(String accessToken) { .header("Authorization", "OAuth " + accessToken), Validate.class); } + @Unofficial + public Mono validFromKraken(Credential credential) { + return validFromKraken(credential.getAccessToken()); + } + + @Unofficial + public Mono validFromKraken(UserCredential credential) { + return validFromKraken(credential.getAccessToken()); + } + + @Unofficial + private Mono validFromKraken(String accessToken) { + return http.exchangeAs(Routes.get("https://api.twitch.tv/kraken").newRequest() + .header("Authorization", "OAuth " + accessToken) + .header("Client-ID", getClient().getConfiguration().getClientId()) + .header("Accept", "application/vnd.twitchtv.v5+json"), KrakenToken.class) + .map(k -> k.token); + } + public Mono create(String code, String redirectUri) { return http.exchangeAs( Routes.post("/token") @@ -77,13 +95,17 @@ public Mono create(String code, String redirectUri) { } public Mono refresh(Credential credential) { + return refresh(credential.getRefreshToken()); + } + + private Mono refresh(String refreshToken) { return http.exchangeAs( Routes.post("/token") .newRequest() .queryParam("grant_type", "refresh_token") .queryParam("client_id", getClient().getConfiguration().getClientId()) .queryParam("client_secret", getClient().getConfiguration().getClientSecret()) - .queryParam("refresh", credential.getRefreshToken()), + .queryParam("refresh", refreshToken), AccessToken.class).zipWhen(this::valid, Credential::new) .cast(Credential.class) .doOnSuccess(credentialStorage::register); @@ -99,10 +121,11 @@ public Mono revoke(Credential credential) { } public Mono buildFromCredentials(UserCredential userCredential) { - return valid(userCredential) - .map(valid -> new Credential(userCredential, valid)) - .cast(Credential.class) - .doOnSuccess(credentialStorage::register); + return validFromKraken(userCredential) + .flatMap(kraken -> kraken.isValid() ? + valid(userCredential).map(valid -> new Credential(userCredential, valid, kraken)) : + refresh(userCredential.getRefreshToken()) + ).doOnSuccess(credentialStorage::register); } public AuthorizationUriBuilder buildAuthorizationUrl() { @@ -112,4 +135,16 @@ public AuthorizationUriBuilder buildAuthorizationUrl() { public Storage getCredentialStorage() { return credentialStorage; } + + private final class KrakenToken { + private final Kraken token; + + private KrakenToken(Kraken token) { + this.token = token; + } + + public Kraken getToken() { + return token; + } + } } diff --git a/core/src/main/java/glitch/auth/objects/adapters/ExpireInstantAdapter.java b/core/src/main/java/glitch/auth/objects/adapters/ExpireInstantAdapter.java deleted file mode 100644 index 163b262..0000000 --- a/core/src/main/java/glitch/auth/objects/adapters/ExpireInstantAdapter.java +++ /dev/null @@ -1,21 +0,0 @@ -package glitch.auth.objects.adapters; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import java.io.IOException; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -public final class ExpireInstantAdapter extends TypeAdapter { - @Override - public void write(JsonWriter out, Instant value) throws IOException { - long time = value.getEpochSecond() - System.currentTimeMillis(); - out.value((time > 0L) ? time : 0); - } - - @Override - public Instant read(JsonReader in) throws IOException { - return Instant.now().plus(in.nextLong(), ChronoUnit.SECONDS); - } -} diff --git a/core/src/main/java/glitch/service/AbstractHttpService.java b/core/src/main/java/glitch/service/AbstractHttpService.java index 3077c3b..dd32818 100644 --- a/core/src/main/java/glitch/service/AbstractHttpService.java +++ b/core/src/main/java/glitch/service/AbstractHttpService.java @@ -21,7 +21,7 @@ * @since 1.0 */ public abstract class AbstractHttpService implements IService { - protected final Logger LOG = LoggerFactory.getLogger(this.getClass()); + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); protected final HttpClient http; private final GlitchClient client; diff --git a/core/src/main/java/glitch/service/ISocketService.java b/core/src/main/java/glitch/service/ISocketService.java index 68b752e..88194f2 100644 --- a/core/src/main/java/glitch/service/ISocketService.java +++ b/core/src/main/java/glitch/service/ISocketService.java @@ -43,4 +43,12 @@ public interface ISocketService> extends IService { * @return events */ > Flux onEvent(Class type); + + /** + * Dispatch any events using {@link reactor.core.publisher.FluxProcessor} + * + * @return events + */ + Flux> onEvents(); + } diff --git a/core/src/main/kotlin/glitch/auth/objects/json/AccessToken.kt b/core/src/main/kotlin/glitch/auth/objects/json/AccessToken.kt index 0463760..b7ad856 100644 --- a/core/src/main/kotlin/glitch/auth/objects/json/AccessToken.kt +++ b/core/src/main/kotlin/glitch/auth/objects/json/AccessToken.kt @@ -1,17 +1,14 @@ package glitch.auth.objects.json import com.google.gson.annotations.SerializedName +import glitch.api.objects.json.interfaces.Creation import glitch.auth.GlitchScope -import java.time.Instant -interface AccessToken { +interface AccessToken : Creation { val accessToken: String val refreshToken: String - @get:SerializedName("expires_in") - val expiredAt: Instant - @get:SerializedName("scope") val scopes: Set } diff --git a/core/src/main/kotlin/glitch/auth/objects/json/Credential.kt b/core/src/main/kotlin/glitch/auth/objects/json/Credential.kt index 696c8bf..db03abe 100644 --- a/core/src/main/kotlin/glitch/auth/objects/json/Credential.kt +++ b/core/src/main/kotlin/glitch/auth/objects/json/Credential.kt @@ -8,7 +8,7 @@ import java.time.temporal.ChronoUnit data class Credential( override val accessToken: String, override val refreshToken: String, - override val expiredAt: Instant, + override val createdAt: Instant, override val scopes: Set, override val clientId: String, override val login: String, @@ -17,20 +17,22 @@ data class Credential( constructor(token: AccessToken, validate: Validate) : this( token.accessToken, token.refreshToken, - token.expiredAt, + token.createdAt, validate.scopes, validate.clientId, validate.login, validate.userId ) - constructor(token: UserCredential, validate: Validate) : this( + constructor(token: UserCredential, validate: Validate, kraken: Kraken) : this( token.accessToken, token.refreshToken, - Instant.now().plus(60, ChronoUnit.DAYS), + kraken.authorization.createdAt, validate.scopes, validate.clientId, validate.login, validate.userId ) + + val expiredAt: Instant = createdAt.plus(60, ChronoUnit.DAYS) } diff --git a/core/src/main/kotlin/glitch/auth/objects/json/impl/AccessTokenImpl.kt b/core/src/main/kotlin/glitch/auth/objects/json/impl/AccessTokenImpl.kt index 24a703d..a25e3ad 100644 --- a/core/src/main/kotlin/glitch/auth/objects/json/impl/AccessTokenImpl.kt +++ b/core/src/main/kotlin/glitch/auth/objects/json/impl/AccessTokenImpl.kt @@ -4,13 +4,11 @@ import com.google.gson.annotations.SerializedName import glitch.auth.GlitchScope import glitch.auth.objects.json.AccessToken import java.time.Instant -import java.time.temporal.ChronoUnit data class AccessTokenImpl( override val accessToken: String, override val refreshToken: String, - @field:SerializedName("expires_in") - override val expiredAt: Instant = Instant.now().plus(60, ChronoUnit.DAYS), - @field:SerializedName("scope") - override val scopes: Set + @SerializedName("scope") + override val scopes: Set, + override val createdAt: Instant = Instant.now() ) : AccessToken diff --git a/core/src/main/kotlin/glitch/auth/objects/json/impl/ValidateImpl.kt b/core/src/main/kotlin/glitch/auth/objects/json/impl/ValidateImpl.kt index 837a74c..a4a2e34 100644 --- a/core/src/main/kotlin/glitch/auth/objects/json/impl/ValidateImpl.kt +++ b/core/src/main/kotlin/glitch/auth/objects/json/impl/ValidateImpl.kt @@ -2,35 +2,10 @@ package glitch.auth.objects.json.impl import glitch.auth.GlitchScope import glitch.auth.objects.json.Validate -import java.util.* data class ValidateImpl( override val clientId: String, override val login: String, override val scopes: Set, override val userId: Long -) : Validate { - - override fun equals(o: Any?): Boolean { - if (this === o) return true - if (o !is ValidateImpl) return false - val validate = o as ValidateImpl? - return clientId == validate!!.clientId && - login == validate.login && - scopes == validate.scopes && - userId == validate.userId - } - - override fun hashCode(): Int { - return Objects.hash(clientId, login, scopes, userId) - } - - override fun toString(): String { - return "Validate{" + - "clientId='" + clientId + '\''.toString() + - ", login='" + login + '\''.toString() + - ", scopes=" + scopes + - ", userId=" + userId + - '}'.toString() - } -} +) : Validate diff --git a/core/src/main/kotlin/glitch/auth/objects/json/kraken.kt b/core/src/main/kotlin/glitch/auth/objects/json/kraken.kt new file mode 100644 index 0000000..b40aab7 --- /dev/null +++ b/core/src/main/kotlin/glitch/auth/objects/json/kraken.kt @@ -0,0 +1,30 @@ +package glitch.auth.objects.json + +import com.google.gson.annotations.SerializedName +import glitch.api.objects.json.interfaces.Creation +import glitch.api.objects.json.interfaces.Updated +import glitch.auth.GlitchScope +import java.time.Instant + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +data class Kraken( + @SerializedName("valid") + val isValid: Boolean, + val authorization: Authorization, + @SerializedName("user_name") + val username: String, + val userId: Long, + val clientId: String +) + +data class Authorization( + @SerializedName("scopes") + val scope: Set, + override val createdAt: Instant, + override val updatedAt: Instant +) : Creation, Updated \ No newline at end of file diff --git a/core/src/test/java/glitch/GlitchClientTest.java b/core/src/test/java/glitch/GlitchClientTest.java new file mode 100644 index 0000000..56de3c9 --- /dev/null +++ b/core/src/test/java/glitch/GlitchClientTest.java @@ -0,0 +1,74 @@ +package glitch; + +import glitch.auth.GlitchScope; +import glitch.auth.UserCredential; +import glitch.auth.store.CachedStorage; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.UUID; +import org.junit.Test; +import reactor.test.StepVerifier; + +import static org.junit.Assert.assertTrue; + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +public class GlitchClientTest { + public static final GlitchClient CLIENT = GlitchClient.builder() + .setClientId("fr681lv9fptltgo5r35nvqgczmgxap") + .setClientSecret("olu3ppmijinmtr7m4ncfr8fy8h21ld") + .setStorage(new CachedStorage(new LinkedHashSet<>())) + .build(); + + private final UserCredential credential = new UserCredential( + "2xb5jjejkto6man5ufii7wliwrjzjw", + "vmm5quqt1d4reo72e9vykkweemyc6ol4y4q5acb37h64ntt8ne" + ); + + @Test + public void testValidation() { + StepVerifier.create(CLIENT.getCredentialManager().valid(credential)) + .expectSubscription() + .expectNextMatches(validate -> + validate.getUserId() == 120074641L && + Objects.equals(validate.getLogin(), "sandalphon_ai") && + validate.getScopes().contains(GlitchScope.CHAT_READ) + ).expectComplete() + .verify(); + } + + @Test + public void buildCredential() { + StepVerifier.create(CLIENT.getCredentialManager().buildFromCredentials(credential)) + .expectSubscription() + .expectNextMatches(c -> + // checks if saved after subscription + CLIENT.getCredentialManager().getCredentialStorage().getById(c.getUserId()) + .blockOptional().isPresent() && + // asserting + c.getUserId() == 120074641L && + Objects.equals(c.getLogin(), "sandalphon_ai") && + c.getScopes().contains(GlitchScope.CHAT_READ) && + Objects.equals(c.getAccessToken(), credential.getAccessToken()) && + Objects.equals(c.getRefreshToken(), credential.getRefreshToken()) + ).expectComplete() + .verify(); + } + + public void buildAuthUrl() { + UUID state = UUID.randomUUID(); + + String authUrl = CLIENT.getCredentialManager().buildAuthorizationUrl().addScope(GlitchScope.CHAT_READ, GlitchScope.CHAT_EDIT) + .withForceVerify() + .withState(state.toString()) + .withRedirectUri("https://localhost/twitch") + .build(); + + assertTrue(authUrl.contains("scope=chat:read+chat:edit")); + assertTrue(authUrl.contains("redirect_uri=https://localhost/twitch")); + assertTrue(authUrl.contains("force_verify=true")); + } +} \ No newline at end of file diff --git a/core/src/test/java/glitch/test/api/http/HttpClientTest.java b/core/src/test/java/glitch/api/http/HttpClientTest.java similarity index 78% rename from core/src/test/java/glitch/test/api/http/HttpClientTest.java rename to core/src/test/java/glitch/api/http/HttpClientTest.java index bf1dab8..1b203ae 100644 --- a/core/src/test/java/glitch/test/api/http/HttpClientTest.java +++ b/core/src/test/java/glitch/api/http/HttpClientTest.java @@ -1,9 +1,7 @@ -package glitch.test.api.http; +package glitch.api.http; -import glitch.api.http.HttpClient; -import glitch.api.http.HttpRequest; -import glitch.api.http.Routes; import java.io.IOException; +import java.util.Objects; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -15,10 +13,6 @@ import org.slf4j.LoggerFactory; import reactor.test.StepVerifier; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; - /** * @author Damian Staszewski [damian@stachuofficial.tv] * @version %I%, %G% @@ -27,7 +21,7 @@ public class HttpClientTest { private static final Logger LOG = LoggerFactory.getLogger(HttpClientTest.class); - private static final MockWebServer webserver = new MockWebServer(); + private static final MockWebServer WEB_SERVER = new MockWebServer(); private HttpClient httpClient = HttpClient.builder() .withBaseUrl("http://localhost:8080/endpoint") .addHeader("Client-ID", "0123456789") @@ -35,7 +29,7 @@ public class HttpClientTest { @BeforeClass public static void setUp() { - webserver.setDispatcher(new Dispatcher() { + WEB_SERVER.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request.getPath().contains("/endpoint/test1")) { @@ -57,7 +51,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio }); try { - webserver.start(8080); + WEB_SERVER.start(8080); } catch (IOException e) { LOG.error("Cannot start web server!", e); } @@ -66,7 +60,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio @AfterClass public static void tearDown() { try { - webserver.close(); + WEB_SERVER.close(); } catch (IOException e) { LOG.error("Cannot stopping web server!", e); } @@ -81,7 +75,7 @@ public void stringResponse() { .expectNextMatches(response -> (response != null && response.isSuccessful()) && response.getStatus().getCode() == 200 && response.getHeader("Client-ID", 0).equals("0123456789") && - response.getBodyString().equals("{\"primary\": \"Tested Primary Endpoint\"}")) + Objects.equals(response.getBodyString(), "{\"primary\": \"Tested Primary Endpoint\"}")) .expectComplete() .verify(); } @@ -92,14 +86,11 @@ public void jsonResponse() { StepVerifier.create(httpClient.exchangeAs(request, TestingResponse.class)) .expectSubscription() - .expectNextMatches(body -> { - assertEquals(body.getPrimary(), "Tested Primary Endpoint"); - assertFalse(body.isSecondary()); - assertEquals(body.getTertiary(), 104L); - assertNull(body.getQuaternary()); - return true; - }) - .expectComplete() + .expectNextMatches(body -> body.getPrimary().equals("Tested Primary Endpoint") && + !body.isSecondary() && + body.getTertiary() == 104L && + body.getQuaternary() == null + ).expectComplete() .verify(); } diff --git a/core/src/test/java/glitch/api/ws/WebSocketClient.java b/core/src/test/java/glitch/api/ws/WebSocketClient.java new file mode 100644 index 0000000..ecd2ccf --- /dev/null +++ b/core/src/test/java/glitch/api/ws/WebSocketClient.java @@ -0,0 +1,54 @@ +package glitch.api.ws; + +import com.google.gson.Gson; +import glitch.GlitchClient; +import glitch.GlitchClientTest; +import glitch.api.ws.events.IEvent; +import glitch.service.ISocketService; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +public class WebSocketClient implements ISocketService { + private final WebSocket ws = WebSocket.builder(this) + .setEventConverter((client, raw) -> new MessageEvent(client, new Gson().toJsonTree(raw))) + .build("ws://demos.kaazing.com/echo"); + + @Override + public Mono login() { + return ws.connect(); + } + + @Override + public void logout() { + ws.disconnect(); + } + + @Override + public Mono retry() { + return ws.retry(); + } + + @Override + public > Flux onEvent(Class type) { + return ws.onEvent(type); + } + + @Override + public Flux> onEvents() { + return this.ws.onEvents(); + } + + @Override + public GlitchClient getClient() { + return GlitchClientTest.CLIENT; + } + + Mono send(String message) { + return ws.send(Mono.just(message)); + } +} diff --git a/core/src/test/java/glitch/api/ws/WebSocketTest.java b/core/src/test/java/glitch/api/ws/WebSocketTest.java new file mode 100644 index 0000000..f461273 --- /dev/null +++ b/core/src/test/java/glitch/api/ws/WebSocketTest.java @@ -0,0 +1,50 @@ +package glitch.api.ws; + +import java.util.stream.StreamSupport; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import reactor.test.StepVerifier; + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +public class WebSocketTest { + private static WebSocketClient ws = new WebSocketClient(); + + @BeforeClass + public static void setUp() { + ws.login().subscribe(); + } + + @AfterClass + public static void tearDown() { + ws.logout(); + } + + @Test + public void echoTest() { + ws.send("{\"hello\":\"world\"}").subscribe(ignore -> + StepVerifier.create(ws.onEvent(MessageEvent.class)) + .expectFusion() + .expectNextMatches(event -> event.getData().getAsJsonObject().getAsJsonPrimitive("hello").getAsString().equals("world")) + .expectComplete() + .verify() + ); + } + + @Test + public void arrayTest() { + ws.send("[{\"hello\":\"world\"},{\"pick\":1}]").subscribe(ignore -> + StepVerifier.create(ws.onEvent(MessageEvent.class)) + .expectFusion() + .expectNextMatches(event -> StreamSupport.stream(event.getData().getAsJsonArray().spliterator(), true) + .anyMatch(e -> e.getAsJsonObject().getAsJsonPrimitive("hello") + .getAsString().equals("world"))) + .expectComplete() + .verify() + ); + } +} \ No newline at end of file diff --git a/core/src/test/java/glitch/test/api/http/TestingResponse.java b/core/src/test/java/glitch/test/api/http/TestingResponse.java deleted file mode 100644 index 453a7bc..0000000 --- a/core/src/test/java/glitch/test/api/http/TestingResponse.java +++ /dev/null @@ -1,59 +0,0 @@ -package glitch.test.api.http; - -import java.util.Objects; - -public class TestingResponse { - private final String primary; - private final boolean secondary; - private final long tertiary; - private final Object quaternary; - - public TestingResponse(String primary, boolean secondary, long tertiary, Object quaternary) { - this.primary = primary; - this.secondary = secondary; - this.tertiary = tertiary; - this.quaternary = quaternary; - } - - public String getPrimary() { - return primary; - } - - public boolean isSecondary() { - return secondary; - } - - public long getTertiary() { - return tertiary; - } - - public Object getQuaternary() { - return quaternary; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof TestingResponse)) return false; - TestingResponse that = (TestingResponse) o; - return isSecondary() == that.isSecondary() && - getTertiary() == that.getTertiary() && - Objects.equals(getPrimary(), that.getPrimary()) && - Objects.equals(getQuaternary(), that.getQuaternary()); - } - - @Override - public int hashCode() { - return Objects.hash(getPrimary(), isSecondary(), getTertiary(), getQuaternary()); - } - - @Override - public String toString() { - return "TestingResponse{" + - "primary='" + primary + '\'' + - ", secondary=" + secondary + - ", tertiary=" + tertiary + - ", quaternary=" + quaternary + - '}'; - } -} diff --git a/core/src/test/kotlin/glitch/api/http/TestingResponse.kt b/core/src/test/kotlin/glitch/api/http/TestingResponse.kt new file mode 100644 index 0000000..60e12a7 --- /dev/null +++ b/core/src/test/kotlin/glitch/api/http/TestingResponse.kt @@ -0,0 +1,8 @@ +package glitch.api.http + +data class TestingResponse( + val primary: String, + val isSecondary: Boolean, + val tertiary: Long, + val quaternary: Any? +) diff --git a/core/src/test/kotlin/glitch/api/ws/MessageEvent.kt b/core/src/test/kotlin/glitch/api/ws/MessageEvent.kt new file mode 100644 index 0000000..fc1ed8a --- /dev/null +++ b/core/src/test/kotlin/glitch/api/ws/MessageEvent.kt @@ -0,0 +1,14 @@ +package glitch.api.ws + +import com.google.gson.JsonElement +import glitch.api.ws.events.IEvent + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +data class MessageEvent( + override val client: WebSocketClient, + val data: JsonElement +) : IEvent diff --git a/gradle.properties b/gradle.properties index d540b3c..ab01370 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.6.0 +version=0.6.1 group=io.glitchlib project_url=https\://glitchlib.github.io/ \ No newline at end of file diff --git a/kraken/src/main/java/glitch/kraken/services/ChannelService.java b/kraken/src/main/java/glitch/kraken/services/ChannelService.java index f97338c..af6e3e1 100644 --- a/kraken/src/main/java/glitch/kraken/services/ChannelService.java +++ b/kraken/src/main/java/glitch/kraken/services/ChannelService.java @@ -23,7 +23,7 @@ public class ChannelService extends AbstractHttpService { - private static final Set commercialDuration = new LinkedHashSet<>(Arrays.asList(30, 60, 90, 120, 150, 180)); + private static final Set COMMERCIAL_DURATION = new LinkedHashSet<>(Arrays.asList(30, 60, 90, 120, 150, 180)); public ChannelService(GlitchKraken rest) { super(rest.getClient(), rest.getHttpClient()); @@ -125,7 +125,7 @@ public ChannelVideosRequest getChannelVideos(Long id) { } public Mono startChannelCommercial(Long id, Credential credential, int duration) { - Map dur = Collections.singletonMap("length", commercialDuration.stream().min(Comparator.comparingInt(i -> Math.abs(i - duration))).orElse(30)); + Map dur = Collections.singletonMap("length", COMMERCIAL_DURATION.stream().min(Comparator.comparingInt(i -> Math.abs(i - duration))).orElse(30)); return Mono.just(checkRequiredScope(credential.getScopes(), GlitchScope.CHANNEL_COMMERCIAL)) .flatMap(b -> { diff --git a/pubsub/build.gradle.kts b/pubsub/build.gradle.kts index 8abef7e..734ddd3 100644 --- a/pubsub/build.gradle.kts +++ b/pubsub/build.gradle.kts @@ -1,8 +1,4 @@ dependencies { compileOnly(project(":core")) - compileOnly(project(":kraken")) - compileOnly(project(":helix")) testCompile(project(":core")) - testCompile(project(":kraken")) - testCompile(project(":helix")) } \ No newline at end of file diff --git a/pubsub/src/main/java/glitch/pubsub/GlitchPubSub.java b/pubsub/src/main/java/glitch/pubsub/GlitchPubSub.java index 59cd8b8..9f9f454 100644 --- a/pubsub/src/main/java/glitch/pubsub/GlitchPubSub.java +++ b/pubsub/src/main/java/glitch/pubsub/GlitchPubSub.java @@ -1,15 +1,11 @@ package glitch.pubsub; -import com.fatboyindustrial.gsonjavatime.Converters; import com.google.gson.*; import glitch.GlitchClient; import glitch.api.ws.WebSocket; import glitch.api.ws.events.IEvent; import glitch.api.ws.events.PingEvent; -import glitch.pubsub.events.PubSubEvent; import glitch.pubsub.events.json.ModerationData; -import glitch.pubsub.events.json.SingleRequest; -import glitch.pubsub.events.json.TopicRequest; import glitch.pubsub.events.json.VideoPlayback; import glitch.pubsub.object.adapters.MessageTypeAdapter; import glitch.pubsub.object.adapters.ModerationActionAdapter; @@ -38,15 +34,15 @@ public final class GlitchPubSub implements ISocketService { private static final Logger LOG = LoggerFactory.getLogger(GlitchPubSub.class); protected final WebSocket ws; private final GlitchClient client; - private final TopicsCache topicsCache = new TopicsCache(this); + private final TopicsCache topicsCache; private final Gson gson; @SuppressWarnings("unchecked") private GlitchPubSub( GlitchClient client, GsonBuilder gson, boolean secure, Map topics, FluxProcessor, IEvent> eventProcessor, - boolean disableAutoPing - ) { + boolean disableAutoPing, + boolean shutdownHook) { this.client = client; this.gson = gson.registerTypeAdapter(GlitchPubSub.class, (JsonDeserializer) (json, type, context) -> GlitchPubSub.this).create(); this.ws = WebSocket.builder(this) @@ -54,22 +50,27 @@ private GlitchPubSub( .setEventConverter(new PubSubConverter(this.gson)) .setEventProcessor(eventProcessor) .build((secure) ? URI_SECURE : URI); - topics.forEach(this.topicsCache::register); - this.ws.onEvent(PubSubEvent.class) - .subscribe(PubSubUtils::consume); + this.topicsCache = new TopicsCache(this, topics); this.ws.onEvent(PingEvent.class) - .subscribe(ping -> { + .subscribe(ignore -> { if (!disableAutoPing) { - buildMessage(MessageType.PONG, null).subscribe(); + this.ws.send(Mono.just(createMessage(MessageType.PONG, null))).subscribe(); } }); + + if (shutdownHook) { + Runtime.getRuntime().addShutdownHook(new Thread(this::logout)); + } } public static Builder builder(GlitchClient client) { return new Builder(client); } - Mono buildMessage(MessageType type, @Nullable Topic topic) { + String createMessage(MessageType type, @Nullable Topic topic) { + JsonObject object = new JsonObject(); + object.add("type", new JsonPrimitive(type.name())); + if (Arrays.asList(MessageType.LISTEN, MessageType.UNLISTEN).contains(type) && topic != null) { JsonArray topics = new JsonArray(); topics.add(new JsonPrimitive(topic.getRawType())); @@ -80,10 +81,11 @@ Mono buildMessage(MessageType type, @Nullable Topic topic) { dataType.add("auth_token", new JsonPrimitive(topic.getCredential().getAccessToken())); } - return this.ws.send(Mono.just(gson.toJson(new TopicRequest(type, topic.getCode().toString(), dataType)))); - } else { - return this.ws.send(Mono.just(gson.toJson(new SingleRequest(type)))); + object.addProperty("nonce", topic.getCode().toString()); + object.add("data", dataType); } + + return gson.toJson(object); } public TopicsCache getTopicsCache() { @@ -112,24 +114,29 @@ public > Flux onEvent(Class type) { return this.ws.onEvent(type); } + @Override + public Flux> onEvents() { + return this.ws.onEvents(); + } + @Override public GlitchClient getClient() { return client; } private Mono doInit() { - return Flux.fromIterable(topicsCache.getActive()) - .flatMap(t -> buildMessage(MessageType.LISTEN, t)) - .then(); + return this.ws.send(Flux.fromIterable(topicsCache.getActive()) + .map(t -> createMessage(MessageType.LISTEN, t))); } public static class Builder { private final GlitchClient client; - private final GsonBuilder gsonBuilder = Converters.registerAll(new GsonBuilder()) + private final GsonBuilder gsonBuilder = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); private final AtomicBoolean secure = new AtomicBoolean(true); private final Map topics = new LinkedHashMap<>(50); private final AtomicBoolean disableAutoPing = new AtomicBoolean(false); + private final AtomicBoolean shutdownHook = new AtomicBoolean(false); private FluxProcessor, IEvent> eventProcessor = EmitterProcessor.create(true); private Builder(GlitchClient client) { @@ -158,6 +165,11 @@ public Builder setEventProcessor(FluxProcessor, IEvent { - private final Gson gson; - - PubSubConverter(Gson gson) { - this.gson = gson; - } - - @Override - public IEvent convert(GlitchPubSub client, String raw) { - return new PubSubEvent(client, gson, gson.toJsonTree(raw).getAsJsonObject()); - } - -// @Override -// public IEvent convert(ByteString value, GlitchPubSub client) { -// JsonElement r = gson.toJsonTree(value.utf8()); -// Response response = gson.fromJson(r, Response.class); -// -// switch (response.getType()) { -// case PING: -// return new PingEvent<>(client); -// case PONG: -// return new PongEvent<>(client); -// case RECONNECT: -// return new ReconnectAlertEvent(client); -// case RESPONSE: -// return new ResponseListenedEvent(client, UUID.fromString(response.getNonce()), response.getError()); -// case MESSAGE: -// return doResponseMessage(client, response.getData()); -// } -// return new UnknownResponseEvent(client, r); -// } -// -// private IEvent doResponseMessage(GlitchPubSub client, ResponseData data) { -// Topic topic = data.getTopic(); -// JsonObject content = data.getMessage(); -// -// switch (topic.getType()) { -// case FOLLOW: -// return new FollowEvent(client, topic, client.gson.fromJson(content, Following.class)); -// case WHISPERS: -// WhisperMode whisperMode = client.gson.fromJson(content, WhisperMode.class); -// switch (whisperMode.type) { -// case THREAD: -// return new WhisperThreadEvent(client, topic, client.gson.fromJson(whisperMode.data, WhisperThread.class)); -// case WHISPER_RECEIVED: -// return new WhisperReceivedEvent(client, topic, client.gson.fromJson(whisperMode.data, WhisperMessage.class)); -// case WHISPER_SENT: -// return new WhisperSentEvent(client, topic, client.gson.fromJson(whisperMode.data, WhisperMessage.class)); -// } -// return new RawMessageEvent(client, topic, content); -// case CHANNEL_BITS: -// return new BitsEvent(client, topic, client.gson.fromJson(content, BitsMessage.class)); -// case VIDEO_PLAYBACK: -// VideoPlayback playback = client.gson.fromJson(content, VideoPlayback.class); -// -// switch (playback.getType()) { -// case STREAM_UP: -// return new StreamUpEvent(client, topic, client.gson.fromJson(content, StreamUp.class)); -// case STREAM_DOWN: -// return new StreamDownEvent(client, topic, client.gson.fromJson(content.get("server_time"), Instant.class)); -// case VIEW_COUNT: -// return new ViewCountEvent(client, topic, client.gson.fromJson(content, ViewCount.class)); -// } -// -// return new RawMessageEvent(client, topic, content); -// case CHANNEL_COMMERCE: -// return new CommerceEvent(client, topic, client.gson.fromJson(content, CommerceMessage.class)); -// case CHANNEL_SUBSCRIPTION: -// SubscriptionMessage sub = client.gson.fromJson(content, SubscriptionMessage.class); -// -// if (sub.getContext().equals(SubscriptionContext.SUBGIFT)) { -// return new SubGiftEvent(client, topic, client.gson.fromJson(content, GiftSubscriptionMessage.class)); -// } else { -// return new SubscriptionEvent(client, topic, sub); -// } -// case CHAT_MODERATION_ACTIONS: -// return doResponseModeration(client, topic, content.getAsJsonObject("data")); -// -// case CHANNEL_EXTENSION_BROADCAST: -// return new ChannelExtensionBroadcastEvent(client, topic, content.getAsJsonArray("content")); -// } -// return new RawMessageEvent(client, topic, content); -// } -// -// private IEvent doResponseModeration(GlitchPubSub client, Topic topic, JsonObject data) { -// ModerationData modData = client.gson.fromJson(data, ModerationData.class); -// switch (modData.getModerationAction()) { -// case TIMEOUT: -// return new TimeoutUserEvent(client, topic, -// new Timeout(modData.getCreatedBy(), -// modData.getCreatedById(), -// modData.getArgs().get(0), -// modData.getTargetId(), -// Integer.parseInt(modData.getArgs().get(1)), -// (modData.getArgs().size() > 2) ? modData.getArgs().get(2) : null)); -// case BAN: -// return new BanUserEvent(client, topic, -// new Ban(modData.getCreatedBy(), -// modData.getCreatedById(), -// modData.getArgs().get(0), -// modData.getTargetId(), -// (modData.getArgs().size() > 1) ? modData.getArgs().get(1) : null)); -// case UNBAN: -// case UNTIMEOUT: -// return new UnbanUserEvent(client, topic, new Unban( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// modData.getArgs().get(0), -// modData.getTargetId() -// )); -// case HOST: -// return new HostEvent(client, topic, new Host( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// modData.getArgs().get(0), -// modData.getTargetId() -// )); -// case SUBSCRIBERS: -// return new SubscribersOnlyEvent(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// true -// )); -// case SUBSCRIBERSOFF: -// return new SubscribersOnlyEvent(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// false -// )); -// case CLEAR: -// return new ClearChatEvent(client, topic, new Moderator( -// modData.getCreatedBy(), -// modData.getCreatedById() -// )); -// case EMOTEONLY: -// return new EmoteOnlyEvent(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// true -// )); -// case EMOTEONLYOFF: -// return new EmoteOnlyEvent(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// false -// )); -// case R9KBETA: -// return new Robot9000Event(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// true -// )); -// case R9KBETAOFF: -// return new Robot9000Event(client, topic, new ModeratorActivation( -// modData.getCreatedBy(), -// modData.getCreatedById(), -// false -// )); -// case DELETE: -// return new MessageDeleteEvent(client, topic, new MessageDelete( -// modData.getArgs().get(2), -// modData.getCreatedBy(), -// modData.getCreatedById(), -// modData.getArgs().get(1), -// modData.getArgs().get(0), -// modData.getTargetId() -// )); -// } -// return new RawMessageEvent(client, topic, data); -// } -// -// private static class WhisperMode { -// @JsonAdapter(value = WhisperTypeAdapter.class) -// private final Type type; -// private final String data; -// -// public WhisperMode(Type type, String data) { -// this.type = type; -// this.data = data; -// } -// -// public Type getType() { -// return this.type; -// } -// -// public String getData() { -// return this.data; -// } -// -// enum Type { -// WHISPER_RECEIVED, -// WHISPER_SENT, -// THREAD -// } -// -// private class WhisperTypeAdapter extends TypeAdapter { -// @Override -// public void write(JsonWriter out, Type value) throws IOException { -// out.value(value.name().toLowerCase()); -// } -// -// @Override -// public Type read(JsonReader in) throws IOException { -// for (Type t : Type.values()) { -// if (t.name().equalsIgnoreCase(in.nextString())) { -// return t; -// } -// } -// throw new JsonParseException("Unknown whisper type: " + in.nextString()); -// } -// } -// } -} diff --git a/pubsub/src/main/java/glitch/pubsub/Topic.java b/pubsub/src/main/java/glitch/pubsub/Topic.java index 66809c2..b91f333 100644 --- a/pubsub/src/main/java/glitch/pubsub/Topic.java +++ b/pubsub/src/main/java/glitch/pubsub/Topic.java @@ -1,12 +1,8 @@ package glitch.pubsub; import glitch.api.http.Unofficial; -import glitch.auth.GlitchScope; import glitch.auth.objects.json.Credential; -import glitch.exceptions.http.ScopeIsMissingException; -import java.io.Serializable; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -23,135 +19,6 @@ public Topic(Type type, String[] suffix, Credential credential) { this.credential = credential; } - /** - * Anyone cheers on a specified channel. - */ - public static Topic bits(Long channelId, Credential credential) { - return new Topic(Type.CHANNEL_BITS, toArray(channelId), credential); - } - - /** - * Anyone cheers on a authorized channel. - */ - public static Topic bits(Credential credential) { - return bits(credential.getUserId(), credential); - } - - /** - * Anyone cheers on a specified channel. - */ - public static Topic bitsV2(Long channelId, Credential credential) { - if (credential.getScopes().contains(GlitchScope.BITS_READ)) { - return new Topic(Type.CHANNEL_BITS_V2, toArray(channelId), credential); - } else throw new ScopeIsMissingException(GlitchScope.BITS_READ); - } - - /** - * Anyone cheers on a authorized channel. - */ - public static Topic bitsV2(Credential credential) { - return bitsV2(credential.getUserId(), credential); - } - - /** - * Anyone subscribes (first month), resubscribes (subsequent months), or gifts a subscription to a channel. - *

- * Subgift subscription messages contain recipient information. - */ - public static Topic subscription(Long channelId, Credential credential) throws ScopeIsMissingException { - if (credential.getScopes().contains(GlitchScope.CHANNEL_SUBSCRIPTIONS)) { - return new Topic(Type.CHANNEL_SUBSCRIPTION, toArray(channelId), credential); - } else throw new ScopeIsMissingException(GlitchScope.CHANNEL_SUBSCRIPTIONS); - } - - /** - * Anyone subscribes (first month), resubscribes (subsequent months), or gifts a subscription to a authorized channel. - *

- * Subgift subscription messages contain recipient information. - */ - public static Topic subscription(Credential credential) throws ScopeIsMissingException { - return subscription(credential.getUserId(), credential); - } - - /** - * Anyone whispers the specified user. - */ - public static Topic whispers(Credential credential) throws ScopeIsMissingException { - if (credential.getScopes().contains(GlitchScope.CHAT_LOGIN) || credential.getScopes().contains(GlitchScope.WHISPERS_READ)) { - return new Topic(Type.WHISPERS, toArray(credential.getUserId()), credential); - } else throw new ScopeIsMissingException(GlitchScope.WHISPERS_READ); - } - - /** - * Anyone follow on a specified channel. - */ - public static Topic following(Long channelId) { - return new Topic(Type.FOLLOW, toArray(channelId), null); - } - - /** - * Listening moderation actions in specific channel. - * Owner ID must be a moderator in specific channel. - */ - public static Topic moderationActions(Long channelId, Credential credential) throws ScopeIsMissingException { - if (credential.getScopes().contains(GlitchScope.CHAT_LOGIN) || credential.getScopes().contains(GlitchScope.CHANNEL_MODERATE)) { - return new Topic(Type.CHAT_MODERATION_ACTIONS, toArray(channelId, credential.getUserId()), credential); - } else throw new ScopeIsMissingException(GlitchScope.CHANNEL_MODERATE); - } - - /** - * Listening moderation actions on the own channel. - */ - public static Topic moderationActions(Credential credential) throws ScopeIsMissingException { - return moderationActions(credential.getUserId(), credential); - } - - /** - * Listens EBS broadcast sent to specific extension on a specific channel - */ - public static Topic extensionBroadcast() { - throw new UnsupportedOperationException("Extensions is currently unsupported"); - } - - /** - * Listening live stream with view counter in specific channel name - */ - public static Topic videoPlayback(String channelName) { - return new Topic(Type.VIDEO_PLAYBACK, toArray(channelName), null); - } - - /** - * Anyone makes a purchase on a channel. - */ - public static Topic commerce(Long channelId, Credential credential) { - return new Topic(Type.CHANNEL_COMMERCE, toArray(channelId), credential); - } - - /** - * Anyone makes a purchase on your channel. - */ - public static Topic commerce(Credential credential) { - return commerce(credential.getUserId(), credential); - } - - private static String[] toArray(S... serialized) { - return Arrays.stream(serialized).map(String::valueOf).toArray(String[]::new); - } - - public static Topic fromRaw(String raw) { - String[] split = raw.split("."); - - String[] suffix = Arrays.stream(split).filter(e -> !e.equals(split[0])) - .map(s -> s.replace("-broadcast", "")) - .toArray(String[]::new); - - if (split.length > 2 && split[0].equals(Type.CHAT_MODERATION_ACTIONS.value)) { - return new Topic(Type.CHAT_MODERATION_ACTIONS, suffix, null); - } else { - return new Topic(Type.readType(split[0]), suffix, null); - } - } - public UUID getCode() { return UUID.nameUUIDFromBytes(getRawType().getBytes(Charset.forName("UTF-8"))); } @@ -239,13 +106,13 @@ public String toRaw(String... subject) { VIDEO_PLAYBACK("video-playback"); - private final String value; + final String value; Type(String value) { this.value = value; } - private static Type readType(String raw) { + static Type readType(String raw) { for (Type t : values()) { if (t.value.equals(raw)) return t; } diff --git a/pubsub/src/main/java/glitch/pubsub/Topics.java b/pubsub/src/main/java/glitch/pubsub/Topics.java new file mode 100644 index 0000000..2051578 --- /dev/null +++ b/pubsub/src/main/java/glitch/pubsub/Topics.java @@ -0,0 +1,144 @@ +package glitch.pubsub; + +import glitch.auth.GlitchScope; +import glitch.auth.objects.json.Credential; +import glitch.exceptions.http.ScopeIsMissingException; +import java.io.Serializable; +import java.util.Arrays; + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +public class Topics { + + /** + * Anyone cheers on a specified channel. + */ + public static Topic bits(Long channelId, Credential credential) { + return new Topic(Topic.Type.CHANNEL_BITS, toArray(channelId), credential); + } + + /** + * Anyone cheers on a authorized channel. + */ + public static Topic bits(Credential credential) { + return bits(credential.getUserId(), credential); + } + + /** + * Anyone cheers on a specified channel. + */ + public static Topic bitsV2(Long channelId, Credential credential) { + if (credential.getScopes().contains(GlitchScope.BITS_READ)) { + return new Topic(Topic.Type.CHANNEL_BITS_V2, toArray(channelId), credential); + } else throw new ScopeIsMissingException(GlitchScope.BITS_READ); + } + + /** + * Anyone cheers on a authorized channel. + */ + public static Topic bitsV2(Credential credential) { + return bitsV2(credential.getUserId(), credential); + } + + /** + * Anyone subscribes (first month), resubscribes (subsequent months), or gifts a subscription to a channel. + *

+ * Subgift subscription messages contain recipient information. + */ + public static Topic subscription(Long channelId, Credential credential) throws ScopeIsMissingException { + if (credential.getScopes().contains(GlitchScope.CHANNEL_SUBSCRIPTIONS)) { + return new Topic(Topic.Type.CHANNEL_SUBSCRIPTION, toArray(channelId), credential); + } else throw new ScopeIsMissingException(GlitchScope.CHANNEL_SUBSCRIPTIONS); + } + + /** + * Anyone subscribes (first month), resubscribes (subsequent months), or gifts a subscription to a authorized channel. + *

+ * Subgift subscription messages contain recipient information. + */ + public static Topic subscription(Credential credential) throws ScopeIsMissingException { + return subscription(credential.getUserId(), credential); + } + + /** + * Anyone whispers the specified user. + */ + public static Topic whispers(Credential credential) throws ScopeIsMissingException { + if (credential.getScopes().contains(GlitchScope.CHAT_LOGIN) || credential.getScopes().contains(GlitchScope.WHISPERS_READ)) { + return new Topic(Topic.Type.WHISPERS, toArray(credential.getUserId()), credential); + } else throw new ScopeIsMissingException(GlitchScope.WHISPERS_READ); + } + + /** + * Anyone follow on a specified channel. + */ + public static Topic following(Long channelId) { + return new Topic(Topic.Type.FOLLOW, toArray(channelId), null); + } + + /** + * Listening moderation actions in specific channel. + * Owner ID must be a moderator in specific channel. + */ + public static Topic moderationActions(Long channelId, Credential credential) throws ScopeIsMissingException { + if (credential.getScopes().contains(GlitchScope.CHAT_LOGIN) || credential.getScopes().contains(GlitchScope.CHANNEL_MODERATE)) { + return new Topic(Topic.Type.CHAT_MODERATION_ACTIONS, toArray(channelId, credential.getUserId()), credential); + } else throw new ScopeIsMissingException(GlitchScope.CHANNEL_MODERATE); + } + + /** + * Listening moderation actions on the own channel. + */ + public static Topic moderationActions(Credential credential) throws ScopeIsMissingException { + return moderationActions(credential.getUserId(), credential); + } + + /** + * Listens EBS broadcast sent to specific extension on a specific channel + */ + public static Topic extensionBroadcast() { + throw new UnsupportedOperationException("Extensions is currently unsupported"); + } + + /** + * Listening live stream with view counter in specific channel name + */ + public static Topic videoPlayback(String channelName) { + return new Topic(Topic.Type.VIDEO_PLAYBACK, toArray(channelName), null); + } + + /** + * Anyone makes a purchase on a channel. + */ + public static Topic commerce(Long channelId, Credential credential) { + return new Topic(Topic.Type.CHANNEL_COMMERCE, toArray(channelId), credential); + } + + /** + * Anyone makes a purchase on your channel. + */ + public static Topic commerce(Credential credential) { + return commerce(credential.getUserId(), credential); + } + + private static String[] toArray(S... serialized) { + return Arrays.stream(serialized).map(String::valueOf).toArray(String[]::new); + } + + public static Topic fromRaw(String raw) { + String[] split = raw.split("."); + + String[] suffix = Arrays.stream(split).filter(e -> !e.equals(split[0])) + .map(s -> s.replace("-broadcast", "")) + .toArray(String[]::new); + + if (split.length > 2 && split[0].equals(Topic.Type.CHAT_MODERATION_ACTIONS.value)) { + return new Topic(Topic.Type.CHAT_MODERATION_ACTIONS, suffix, null); + } else { + return new Topic(Topic.Type.readType(split[0]), suffix, null); + } + } +} diff --git a/pubsub/src/main/java/glitch/pubsub/TopicsCache.java b/pubsub/src/main/java/glitch/pubsub/TopicsCache.java index 9551274..3a611be 100644 --- a/pubsub/src/main/java/glitch/pubsub/TopicsCache.java +++ b/pubsub/src/main/java/glitch/pubsub/TopicsCache.java @@ -13,8 +13,9 @@ public class TopicsCache { private final GlitchPubSub client; private final Map topics = new LinkedHashMap<>(50); - TopicsCache(GlitchPubSub client) { + TopicsCache(GlitchPubSub client, Map topics) { this.client = client; + this.topics.putAll(topics); } public Collection getAll() { @@ -99,10 +100,9 @@ Mono unregister(Topic topic, boolean disable) { return Mono.error(new TopicException("Topic is not registered or it is not exist: " + topic.getRawType())); } - Mono exchange(MessageType type, Topic topic) { if (client.ws.isConnected()) { - return client.buildMessage(type, topic); + return client.ws.send(Mono.just(this.client.createMessage(type, topic))); } else return Mono.error(new GlitchException("Cannot send message!", new NotYetConnectedException())); } diff --git a/pubsub/src/main/java/glitch/pubsub/object/adapters/ServerTimeAdapter.java b/pubsub/src/main/java/glitch/pubsub/object/adapters/ServerTimeAdapter.java new file mode 100644 index 0000000..dc74cec --- /dev/null +++ b/pubsub/src/main/java/glitch/pubsub/object/adapters/ServerTimeAdapter.java @@ -0,0 +1,26 @@ +package glitch.pubsub.object.adapters; + +import com.google.gson.*; +import java.lang.reflect.Type; +import java.time.Instant; + +/** + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +public class ServerTimeAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + String[] secdons = json.getAsString().split("\\."); + long s = Long.parseLong(secdons[0]); + long ns = Long.parseLong(secdons[1]) * 100; + return Instant.ofEpochSecond(s, ns); + } + + @Override + public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getNano()); + } +} diff --git a/pubsub/src/main/java/glitch/pubsub/object/adapters/VideoPlaybackTypeAdapter.java b/pubsub/src/main/java/glitch/pubsub/object/adapters/VideoPlaybackTypeAdapter.java index 60865af..f4bd31b 100644 --- a/pubsub/src/main/java/glitch/pubsub/object/adapters/VideoPlaybackTypeAdapter.java +++ b/pubsub/src/main/java/glitch/pubsub/object/adapters/VideoPlaybackTypeAdapter.java @@ -7,11 +7,18 @@ import java.io.IOException; public class VideoPlaybackTypeAdapter extends TypeAdapter { + + /** + * {@inheritDoc} + */ @Override public void write(JsonWriter out, VideoPlayback.Type value) throws IOException { out.value(value.name().toLowerCase()); } + /** + * {@inheritDoc} + */ @Override public VideoPlayback.Type read(JsonReader in) throws IOException { String type = in.nextString().replace("-", "_"); diff --git a/pubsub/src/main/kotlin/glitch/pubsub/PubSubConverter.kt b/pubsub/src/main/kotlin/glitch/pubsub/PubSubConverter.kt new file mode 100644 index 0000000..28a2ff8 --- /dev/null +++ b/pubsub/src/main/kotlin/glitch/pubsub/PubSubConverter.kt @@ -0,0 +1,195 @@ +package glitch.pubsub + +import com.google.gson.* +import com.google.gson.annotations.JsonAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import glitch.api.ws.IEventConverter +import glitch.api.ws.events.IEvent +import glitch.api.ws.events.PingEvent +import glitch.api.ws.events.PongEvent +import glitch.pubsub.`object`.enums.MessageType +import glitch.pubsub.`object`.enums.SubscriptionContext +import glitch.pubsub.events.* +import glitch.pubsub.events.json.* +import glitch.pubsub.exceptions.TopicException +import java.io.IOException +import java.util.* + +/** + * + * @author Damian Staszewski [damian@stachuofficial.tv] + * @version %I%, %G% + * @since 1.0 + */ +class PubSubConverter(private val gson: Gson) : IEventConverter { + + + override fun convert(client: GlitchPubSub, raw: String): IEvent { + val `object`: JsonObject = JsonParser().parse(raw).asJsonObject + + val type = MessageType.valueOf(`object`["type"].asString) + + return when (type) { + MessageType.PING -> PingEvent(client) + MessageType.PONG -> PongEvent(client) + MessageType.RECONNECT -> ReconnectRequiredEvent(client) + MessageType.RESPONSE -> doResponse(client, `object`) + MessageType.MESSAGE -> doMessage(client, `object`) + else -> PubSubEvent(client, `object`) + } + } + + private fun doResponse(client: GlitchPubSub, data: JsonObject): IEvent { + val topicsCache = client.topicsCache + val nonce = UUID.fromString(data["nonce"].asString) + val error = data["error"].asString.orEmpty() + for (topic in topicsCache.active) { + if (topic.code == nonce) { + return if (error.isBlank()) { + SucessfulResponseEvent(client, topic) + } else { + val err = when (error) { + "ERR_BADMESSAGE" -> "Inappropriate message!" + "ERR_BADAUTH" -> "Failed to using authorization!" + "ERR_SERVER" -> "Internal Server Error!" + "ERR_BADTOPIC" -> "Inappropriate topic!" + else -> error + } + ErrorResponseEvent(client, topic, TopicException(err)) + } + } + } + return ErrorResponseEvent(client, null, TopicException("Unknown registered topic for nonce: $nonce")) + } + + private fun doMessage(client: GlitchPubSub, `object`: JsonObject): IEvent { + val topicsCache = client.topicsCache + val data = `object`["data"].asJsonObject + val topicRaw = data.get("topic").asString + val rawMessage = JsonParser().parse(data.get("message").asString).asJsonObject + + for (topic in topicsCache.active) { + if (topic.rawType == topicRaw) { + return handleMessage(client, topic, rawMessage) + } + } + + return UnknownMessageEvent(client, topicRaw, rawMessage) + } + + private fun handleMessage(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject): IEvent { + return when (topic.type) { + Topic.Type.FOLLOW -> message_follow(client, topic, rawMessage) + Topic.Type.WHISPERS -> message_whisper(client, topic, rawMessage) + Topic.Type.CHANNEL_BITS, Topic.Type.CHANNEL_BITS_V2 -> message_bits(client, topic, rawMessage) + Topic.Type.VIDEO_PLAYBACK -> message_playback(client, topic, rawMessage) + Topic.Type.CHANNEL_COMMERCE -> message_commerce(client, topic, rawMessage) + Topic.Type.CHANNEL_SUBSCRIPTION -> message_sub(client, topic, rawMessage) + Topic.Type.CHAT_MODERATION_ACTIONS -> message_moderation(client, topic, rawMessage) + Topic.Type.CHANNEL_EXTENSION_BROADCAST -> message_ebs(client, topic, rawMessage) + } + } + + private fun message_follow(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject) = + FollowEvent(client, topic, gson.fromJson(rawMessage, Following::class.java)) + + private fun message_whisper(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject): IEvent { + val whisper = gson.fromJson(rawMessage, WhisperMode::class.java) + return when (whisper.type) { + WhisperMode.Type.THREAD -> + WhisperThreadEvent(client, topic, gson.fromJson(whisper.data, WhisperThread::class.java)) + WhisperMode.Type.WHISPER_RECEIVED -> + WhisperReceivedEvent(client, topic, gson.fromJson(whisper.data, WhisperMessage::class.java)) + WhisperMode.Type.WHISPER_SENT -> + WhisperSentEvent(client, topic, gson.fromJson(whisper.data, WhisperMessage::class.java)) + } + } + + private fun message_bits(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject) = + BitsEvent(client, topic, gson.fromJson(rawMessage, BitsMessage::class.java)) + + private fun message_playback(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject): IEvent { + val playback = gson.fromJson(rawMessage, VideoPlayback::class.java) + + return when (playback.type) { + VideoPlayback.Type.STREAM_UP -> + StreamUpEvent(client, topic, gson.fromJson(rawMessage, StreamUp::class.java)) + VideoPlayback.Type.STREAM_DOWN -> + StreamDownEvent(client, topic, gson.fromJson(rawMessage, StreamDown::class.java)) + VideoPlayback.Type.VIEW_COUNT -> + ViewCountEvent(client, topic, gson.fromJson(rawMessage, ViewCount::class.java)) + } + } + + private fun message_commerce(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject) = + CommerceEvent(client, topic, gson.fromJson(rawMessage, Commerce::class.java)) + + private fun message_sub(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject): IEvent { + val sub = gson.fromJson(rawMessage, SubscriptionMessage::class.java) + + return if (sub.context == SubscriptionContext.SUBGIFT) { + SubGiftEvent(client, topic, gson.fromJson(rawMessage, GiftSubscriptionMessage::class.java)) + } else { + SubscriptionEvent(client, topic, sub) + } + } + + private fun message_moderation(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject): IEvent { + val modData = gson.fromJson(rawMessage, ModerationData::class.java) + + return when (modData.moderationAction) { + ModerationData.Action.DELETE -> + MessageDeleteEvent(client, topic, MessageDelete(modData)) + ModerationData.Action.TIMEOUT -> + TimeoutUserEvent(client, topic, Timeout(modData)) + ModerationData.Action.BAN -> + BanUserEvent(client, topic, Ban(modData)) + ModerationData.Action.UNBAN, ModerationData.Action.UNTIMEOUT -> + UnbanUserEvent(client, topic, Unban(modData)) + ModerationData.Action.HOST -> + HostEvent(client, topic, Host(modData)) + ModerationData.Action.SUBSCRIBERS, ModerationData.Action.SUBSCRIBERSOFF -> + SubscribersOnlyEvent(client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.SUBSCRIBERS)) + ModerationData.Action.CLEAR -> + ClearChatEvent(client, topic, Moderator(modData)) + ModerationData.Action.EMOTEONLY, ModerationData.Action.EMOTEONLYOFF -> + EmoteOnlyEvent(client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.EMOTEONLY)) + ModerationData.Action.R9KBETA, ModerationData.Action.R9KBETAOFF -> + Robot9000Event(client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.R9KBETA)) + } + } + + private fun message_ebs(client: GlitchPubSub, topic: Topic, rawMessage: JsonObject) = + ChannelExtensionBroadcastEvent(client, topic, gson.toJsonTree(rawMessage).asJsonObject.getAsJsonArray("content")) + + + internal data class WhisperMode( + @JsonAdapter(WhisperTypeAdapter::class) + internal val type: Type, + internal val data: String + ) { + internal enum class Type { + WHISPER_RECEIVED, + WHISPER_SENT, + THREAD + } + + internal inner class WhisperTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: Type) { + out.value(value.name.toLowerCase()) + } + + @Throws(IOException::class) + override fun read(`in`: JsonReader): Type { + for (t in Type.values()) { + if (t.name.equals(`in`.nextString(), ignoreCase = true)) { + return t + } + } + throw JsonParseException("Unknown whisper type: " + `in`.nextString()) + } + } + } +} \ No newline at end of file diff --git a/pubsub/src/main/kotlin/glitch/pubsub/PubSubUtils.kt b/pubsub/src/main/kotlin/glitch/pubsub/PubSubUtils.kt deleted file mode 100644 index c7b07f4..0000000 --- a/pubsub/src/main/kotlin/glitch/pubsub/PubSubUtils.kt +++ /dev/null @@ -1,209 +0,0 @@ -package glitch.pubsub - -import glitch.api.ws.events.IEvent -import glitch.api.ws.events.PingEvent -import glitch.api.ws.events.PongEvent -import glitch.pubsub.`object`.enums.MessageType -import glitch.pubsub.events.* -import glitch.pubsub.exceptions.TopicException -import java.util.* -import com.google.gson.JsonParseException -import java.io.IOException -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import com.google.gson.TypeAdapter -import com.google.gson.annotations.JsonAdapter -import glitch.pubsub.events.WhisperThreadEvent -import glitch.pubsub.events.WhisperReceivedEvent -import glitch.pubsub.events.WhisperSentEvent -import glitch.pubsub.events.StreamUpEvent -import glitch.pubsub.events.json.* -import glitch.pubsub.events.CommerceEvent -import glitch.pubsub.events.json.Commerce -import glitch.pubsub.events.json.GiftSubscriptionMessage -import glitch.pubsub.events.SubGiftEvent -import glitch.pubsub.`object`.enums.SubscriptionContext -import glitch.pubsub.events.SubscriptionEvent -import glitch.pubsub.events.json.SubscriptionMessage -import glitch.pubsub.events.ChannelExtensionBroadcastEvent -import glitch.pubsub.events.TimeoutUserEvent -import glitch.pubsub.events.json.ModerationData -import glitch.pubsub.events.json.Timeout - - -/** - * - * @author Damian Staszewski [damian@stachuofficial.tv] - * @version %I%, %G% - * @since 1.0 - */ -object PubSubUtils { - @JvmStatic - fun consume(event: PubSubEvent) { - val type = MessageType.valueOf(event.data.get("type").asString) - - when (type) { - MessageType.PING -> event.dispatch(PingEvent(event.client)) - MessageType.PONG -> event.dispatch(PongEvent(event.client)) - MessageType.RECONNECT -> event.dispatch(ReconnectRequiredEvent(event.client)) - MessageType.RESPONSE -> doResponse(event) - MessageType.MESSAGE -> doMessage(event) - } - } - - private fun doResponse(event: PubSubEvent) = - event.dispatch(event.let { - val topicsCache = it.client.topicsCache - val nonce = UUID.fromString(it.data["nonce"].asString) - val error = it.data["error"].asString.orEmpty() - for (topic in topicsCache.all) { - if (topic.code == nonce) { - return@let if (error.isBlank()) { - SucessfulResponseEvent(event.client, topic) - } else { - val err = when (error) { - "ERR_BADMESSAGE" -> "Inappropriate message!" - "ERR_BADAUTH" -> "Failed to using authorization!" - "ERR_SERVER" -> "Internal Server Error!" - "ERR_BADTOPIC" -> "Inappropriate topic!" - else -> error - } - ErrorResponseEvent(event.client, topic, TopicException(err)) - } - } - } - return@let ErrorResponseEvent(event.client, null, TopicException("Unknown registered topic for nonce: $nonce")) - }) - - private fun doMessage(event: PubSubEvent) { - val topicsCache = event.client.topicsCache - val data = event.data.get("data").asJsonObject - val topicRaw = data.get("topic").asString - val rawMessage = data.get("message").asString - - for (topic in topicsCache.active) { - if (topic.rawType == topicRaw) { - handleMessage(event, topic, rawMessage) - return - } - } - - event.dispatch(UnknownMessageEvent(event.client, topicRaw, event.mapper.toJsonTree(rawMessage).asJsonObject)) - } - - private fun PubSubEvent.dispatch(event: IEvent) = client.ws.dispatch(event) - - private fun handleMessage(event: PubSubEvent, topic: Topic, rawMessage: String) { - val client = event.client - - when (topic.type) { - Topic.Type.FOLLOW -> message_follow(event, topic, rawMessage) - Topic.Type.WHISPERS -> message_whisper(event, topic, rawMessage) - Topic.Type.CHANNEL_BITS, Topic.Type.CHANNEL_BITS_V2 -> message_bits(event, topic, rawMessage) - Topic.Type.VIDEO_PLAYBACK -> message_playback(event, topic, rawMessage) - Topic.Type.CHANNEL_COMMERCE -> message_commerce(event, topic, rawMessage) - Topic.Type.CHANNEL_SUBSCRIPTION -> message_sub(event, topic, rawMessage) - Topic.Type.CHAT_MODERATION_ACTIONS -> message_moderation(event, topic, rawMessage) - Topic.Type.CHANNEL_EXTENSION_BROADCAST -> message_ebs(event, topic, rawMessage) - } - } - - private fun message_follow(event: PubSubEvent, topic: Topic, rawMessage: String) { - event.dispatch(FollowEvent(event.client, topic, event.mapper.fromJson(rawMessage, Following::class.java))) - } - private fun message_whisper(event: PubSubEvent, topic: Topic, rawMessage: String) { - val whisper = event.mapper.fromJson(rawMessage, WhisperMode::class.java) - when (whisper.type) { - PubSubUtils.WhisperMode.Type.THREAD -> - event.dispatch(WhisperThreadEvent(event.client, topic, event.mapper.fromJson(whisper.data, WhisperThread::class.java))) - PubSubUtils.WhisperMode.Type.WHISPER_RECEIVED -> - event.dispatch(WhisperReceivedEvent(event.client, topic, event.mapper.fromJson(whisper.data, WhisperMessage::class.java))) - PubSubUtils.WhisperMode.Type.WHISPER_SENT -> - event.dispatch(WhisperSentEvent(event.client, topic, event.mapper.fromJson(whisper.data, WhisperMessage::class.java))) - } - } - private fun message_bits(event: PubSubEvent, topic: Topic, rawMessage: String) { - event.dispatch(BitsEvent(event.client, topic, event.mapper.fromJson(rawMessage, BitsMessage::class.java))) - } - private fun message_playback(event: PubSubEvent, topic: Topic, rawMessage: String) { - val playback = event.mapper.fromJson(rawMessage, VideoPlayback::class.java) - - when(playback.type) { - VideoPlayback.Type.STREAM_UP -> - event.dispatch(StreamUpEvent(event.client, topic, event.mapper.fromJson(rawMessage, StreamUp::class.java))) - VideoPlayback.Type.STREAM_DOWN -> - event.dispatch(StreamDownEvent(event.client, topic, event.mapper.fromJson(rawMessage, StreamDown::class.java))) - VideoPlayback.Type.VIEW_COUNT -> - event.dispatch(ViewCountEvent(event.client, topic, event.mapper.fromJson(rawMessage, ViewCount::class.java))) - } - } - private fun message_commerce(event: PubSubEvent, topic: Topic, rawMessage: String) { - event.dispatch(CommerceEvent(event.client, topic, event.mapper.fromJson(rawMessage, Commerce::class.java))) - } - private fun message_sub(event: PubSubEvent, topic: Topic, rawMessage: String) { - val sub = event.mapper.fromJson(rawMessage, SubscriptionMessage::class.java) - - if (sub.context == SubscriptionContext.SUBGIFT) { - event.dispatch(SubGiftEvent(event.client, topic, event.mapper.fromJson(rawMessage, GiftSubscriptionMessage::class.java))) - } else { - event.dispatch(SubscriptionEvent(event.client, topic, sub)) - } - } - private fun message_moderation(event: PubSubEvent, topic: Topic, rawMessage: String) { - val modData = event.mapper.fromJson(rawMessage, ModerationData::class.java) - - when (modData.moderationAction) { - ModerationData.Action.DELETE -> - event.dispatch(MessageDeleteEvent(event.client, topic, MessageDelete(modData))) - ModerationData.Action.TIMEOUT -> - event.dispatch(TimeoutUserEvent(event.client, topic, Timeout(modData))) - ModerationData.Action.BAN -> - event.dispatch(BanUserEvent(event.client, topic, Ban(modData))) - ModerationData.Action.UNBAN, ModerationData.Action.UNTIMEOUT -> - event.dispatch(UnbanUserEvent(event.client, topic, Unban(modData))) - ModerationData.Action.HOST -> - event.dispatch(HostEvent(event.client, topic, Host(modData))) - ModerationData.Action.SUBSCRIBERS, ModerationData.Action.SUBSCRIBERSOFF -> - event.dispatch(SubscribersOnlyEvent(event.client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.SUBSCRIBERS))) - ModerationData.Action.CLEAR -> - event.dispatch(ClearChatEvent(event.client, topic, Moderator(modData))) - ModerationData.Action.EMOTEONLY, ModerationData.Action.EMOTEONLYOFF -> - event.dispatch(EmoteOnlyEvent(event.client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.EMOTEONLY))) - ModerationData.Action.R9KBETA, ModerationData.Action.R9KBETAOFF -> - event.dispatch(Robot9000Event(event.client, topic, ActivationByMod(modData, modData.moderationAction == ModerationData.Action.R9KBETA))) - - } - } - private fun message_ebs(event: PubSubEvent, topic: Topic, rawMessage: String) { - event.dispatch(ChannelExtensionBroadcastEvent(event.client, topic, event.mapper.toJsonTree(rawMessage).asJsonObject.getAsJsonArray("content"))) - } - - internal data class WhisperMode( - @JsonAdapter(WhisperTypeAdapter::class) - internal val type: Type, - internal val data: String - ) { - internal enum class Type { - WHISPER_RECEIVED, - WHISPER_SENT, - THREAD - } - - internal inner class WhisperTypeAdapter : TypeAdapter() { - @Throws(IOException::class) - override fun write(out: JsonWriter, value: Type) { - out.value(value.name.toLowerCase()) - } - - @Throws(IOException::class) - override fun read(`in`: JsonReader): Type { - for (t in Type.values()) { - if (t.name.equals(`in`.nextString(), ignoreCase = true)) { - return t - } - } - throw JsonParseException("Unknown whisper type: " + `in`.nextString()) - } - } - } -} \ No newline at end of file diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/bits.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/bits.kt index 96319b4..c92589f 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/bits.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/bits.kt @@ -1,6 +1,9 @@ package glitch.pubsub.events.json +import com.fatboyindustrial.gsonjavatime.InstantConverter +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName +import java.time.Instant /** * @@ -30,7 +33,8 @@ data class Data( @SerializedName("chat_message") val message: String, val context: String, - val time: String, + @JsonAdapter(InstantConverter::class) + val time: Instant, val totalBitsUsed: Int, val userId: Long?, @SerializedName("user_name") diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/messages.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/messages.kt index fc8cf68..f58e4cd 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/messages.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/messages.kt @@ -1,5 +1,7 @@ package glitch.pubsub.events.json +import com.fatboyindustrial.gsonjavatime.InstantConverter +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName import java.time.Instant @@ -24,6 +26,7 @@ data class Commerce( val channelName: String, val userId: Long, val channelId: Long, + @JsonAdapter(InstantConverter::class) val time: Instant, val itemImageUrl: String, val itemDescription: String, diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/moderation.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/moderation.kt index 0e946cf..4f33d1c 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/moderation.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/moderation.kt @@ -60,14 +60,14 @@ data class Host( ) } -class MessageDelete( +data class MessageDelete( override val id: UUID, override val moderatorName: String, override val moderatorId: Long, override val targetName: String, override val targetId: Long, val message: String -): IDObject, IModerator, ITarget { +) : IDObject, IModerator, ITarget { constructor(data: ModerationData) : this( UUID.fromString(data.args[2]), data.createdBy, @@ -78,7 +78,7 @@ class MessageDelete( ) } -class ModerationData( +data class ModerationData( val type: String, @JsonAdapter(ModerationActionAdapter::class) val moderationAction: Action, diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/subscription.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/subscription.kt index 03c9c72..e350169 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/subscription.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/subscription.kt @@ -1,5 +1,7 @@ package glitch.pubsub.events.json +import com.fatboyindustrial.gsonjavatime.InstantConverter +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName import glitch.api.objects.enums.SubscriptionType import glitch.pubsub.`object`.enums.SubscriptionContext @@ -12,6 +14,7 @@ data class GiftSubscriptionMessage( override val channelName: String, override val userId: Long, override val channelId: Long, + @JsonAdapter(InstantConverter::class) override val time: Instant, @SerializedName("sub_plan") override val subscriptionType: SubscriptionType, @@ -33,6 +36,7 @@ data class SubscriptionMessage( override val channelName: String, override val userId: Long, override val channelId: Long, + @JsonAdapter(InstantConverter::class) override val time: Instant, @SerializedName("sub_plan") override val subscriptionType: SubscriptionType, diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/video_playback.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/video_playback.kt index 60be272..688736d 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/video_playback.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/video_playback.kt @@ -1,6 +1,7 @@ package glitch.pubsub.events.json import com.google.gson.annotations.JsonAdapter +import glitch.pubsub.`object`.adapters.ServerTimeAdapter import glitch.pubsub.`object`.adapters.VideoPlaybackTypeAdapter import java.time.Instant @@ -13,6 +14,7 @@ import java.time.Instant data class VideoPlayback( @JsonAdapter(VideoPlaybackTypeAdapter::class) val type: Type, + @JsonAdapter(ServerTimeAdapter::class) val serverTime: Instant ) { @@ -36,6 +38,7 @@ data class VideoPlayback( * @since 1.0 */ data class StreamUp( + @JsonAdapter(ServerTimeAdapter::class) val serverTime: Instant, val delay: Int ) @@ -46,7 +49,11 @@ data class StreamUp( * @version %I%, %G% * @since 1.0 */ -class ViewCount(val serverTime: Instant, val viewers: Long) +data class ViewCount( + @JsonAdapter(ServerTimeAdapter::class) + val serverTime: Instant, + val viewers: Long +) /** * @@ -55,5 +62,6 @@ class ViewCount(val serverTime: Instant, val viewers: Long) * @since 1.0 */ data class StreamDown( + @JsonAdapter(ServerTimeAdapter::class) val serverTime: Instant ) \ No newline at end of file diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/json/whisper.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/json/whisper.kt index 53259bd..c271aaf 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/json/whisper.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/json/whisper.kt @@ -1,5 +1,7 @@ package glitch.pubsub.events.json +import com.fatboyindustrial.gsonjavatime.InstantConverter +import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName import glitch.api.objects.enums.UserType import glitch.api.objects.json.Badge @@ -16,6 +18,7 @@ import java.time.Instant data class WhisperThread( val isArchived: Boolean, val isMuted: Boolean, + @JsonAdapter(InstantConverter::class) val whitelistedUntil: Instant ) @@ -30,6 +33,7 @@ data class WhisperMessage( @SerializedName("body") val message: String, @SerializedName("sent_ts") + @JsonAdapter(InstantConverter::class) val createdAt: Instant, @SerializedName("from_id") val senderId: Long, diff --git a/pubsub/src/main/kotlin/glitch/pubsub/events/pubsub.kt b/pubsub/src/main/kotlin/glitch/pubsub/events/pubsub.kt index f7be384..cab3a88 100644 --- a/pubsub/src/main/kotlin/glitch/pubsub/events/pubsub.kt +++ b/pubsub/src/main/kotlin/glitch/pubsub/events/pubsub.kt @@ -1,6 +1,5 @@ package glitch.pubsub.events -import com.google.gson.Gson import com.google.gson.JsonObject import glitch.api.ws.events.IEvent import glitch.pubsub.GlitchPubSub @@ -15,7 +14,6 @@ import glitch.pubsub.exceptions.PubSubException */ data class PubSubEvent( override val client: GlitchPubSub, - val mapper: Gson, val data: JsonObject ) : IEvent