Newer
Older
Twilight / src / main / kotlin / net / taehui / twilight / www / WwwAvatar.kt
package net.taehui.twilight.www

import com.fasterxml.jackson.databind.ObjectMapper
import io.netty.buffer.ByteBufUtil
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.*
import io.netty.util.CharsetUtil
import net.taehui.twilight.*
import net.taehui.twilight.system.*
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream
import org.apache.commons.io.IOUtils
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.slf4j.LoggerFactory
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.util.concurrent.CompletableFuture
import kotlin.io.path.inputStream

class WwwAvatar : SimpleChannelInboundHandler<FullHttpRequest>(), Logger {
    private var avatarIP = ""
    private var avatarEstimatedID = ""

    fun send(handler: ChannelHandlerContext, text: Any?) {
        val r = DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1,
            HttpResponseStatus.OK,
            Unpooled.copiedBuffer(ObjectMapper().writeValueAsString(text), CharsetUtil.UTF_8)
        )
        val hs = r.headers()
        hs[HttpHeaderNames.CONTENT_TYPE] = "application/json"
        hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8
        handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE)
    }

    fun send(handler: ChannelHandlerContext, text: Double) {
        val r = DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1,
            HttpResponseStatus.OK,
            Unpooled.copiedBuffer(text.toString(), CharsetUtil.UTF_8)
        )
        val hs = r.headers()
        hs[HttpHeaderNames.CONTENT_TYPE] = "text/plain"
        hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8
        handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE)
    }

    fun send(handler: ChannelHandlerContext, text: String) {
        val r = DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(
                text.toByteArray(
                    StandardCharsets.UTF_8
                )
            )
        )
        val hs = r.headers()
        hs[HttpHeaderNames.CONTENT_TYPE] = "text/plain"
        hs[HttpHeaderNames.CONTENT_ENCODING] = CharsetUtil.UTF_8
        handler.writeAndFlush(r).addListener(ChannelFutureListener.CLOSE)
    }

    fun send(handler: ChannelHandlerContext, data: ByteArray?) {
        if (data != null) {
            handler.writeAndFlush(
                DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(data)
                )
            ).addListener(
                ChannelFutureListener.CLOSE
            )
        } else {
            send204(handler)
        }
    }

    private fun send204(handler: ChannelHandlerContext) {
        handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT)).addListener(
            ChannelFutureListener.CLOSE
        )
    }

    private fun send400(handler: ChannelHandlerContext) {
        handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST))
            .addListener(
                ChannelFutureListener.CLOSE
            )
    }

    private fun send403(handler: ChannelHandlerContext) {
        handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN)).addListener(
            ChannelFutureListener.CLOSE
        )
    }

    private fun send404(handler: ChannelHandlerContext) {
        handler.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)).addListener(
            ChannelFutureListener.CLOSE
        )
    }

    private val loggerID: String
        get() {
            val avatarID = if (avatarEstimatedID.isNotEmpty()) "$avatarEstimatedID?" else ""
            return if (avatarID.isNotEmpty()) "$avatarIP (${avatarID})" else avatarIP
        }

    public override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) {
        avatarIP = msg.headers()["X-Real-IP"] ?: "localhost"
        avatarEstimatedID = AvatarIPSystem.getAvatarID(avatarIP)
        val mode = msg.method()
        if (avatarIP != "localhost") {
            logInfo("$mode ${URLDecoder.decode(msg.uri(), StandardCharsets.UTF_8)}")
        }
        val data = QueryStringDecoder(msg.uri())
        when (mode) {
            HttpMethod.GET -> {
                val params = data.parameters().map {
                    Pair(it.key, it.value[0])
                }.toMap()
                val language = params.getOrDefault("language", "en-US")
                val avatarID = params.getOrDefault("avatarID", "")
                when (data.path()) {
                    "/qwilight/www/note" -> {
                        val viewUnit = params.getOrDefault("viewUnit", "20").toInt()
                        if (viewUnit > 20) {
                            send400(ctx)
                        } else {
                            DB.getNote(
                                params.getOrDefault("want", ""),
                                params.getOrDefault("src", "0").toInt(),
                                params.getOrDefault("fit", "0").toInt(),
                                params.getOrDefault("page", "1").toInt(),
                                viewUnit
                            ).thenAccept { send(ctx, it) }
                        }
                    }

                    "/qwilight/www/comment" -> {
                        val noteID = params.getOrDefault("noteID", "")
                        if (BannedNote.isBanned(noteID)) {
                            send(ctx, object {
                                val favor = null
                                val totalFavor = 0
                                val handled = null
                                val comments = null
                            })
                        } else {
                            if (noteID.isEmpty()) {
                                send400(ctx)
                            } else {
                                val commentID = params.getOrDefault("commentID", "")
                                if (commentID.isEmpty()) {
                                    val viewUnit = params.getOrDefault("viewUnit", "-1").toInt()
                                    val isUbuntu = params.getOrDefault("isUbuntu", "false").toBoolean()
                                    val ubuntuID = params.getOrDefault("ubuntuID", "")
                                    DB.getComment(
                                        noteID,
                                        Utility.getDefaultAvatarID(avatarID),
                                        language,
                                        viewUnit,
                                        isUbuntu,
                                        ubuntuID,
                                        this
                                    ).thenAccept {
                                        send(ctx, it)
                                    }
                                } else {
                                    XZCompressorInputStream(
                                        TwilightComponent.COMMENT_ENTRY_PATH.resolve(
                                            Utility.getNoteID512(noteID)
                                        ).resolve("$commentID.xz")
                                            .inputStream()
                                    ).use {
                                        send(ctx, IOUtils.toByteArray(it))
                                    }
                                }
                            }
                        }
                    }

                    "/qwilight/www/avatar" -> {
                        val want = params.getOrDefault("want", "")
                        if (want.isEmpty()) {
                            send400(ctx)
                        }else {
                            DB.getAvatar(want)
                                .thenAccept {
                                    if (it != null) {
                                        send(ctx, it)
                                    } else {
                                        send204(ctx)
                                    }
                                }
                        }
                    }

                    "/qwilight/www/avatar/favorites/4K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_4,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/6K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_6,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/5K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_5_1,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/7K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_7_1,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/9K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_9,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/10K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_10_2,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/14K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_14_2,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/24K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_24_2,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/favorites/48K" -> DB.getAvatarFavorites(
                        Component.InputMode.INPUT_MODE_48_4,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/lasts/4K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_4, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/6K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_6, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/5K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_5_1, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/7K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_7_1, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/9K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_9, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/10K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_10_2, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/14K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_14_2, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/24K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_24_2, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/lasts/48K" -> DB.getAvatarLasts(Component.InputMode.INPUT_MODE_48_4, avatarID)
                        .thenAccept {
                            send(ctx, it)
                        }

                    "/qwilight/www/avatar/ability/5K" -> DB.getAvatarAbility(
                        Component.InputMode.INPUT_MODE_5_1,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/ability/7K" -> DB.getAvatarAbility(
                        Component.InputMode.INPUT_MODE_7_1,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/ability/9K" -> DB.getAvatarAbility(
                        Component.InputMode.INPUT_MODE_9,
                        avatarID
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/wwwLevels" -> DB.getAvatarWwwLevels(avatarID).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/handled" -> DB.getAvatarHandled(
                        avatarID,
                        params.getOrDefault("levelName", "")
                    ).thenAccept {
                        send(ctx, it)
                    }

                    "/qwilight/www/avatar/levelVS" -> {
                        val targetID = params.getOrDefault("targetID", "")
                        val levelName = params.getOrDefault("levelName", "")
                        if (avatarID.isEmpty() || targetID.isEmpty() || levelName.isEmpty() || avatarID == targetID) {
                            send400(ctx)
                        } else {
                            DB.getAvatarLevelVS(avatarID, targetID, levelName).thenAccept {
                                send(ctx, it)
                            }
                        }
                    }

                    "/qwilight/www/hall/totalTotal" -> DB.getHallTotalTotal().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/atTotal" -> DB.getHallAtTotal().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/totalTop" -> DB.getHallTotalTop().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/atTop" -> DB.getHallAtTop().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/totalStand" -> DB.getHallTotalStand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/atStand" -> DB.getHallAtStand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/totalBand" -> DB.getHallTotalBand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/atBand" -> DB.getHallAtBand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hall/ability/5K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_5_1)
                        .thenAccept { send(ctx, it) }

                    "/qwilight/www/hall/ability/7K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_7_1)
                        .thenAccept { send(ctx, it) }

                    "/qwilight/www/hall/ability/9K" -> DB.getHallAbility(Component.InputMode.INPUT_MODE_9)
                        .thenAccept { send(ctx, it) }

                    "/qwilight/www/hall/level" -> DB.getHallLevel().thenAccept { send(ctx, it) }

                    "/qwilight/www/etc" -> {
                        DB.getEtc(language).thenAccept { send(ctx, it) }
                    }

                    "/qwilight/www/level" -> {
                        val levelName = params.getOrDefault("levelName", "")
                        if (levelName.isEmpty()) {
                            val levelID = params.getOrDefault("levelID", "")
                            if (levelID.isEmpty()) {
                                if (params.containsKey("avatarID")) {
                                    send(ctx, LevelSystem.getLevelNames(avatarID))
                                } else {
                                    val avatarIDMe = params.getOrDefault("avatarIDMe", "")
                                    if (avatarIDMe.isEmpty()) {
                                        send(ctx, object {
                                            val avatars = LevelSystem.avatars
                                            val levelIDs = emptyArray<String>()
                                        })
                                    } else {
                                        DB.getClearedLevelIDs(
                                            avatarIDMe
                                        ).thenAccept {
                                            send(ctx, object {
                                                val avatars = LevelSystem.avatars
                                                val levelIDs = it
                                            })
                                        }
                                    }
                                }
                            } else {
                                LevelSystem.getLevelItem(levelID)?.let { levelItem ->
                                    DB.getLevelNote(levelItem).thenAccept {
                                        send(
                                            ctx, object {
                                                val levelNote = it
                                                val stand = levelItem.stand
                                                val point = levelItem.point
                                                val band = levelItem.band
                                                val judgments = levelItem.judgments
                                                val autoMode = levelItem.autoMode
                                                val noteSaltMode = levelItem.noteSaltMode
                                                val audioMultiplier = levelItem.audioMultiplier
                                                val faintNoteMode = levelItem.faintNoteMode
                                                val judgmentMode = levelItem.judgmentMode
                                                val hitPointsMode = levelItem.hitPointsMode
                                                val noteMobilityMode = levelItem.noteMobilityMode
                                                val longNoteMode = levelItem.longNoteMode
                                                val inputFavorMode = levelItem.inputFavorMode
                                                val noteModifyMode = levelItem.noteModifyMode
                                                val bpmMode = levelItem.bpmMode
                                                val waveMode = levelItem.waveMode
                                                val setNoteMode = levelItem.setNoteMode
                                                val lowestJudgmentConditionMode = levelItem.lowestJudgmentConditionMode
                                                val allowPause = levelItem.allowPause
                                                val avatars = DB.getClearedAvatars(levelID)
                                                val titles = TitleSystem.getTitles(levelID, language)
                                                val edgeIDs = EdgeSystem.getEdgeIDs(levelID)
                                            }
                                        )
                                    }
                                }
                            }
                        } else {
                            val levelGroup = LevelSystem.getLevelGroup(levelName)
                            if (levelGroup != null) {
                                DB.getLevels(
                                    levelGroup
                                ).thenAccept { send(ctx, it) }
                            }
                        }
                    }

                    "/qwilight/www/vote" -> {
                        val voteName = params.getOrDefault("voteName", "")
                        if (voteName.isEmpty()) {
                            send(ctx, VoteSystem.voteNames)
                        } else {
                            send(ctx, VoteSystem.getVoteItems(voteName))
                        }
                    }

                    "/qwilight/www/sites" -> send(ctx, SiteHandler.getCalledSites())

                    "/qwilight/www/defaultNoteDate" -> {
                        val defaultNoteDate = Configure.defaultNoteDate
                        if (defaultNoteDate > params.getOrDefault("date", "0").toLong()) {
                            send(ctx, object {
                                val date = defaultNoteDate
                            })
                        } else {
                            send204(ctx)
                        }
                    }

                    "/qwilight/www/defaultUIDate" -> {
                        val defaultUIDate = Configure.defaultUIDate
                        if (defaultUIDate > params.getOrDefault("date", "0").toLong()) {
                            send(ctx, object {
                                val date = defaultUIDate
                            })
                        } else {
                            send204(ctx)
                        }
                    }

                    "/qwilight/www/title" -> DB.getTitle(Utility.getDefaultAvatarID(avatarID), language).thenAccept {
                        if (it != null) {
                            send(ctx, it)
                        } else {
                            send204(ctx)
                        }
                    }

                    "/qwilight/www/titles" -> DB.getTitles(
                        Utility.getDefaultAvatarID(avatarID), language
                    ).thenAccept { send(ctx, it) }

                    "/qwilight/www/edge" -> {
                        val drawing = EdgeSystem.getDrawing(params.getOrDefault("edgeID", ""))
                        if (drawing != null) {
                            send(ctx, drawing)
                        } else {
                            send204(ctx)
                        }
                    }

                    "/qwilight/www/edges" -> DB.getEdgeIDs(
                        Utility.getDefaultAvatarID(avatarID)
                    ).thenAccept { send(ctx, it) }

                    "/qwilight/www/drawing" -> {
                        val drawingVariety = params.getOrDefault("drawingVariety", "")
                        val abilityClass5K = params.getOrDefault("abilityClass5K", "")
                        val abilityClass7K = params.getOrDefault("abilityClass7K", "")
                        val abilityClass9K = params.getOrDefault("abilityClass9K", "")
                        val levelID = params.getOrDefault("levelID", "")
                        if (drawingVariety.isNotEmpty()) {
                            when (drawingVariety) {
                                "0" -> {
                                    fun sendAvatarDrawing(avatarID: String): CompletableFuture<Void> {
                                        return logFuture {
                                            HttpClients.createDefault().use {
                                                val dataGet = HttpGet(
                                                    Configure.www.taehui + "/avatar/drawing/" + Utility.getDefaultAvatarID(
                                                        avatarID
                                                    )
                                                )
                                                dataGet.setHeader("X-Real-IP", avatarIP)
                                                send(ctx, it.execute(dataGet, HCDataHandler()))
                                            }
                                        }
                                    }

                                    if (avatarID.startsWith("*")) {
                                        val avatarDrawing = ValveSystem.getDrawing(Utility.getDefaultAvatarID(avatarID))
                                        if (avatarDrawing != null) {
                                            send(ctx, avatarDrawing)
                                        } else {
                                            sendAvatarDrawing(avatarID)
                                        }
                                    } else if (avatarID.startsWith("$")) {
                                        if (PlatformIDSystem.hasAvatarID(avatarID)) {
                                            sendAvatarDrawing(avatarID)
                                        } else {
                                            val avatarDrawing = PlatformSystem.getDrawing(avatarID)
                                            if (avatarDrawing != null) {
                                                send(ctx, avatarDrawing)
                                            } else {
                                                sendAvatarDrawing(avatarID)
                                            }
                                        }
                                    } else {
                                        sendAvatarDrawing(avatarID)
                                    }
                                }

                                "2" -> DB.getAvatarEdge(Utility.getDefaultAvatarID(avatarID)).thenAccept {
                                    send(ctx, EdgeSystem.getDrawing(it))
                                }

                                else -> send400(ctx)
                            }
                        } else if (abilityClass5K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getDrawing(
                                    AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K,
                                    abilityClass5K.toDouble()
                                )
                            )
                        } else if (abilityClass7K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getDrawing(
                                    AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K,
                                    abilityClass7K.toDouble()
                                )
                            )
                        } else if (abilityClass9K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getDrawing(
                                    AbilityClassSystem.AbilityClassVariety.INPUT_MODE_9K,
                                    abilityClass9K.toDouble()
                                )
                            )
                        } else if (levelID.isNotEmpty()) {
                            send(ctx, LevelSystem.getLevelIDDrawing(levelID))
                        } else {
                            send400(ctx)
                        }
                    }

                    else -> send404(ctx)
                }
            }

            HttpMethod.POST -> {
                when (data.path()) {
                    "/qwilight/www/fault" -> {
                        QwilightLogging(loggerID).logInfo(
                            msg.content().toString(StandardCharsets.UTF_8)
                        )
                        ctx.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED))
                            .addListener(
                                ChannelFutureListener.CLOSE
                            )
                    }

                    "/qwilight/www/toil" -> {
                        QwilightLogging(loggerID).logInfo(
                            msg.content().toString(StandardCharsets.UTF_8)
                        )
                        ctx.writeAndFlush(DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED))
                            .addListener(
                                ChannelFutureListener.CLOSE
                            )
                    }

                    "/qwilight/www/note" -> {
                        val noteFileData = ByteBufUtil.getBytes(msg.content())
                        var isBanned = false
                        BaseCompiler.handleCompile(noteFileData, -1).forEach {
                            if (it.isBanned || BannedNote.isBanned(it.noteID)) {
                                isBanned = true
                            } else {
                                logFuture {
                                    Files.write(
                                        TwilightComponent.NOTE_ENTRY_PATH.resolve(Utility.getID512(noteFileData)),
                                        noteFileData
                                    )
                                    DB.setNote(
                                        it.noteID,
                                        Utility.getID128(noteFileData),
                                        Utility.getID256(noteFileData),
                                        it
                                    )
                                }
                            }
                        }
                        if (isBanned) {
                            send403(ctx)
                        } else {
                            send204(ctx)
                        }
                    }

                    else -> {
                        send404(ctx)
                    }
                }
            }

            else -> {
                ctx.writeAndFlush(
                    DefaultFullHttpResponse(
                        HttpVersion.HTTP_1_1,
                        HttpResponseStatus.METHOD_NOT_ALLOWED
                    )
                )
                    .addListener(
                        ChannelFutureListener.CLOSE
                    )
            }
        }
    }

    override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
        logFault(cause)
    }

    override fun logInfo(toNotify: String) {
        LoggerFactory.getLogger(javaClass).info("[{}] {}", loggerID, toNotify)
    }

    override fun logFault(e: Throwable) {
        if (Utility.isValidFault(e)) {
            LoggerFactory.getLogger(javaClass).error("[{}] {}", avatarIP, Utility.getFaultText(e))
        }
    }
}