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.io.IOException
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 = ""

    private fun getDefaultDrawingIf(drawing: ByteArray? = null): CompletableFuture<ByteArray?> {
        return if (drawing != null) CompletableFuture.completedFuture(drawing) else logValueFuture {
            try {
                HttpClients.createDefault().use {
                    val dataGet = HttpGet(Configure.www.taehui + "/avatar/drawing")
                    dataGet.setHeader("X-Real-IP", avatarIP)
                    it.execute(dataGet, HCDataHandler())
                }
            } catch (e: IOException) {
                logFault(e)
                null
            }
        }
    }

    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" -> DB.getNote(params).thenAccept { send(ctx, it) }

                    "/qwilight/www/comment" -> {
                        val noteID = params.getOrDefault("noteID", "")
                        if (BannedNoteFile.isBanned(noteID)) {
                            send(ctx, object {
                                val favor = null
                                val totalFavor = 0
                                val comments = null
                            })
                        } else {
                            val commentID = params.getOrDefault("commentID", "")
                            val target = params.getOrDefault("target", "false").toBoolean()
                            if (noteID.isEmpty()) {
                                send400(ctx)
                            } else {
                                if (commentID.isEmpty()) {
                                    DB.getComment(noteID, avatarID, language, target, 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" -> DB.getAvatar(params).thenAccept {
                        if (it != null) {
                            send(ctx, it)
                        } else {
                            send204(ctx)
                        }
                    }

                    "/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/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/wwwLevels" -> DB.getAvatarWwwLevels(avatarID).thenAccept {
                        send(ctx, it)
                    }

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

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

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

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

                    "/qwilight/www/hof/totalTotal" -> DB.getHOFTotalTotal().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/atTotal" -> DB.getHOFAtTotal().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/totalHighest" -> DB.getHOFTotalHighest().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/atHighest" -> DB.getHOFAtHighest().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/totalStand" -> DB.getHOFTotalStand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/atStand" -> DB.getHOFAtStand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/totalBand" -> DB.getHOFTotalBand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/atBand" -> DB.getHOFAtBand().thenAccept { send(ctx, it) }
                    "/qwilight/www/hof/ability/5K" -> DB.getHOFAbility(Component.InputMode.INPUT_MODE_5_1)
                        .thenAccept { send(ctx, it) }

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

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

                    "/qwilight/www/hof/level" -> DB.getHOFLevel().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 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(params).thenAccept {
                        if (it != null) {
                            send(ctx, it)
                        } else {
                            send204(ctx)
                        }
                    }

                    "/qwilight/www/titles" -> DB.getTitles(
                        params
                    ).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(
                        params
                    ).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 levelName = params.getOrDefault("levelName", "")
                        val levelID = params.getOrDefault("levelID", "")
                        if (drawingVariety.isNotEmpty()) {
                            if (avatarID.isEmpty()) {
                                when (drawingVariety) {
                                    "0" -> getDefaultDrawingIf().thenAccept { send(ctx, it) }
                                    "2" -> send(ctx, EdgeSystem.getDrawing("Default"))
                                    else -> send400(ctx)
                                }
                            } else if (avatarID.startsWith("*")) {
                                when (drawingVariety) {
                                    "0" -> getDefaultDrawingIf(ValveSystem.getDrawing(avatarID)).thenAccept {
                                        send(
                                            ctx,
                                            it
                                        )
                                    }

                                    "2" -> send(ctx, EdgeSystem.getDrawing("Default"))
                                    else -> send400(ctx)
                                }
                            } else if (avatarID.startsWith("$")) {
                                when (drawingVariety) {
                                    "0" -> getDefaultDrawingIf(PlatformSystem.getDrawing(avatarID)).thenAccept {
                                        send(
                                            ctx,
                                            it
                                        )
                                    }

                                    "2" -> send(ctx, EdgeSystem.getDrawing("Default"))
                                    else -> send400(ctx)
                                }
                            } else {
                                when (drawingVariety) {
                                    "0" -> 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()))
                                        }
                                    }

                                    "2" -> logFuture {
                                        val edge = DB.getAvatarEdge(Utility.getDefaultAvatarID(avatarID))
                                        send(ctx, EdgeSystem.getDrawing(edge))
                                    }

                                    else -> send400(ctx)
                                }
                            }
                        } else if (abilityClass5K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getAbilityClass(
                                    AbilityClassSystem.AbilityClassVariety.INPUT_MODE_5K,
                                    abilityClass5K.toDouble()
                                )
                            )
                        } else if (abilityClass7K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getAbilityClass(
                                    AbilityClassSystem.AbilityClassVariety.INPUT_MODE_7K,
                                    abilityClass7K.toDouble()
                                )
                            )
                        } else if (abilityClass9K.isNotEmpty()) {
                            send(
                                ctx,
                                AbilityClassSystem.getAbilityClass(
                                    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/note" -> {
                        val noteFileData = ByteBufUtil.getBytes(msg.content())
                        val targetComputing = BaseCompiler.handleCompile(noteFileData, 0).single()
                        if (targetComputing.isBanned || BannedNoteFile.isBanned(targetComputing.noteID)) {
                            send403(ctx)
                        } else {
                            logFuture {
                                Files.write(
                                    TwilightComponent.NOTE_ENTRY_PATH.resolve(Utility.getID512(noteFileData)),
                                    noteFileData
                                )
                                DB.setNote(
                                    targetComputing.noteID,
                                    Utility.getID128(noteFileData),
                                    Utility.getID256(noteFileData),
                                    targetComputing
                                )
                                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))
        }
    }
}