Newer
Older
Twilight / src / main / kotlin / net / taehui / twilight / qwilight / QwilightAvatar.kt
package net.taehui.twilight.qwilight

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import net.taehui.twilight.*
import net.taehui.twilight.BundleIO.BundleVariety
import net.taehui.twilight.BundleIO.BundleVariety.Companion.getBundleVariety
import net.taehui.twilight.Component
import net.taehui.twilight.system.*
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.RandomStringUtils
import org.slf4j.LoggerFactory
import java.net.InetAddress
import java.net.InetSocketAddress
import java.nio.channels.AsynchronousFileChannel
import java.nio.channels.CompletionHandler
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.sql.Timestamp
import java.time.LocalDateTime
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.nameWithoutExtension

class QwilightAvatar : SimpleChannelInboundHandler<EventOuterClass.Event>(), Avatar {
    override val availableCSX = Any()
    override val siteYellMillis = LinkedList<Long>()
    override var lastLogInDate: LocalDateTime = LocalDateTime.MIN
    override lateinit var handler: ChannelHandlerContext
    override var isAwilight = false
    override var isAvailable = true
    private val defaultNoteNames = LinkedList<String>()
    private val defaultUINames = LinkedList<String>()
    override lateinit var avatarIP: InetAddress
    override var avatarEstimatedID = ""
    private val savingBundleIOs = ConcurrentHashMap<String, BundleIO>()
    override var isEstablished = false
    override var isLoggedIn = false
    override var isValve = false
    override var qwilightHash = ""
    override var qwilightDate = ""
    override var language = "ko-KR"
    override var qwilightID = ""
    override var qwilightName = ""
    override var avatarID = ""
    override var avatarName = ""
    override var avatarCompetence = 1
    override var situationValue = NOTE_FILE_MODE
    override var situationText = ""
    override val isEnterQuitAware = true
    override val avatarNameTitle = ""

    override fun send(eventID: EventOuterClass.Event.EventID, text: Any?, vararg data: ByteString?): ChannelFuture {
        return handler.writeAndFlush(EventOuterClass.Event.newBuilder().apply {
            this.eventID = eventID
            if (text != null) {
                this.text = if (text is String) text else ObjectMapper().writeValueAsString(text)
            }
            this.addAllData(data.toList())
        }.build())
    }

    override fun send(event: EventOuterClass.Event) {
        handler.writeAndFlush(event)
    }

    private fun hasAvailableBundleIOs(): Boolean {
        return savingBundleIOs.size < BUNDLE_AVAILABLE_COUNT
    }

    private fun doCallBundle(targetAvatarID: String, isSilent: Boolean) {
        DB.getBundles(
            Utility.getDefaultAvatarID(targetAvatarID)
        ).thenAccept { bundles ->
            if (bundles != null) {
                send(EventOuterClass.Event.EventID.CALL_BUNDLE, object {
                    val targetAvatar = targetAvatarID
                    val targetValue = bundles.second
                    val bundleLength = TwilightComponent.BUNDLE_LENGTH

                    @JvmField
                    val isSilent = isSilent
                    val data = bundles.first.map {
                        object {
                            val date = it[0] as Timestamp
                            val bundleName = it[1] as String
                            val bundleLength = it[2] as Long
                            val bundleCompetence = it[3] as Int
                            val bundleVariety = it[4] as Int
                        }
                    }
                })
            }
        }
    }

    private fun translateTitleItem(target: TitleSystem.TitleItem): String {
        return when (language) {
            "ko-KR" -> target.title1042
            else -> target.title
        }
    }

    override fun toString(): String {
        return if (isEstablished) {
            when (situationValue) {
                NOTE_FILE_MODE -> "[$loggerID] Note File Mode ($situationText)"
                DEFAULT_COMPUTING -> "[$loggerID] Default Computing ($situationText)"
                COMMENT_COMPUTING -> "[$loggerID] Comment Computing ($situationText)"
                AUTO_COMPUTING -> "[$loggerID] Auto Computing ($situationText)"
                QUIT_MODE -> "[$loggerID] Quit Computing ($situationText)"
                NET_COMPUTING -> "[$loggerID] Net Computing ($situationText)"
                IO_COMPUTING -> "[$loggerID] IO Computing ($situationText)"
                else -> "[$loggerID] Idle"
            }
        } else "[$loggerID] Not Established"
    }

    override fun channelActive(ctx: ChannelHandlerContext) {
        handler = ctx
        avatarIP = (handler.channel().remoteAddress() as InetSocketAddress).address
        isAwilight = avatarIP.isLoopbackAddress
        avatarEstimatedID = AvatarIPSystem.getAvatarID(remote)
    }

    override fun channelInactive(ctx: ChannelHandlerContext) {
        AvatarHandler.quitAvatar(avatarID)
    }

    public override fun channelRead0(ctx: ChannelHandlerContext, msg: EventOuterClass.Event) {
        doIfValid(msg) {
            doIfAvailable {
                val jm = ObjectMapper()
                val eventText = msg.text
                val eventData = msg.dataList

                fun saveDefaultNote(eventText: String) {
                    val text = if (eventText.isEmpty()) null else jm.readValue(eventText, Array<String>::class.java)
                    if (text != null) {
                        if (defaultNoteNames.isEmpty()) {
                            Files.list(TwilightComponent.DEFAULT_NOTE_ENTRY).use { defaultNoteFilePaths ->
                                defaultNoteNames.addAll(defaultNoteFilePaths.map { defaultNoteFilePath ->
                                    defaultNoteFilePath.nameWithoutExtension
                                }.toList().subtract(text.toSet()))
                            }
                        } else {
                            send(EventOuterClass.Event.EventID.ALREADY_LOADING_BUNDLE, null)
                            return
                        }
                    }
                    defaultNoteNames.poll().let {
                        if (it != null) {
                            val saveBundleID = UUID.randomUUID().toString()
                            val bundleFileName = "$it.zip"
                            val bundleFilePath = TwilightComponent.DEFAULT_NOTE_ENTRY.resolve(bundleFileName)
                            val fileChannel = AsynchronousFileChannel.open(bundleFilePath)
                            savingBundleIOs[saveBundleID] = BundleIO(
                                bundleFilePath,
                                fileChannel,
                                it,
                                BundleVariety.DEFAULT_NOTE_FILE,
                                "",
                                bundleFileName,
                                false
                            )
                            send(
                                EventOuterClass.Event.EventID.SAVE_BUNDLE,
                                object {
                                    val bundleID = saveBundleID
                                    val bundleVariety = BundleVariety.DEFAULT_NOTE_FILE
                                    val bundleLength = fileChannel.size()
                                }
                            )
                        }
                    }
                }

                fun saveDefaultUI(eventText: String) {
                    if (eventText.toBoolean()) {
                        if (defaultUINames.isEmpty()) {
                            Files.list(TwilightComponent.DEFAULT_UI_ENTRY).use { defaultUIFilePaths ->
                                defaultUINames.addAll(defaultUIFilePaths.map {
                                    it.nameWithoutExtension
                                }.toList())
                            }
                        } else {
                            send(EventOuterClass.Event.EventID.ALREADY_LOADING_BUNDLE, null)
                            return
                        }
                    }
                    defaultUINames.poll().let {
                        if (it != null) {
                            val saveBundleID = UUID.randomUUID().toString()
                            val bundleFileName = "$it.zip"
                            val bundleFilePath = TwilightComponent.DEFAULT_UI_ENTRY.resolve(bundleFileName)
                            val fileChannel = AsynchronousFileChannel.open(bundleFilePath)
                            savingBundleIOs[saveBundleID] = BundleIO(
                                bundleFilePath,
                                fileChannel,
                                it,
                                BundleVariety.DEFAULT_UI,
                                "",
                                bundleFileName,
                                false
                            )
                            send(
                                EventOuterClass.Event.EventID.SAVE_BUNDLE,
                                object {
                                    val bundleID = saveBundleID
                                    val bundleVariety = BundleVariety.DEFAULT_UI
                                    val bundleLength = fileChannel.size()
                                }
                            )
                        }
                    }
                }

                if (!isAwilight) {
                    logEvent(msg)
                }

                when (msg.eventID) {
                    EventOuterClass.Event.EventID.ESTABLISH -> wantNotEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightEstablish::class.java)
                        val isValve = eventData.size > 0
                        language = text.language
                        qwilightHash = text.hash
                        qwilightDate = text.date
                        if (Configure.mode.pause) {
                            send(EventOuterClass.Event.EventID.WARNING, translateLanguage("pause"))
                            val avatarID = "*" + RandomStringUtils.randomAlphanumeric(19)
                            if (isValve) {
                                ValveSystem.putDrawing(avatarID, eventData[0])
                            }
                            AvatarHandler.establish(this, avatarID, text.qwilightName, isValve, true)
                        } else if (BannedIP.isBanned(remote)) {
                            send(EventOuterClass.Event.EventID.WARNING, translateLanguage("bannedIP"))
                        } else if (avatarIP.isLoopbackAddress || avatarIP.isSiteLocalAddress || Configure.hash.contains(
                                qwilightHash
                            )
                        ) {
                            val avatarID = "*" + RandomStringUtils.randomAlphanumeric(19)
                            if (isValve) {
                                ValveSystem.putDrawing(avatarID, eventData[0])
                            }
                            AvatarHandler.establish(this, avatarID, text.qwilightName, isValve, false)
                        } else {
                            if (isValve) {
                                send(EventOuterClass.Event.EventID.WARNING, translateLanguage("unavailableValveDate"))
                            } else {
                                send(
                                    EventOuterClass.Event.EventID.UNAVAILABLE_DATE,
                                    translateLanguage("unavailableDate")
                                )
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.LOG_IN -> wantNotLoggedIn {
                        if (allowLogIn()) {
                            tryLogInDate()
                            val text = jm.readValue(eventText, JSON.QwilightLogIn::class.java)
                            handleLogIn(this, text.avatarID, text.avatarCipher).thenAccept {
                                if (it != null) {
                                    val avatarCompetence = it[3] as Int
                                    if (avatarCompetence >= 1) {
                                        val avatarID = it[1] as String
                                        val avatarName = it[2] as String
                                        AvatarHandler.handleLogIn(
                                            this,
                                            it[0] as String,
                                            avatarID,
                                            avatarName,
                                            avatarCompetence
                                        )
                                        AvatarIPSystem.putAvatarIP(this, avatarID)
                                        DB.getNotifyUbuntu(avatarID)
                                            .forEach { ubuntuID ->
                                                AvatarHandler.getAvatar(
                                                    ubuntuID
                                                )?.let { ubuntu ->
                                                    if (DB.isCrossUbuntu(avatarID, ubuntuID)) {
                                                        ubuntu.send(
                                                            EventOuterClass.Event.EventID.NOTIFY_INFO,
                                                            String.format(
                                                                ubuntu.translateLanguage("ubuntuNotify"),
                                                                avatarName
                                                            )
                                                        )
                                                    }
                                                }
                                            }
                                    } else {
                                        send(
                                            EventOuterClass.Event.EventID.NOTIFY_INFO,
                                            translateLanguage("unavailableAvatar")
                                        )
                                    }
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.NOT_LOG_IN -> wantLoggedIn {
                        AvatarHandler.handleNotLogIn(
                            it
                        )
                    }

                    EventOuterClass.Event.EventID.CALL_UBUNTU -> wantLoggedIn { avatarID ->
                        DB.getUbuntuIDs(avatarID).thenAccept { ubuntuIDs ->
                            send(
                                EventOuterClass.Event.EventID.CALL_UBUNTU,
                                ubuntuIDs.stream().map {
                                    val ubuntuID = it[0]
                                    val situationValue: Int
                                    val situationText: String
                                    if (DB.isCrossUbuntu(avatarID, ubuntuID)) {
                                        val avatarUbuntu =
                                            AvatarHandler.getAvatar(ubuntuID)
                                        situationValue = avatarUbuntu?.situationValue ?: NOT_SIGNED_IN
                                        situationText =
                                            avatarUbuntu?.situationText ?: DB.getLastDate(ubuntuID).toString()
                                    } else {
                                        situationValue = NOT_UBUNTU
                                        situationText = ""
                                    }
                                    object {
                                        val ubuntuID = ubuntuID
                                        val ubuntuName = it[1]
                                        val situationValue = situationValue
                                        val situationText = situationText
                                    }
                                }.toArray()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.ENTER_SITE -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightEnterSite::class.java)
                        SiteHandler.enterSite(UUID.fromString(text.siteID), this, text.siteCipher)
                    }

                    EventOuterClass.Event.EventID.SITE_YELL -> wantEstablished {
                        SiteHandler.putSiteYell(
                            this,
                            jm.readValue(eventText, JSON.QwilightSiteYell::class.java),
                            true
                        )
                    }

                    EventOuterClass.Event.EventID.GET_SITE_YELLS -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightGetSiteYells::class.java)
                        SiteHandler.getSiteYells(UUID.fromString(text.siteID), this, text)
                    }

                    EventOuterClass.Event.EventID.SAVE_AS_BUNDLE -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightSaveAsBundle::class.java)
                        val bundleVariety = text.bundleVariety
                        val bundleName = text.bundleName
                        val etc = text.etc

                        fun saveAsBundle(bundleEntryPath: Path, saveAsBundleName: String, saveAsBundleID: String) {
                            if (hasAvailableBundleIOs()) {
                                if (!Files.isDirectory(bundleEntryPath)) {
                                    Files.createDirectories(bundleEntryPath)
                                }
                                val bundleFilePath = bundleEntryPath.resolve("$saveAsBundleName.tmp")
                                savingBundleIOs[saveAsBundleID] = BundleIO(
                                    bundleFilePath,
                                    AsynchronousFileChannel.open(
                                        bundleFilePath,
                                        StandardOpenOption.CREATE,
                                        StandardOpenOption.WRITE
                                    ),
                                    saveAsBundleName,
                                    getBundleVariety(bundleVariety),
                                    etc,
                                    FilenameUtils.getName(text.bundleEntryPath) + getBundleVariety(
                                        bundleVariety
                                    ).fileVariety,
                                    true
                                )
                                send(
                                    EventOuterClass.Event.EventID.SAVE_AS_BUNDLE,
                                    object {
                                        val bundleID = saveAsBundleID
                                        val bundleVariety = bundleVariety
                                        val bundleName = bundleName
                                        val bundleEntryPath = text.bundleEntryPath
                                    }
                                )
                            } else {
                                send(EventOuterClass.Event.EventID.ALREADY_LOADING_BUNDLE, null)
                            }
                        }

                        if (getBundleVariety(bundleVariety) == BundleVariety.NET) {
                            if (SiteHandler.isNetSiteHand(this, UUID.fromString(etc))) {
                                saveAsBundle(TwilightComponent.BUNDLE_ENTRY_PATH, etc, etc)
                            }
                        } else {
                            val saveAsBundleName =
                                bundleName.ifEmpty { UUID.randomUUID().toString() }
                            DB.hasBundleBefore(
                                avatarID,
                                saveAsBundleName
                            ).thenAccept {
                                if (it) {
                                    send(
                                        EventOuterClass.Event.EventID.WARNING,
                                        translateLanguage("alreadyBundle")
                                    )
                                } else {
                                    saveAsBundle(
                                        TwilightComponent.BUNDLE_ENTRY_PATH.resolve(avatarID),
                                        saveAsBundleName,
                                        UUID.randomUUID().toString()
                                    )
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.SAVING_AS_BUNDLE -> wantEstablished {
                        savingBundleIOs[eventText]?.let {
                            val savingAsBundleData = eventData[0].asReadOnlyByteBuffer()
                            it.fileChannel.write(
                                savingAsBundleData,
                                it.filePosition.toLong(),
                                it,
                                object : CompletionHandler<Int, BundleIO> {
                                    override fun completed(result: Int, attachment: BundleIO) = Unit

                                    override fun failed(exc: Throwable, attachment: BundleIO) {
                                        savingBundleIOs.remove(eventText)?.close()
                                        logFault(exc)
                                    }
                                })
                            if (it.saveFilePosition(savingAsBundleData.capacity())) {
                                savingBundleIOs.remove(eventText)?.close()
                                send(EventOuterClass.Event.EventID.STOP_SAVING_AS_BUNDLE, eventText)
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.STOP_SAVING_AS_BUNDLE -> wantEstablished {
                        savingBundleIOs.remove(eventText)?.close()
                    }

                    EventOuterClass.Event.EventID.SAVED_AS_BUNDLE -> wantEstablished { avatarID ->
                        savingBundleIOs.remove(eventText)?.let { savingBundleIO ->
                            savingBundleIO.fileChannel.write(
                                eventData[0].asReadOnlyByteBuffer(),
                                savingBundleIO.filePosition.toLong(),
                                savingBundleIO,
                                object : CompletionHandler<Int, BundleIO> {
                                    override fun completed(result: Int, attachment: BundleIO) {
                                        attachment.use {
                                            val etc = it.etc
                                            val bundleName = it.bundleName
                                            val bundleVariety = it.bundleVariety
                                            if (bundleVariety == BundleVariety.NET) {
                                                it.setFileVariety()
                                                val bundleFileName = it.bundleFileName
                                                SiteHandler.putSiteYell(
                                                    this@QwilightAvatar,
                                                    JSON.QwilightSiteYell().apply {
                                                        siteID = etc
                                                        siteYell = String.format(
                                                            translateLanguage("tryNetBundle"),
                                                            bundleFileName
                                                        )
                                                    },
                                                    false
                                                )
                                                SiteHandler.setNetBundle(
                                                    UUID.fromString(etc),
                                                    bundleFileName
                                                )
                                            } else {
                                                val bundleLength = it.getBundleLength()
                                                if (DB.isBundleLengthAvailable(avatarID, bundleLength)) {
                                                    it.setFileVariety()
                                                    DB.saveBundle(
                                                        avatarID,
                                                        bundleName,
                                                        bundleLength,
                                                        it.bundleVariety,
                                                        etc
                                                    )
                                                    doCallBundle(avatarID, true)
                                                } else {
                                                    send(
                                                        EventOuterClass.Event.EventID.WARNING,
                                                        translateLanguage("hasNotLength")
                                                    )
                                                }
                                            }
                                        }
                                    }

                                    override fun failed(exc: Throwable, attachment: BundleIO) {
                                        attachment.use {
                                            logFault(exc)
                                        }
                                    }
                                })
                        }
                    }

                    EventOuterClass.Event.EventID.SAVE_BUNDLE -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightSaveBundle::class.java)
                        val etc = text.etc
                        if (hasAvailableBundleIOs()) {
                            if (Utility.isEtcNetBundle(etc)) {
                                val bundleFileName = etc + BundleVariety.NET.fileVariety
                                val bundleFilePath = TwilightComponent.BUNDLE_ENTRY_PATH.resolve(bundleFileName)
                                val fileChannel = AsynchronousFileChannel.open(bundleFilePath)
                                savingBundleIOs[etc] = BundleIO(
                                    bundleFilePath,
                                    fileChannel,
                                    text.bundleName,
                                    BundleVariety.NET,
                                    etc,
                                    bundleFileName,
                                    false
                                )
                                send(
                                    EventOuterClass.Event.EventID.SAVE_BUNDLE,
                                    object {
                                        val bundleID = etc
                                        val bundleVariety = BundleVariety.NET
                                        val bundleLength = fileChannel.size()
                                    }
                                )
                            } else {
                                val targetAvatarID = Utility.getDefaultAvatarID(text.avatarID)
                                DB.getBundle(
                                    targetAvatarID,
                                    text.bundleName
                                ).thenAccept { bundle ->
                                    if (bundle != null) {
                                        val bundleCompetence = bundle[0] as Int
                                        val bundleVariety = bundle[1] as Int
                                        val bundleEtc = bundle[2] as String
                                        val bundleName = bundle[3] as String
                                        if (isSU || Utility.getDefaultAvatarID(avatarID) == targetAvatarID || bundleCompetence == BUNDLE_CALLABLE || bundleCompetence == BUNDLE_AVATAR && isLoggedIn || bundleCompetence == BUNDLE_UBUNTU && DB.isCrossUbuntu(
                                                avatarID,
                                                targetAvatarID
                                            )
                                        ) {
                                            val saveBundleID = UUID.randomUUID().toString()
                                            val bundleFileName =
                                                bundleName + getBundleVariety(bundleVariety).fileVariety
                                            val bundleFilePath =
                                                TwilightComponent.BUNDLE_ENTRY_PATH.resolve(targetAvatarID)
                                                    .resolve(bundleFileName)
                                            val fileChannel =
                                                AsynchronousFileChannel.open(bundleFilePath)
                                            savingBundleIOs[saveBundleID] = BundleIO(
                                                bundleFilePath,
                                                fileChannel,
                                                bundleName,
                                                getBundleVariety(bundleVariety),
                                                bundleEtc,
                                                bundleFileName,
                                                false
                                            )
                                            send(
                                                EventOuterClass.Event.EventID.SAVE_BUNDLE,
                                                object {
                                                    val bundleID = saveBundleID
                                                    val bundleVariety = bundleVariety
                                                    val bundleLength = fileChannel.size()
                                                }
                                            )
                                            val toNotifySaveBundle =
                                                DB.getNotifySaveBundle(targetAvatarID)
                                            if (toNotifySaveBundle == NOTIFY_SAVE_BUNDLE_CALLABLE || toNotifySaveBundle == NOTIFY_SAVE_BUNDLE_AVATAR && isLoggedIn || toNotifySaveBundle == NOTIFY_SAVE_BUNDLE_UBUNTU && DB.isCrossUbuntu(
                                                    avatarID,
                                                    targetAvatarID
                                                )
                                            ) {
                                                AvatarHandler.getAvatar(
                                                    targetAvatarID
                                                )?.let {
                                                    if (it !== this) {
                                                        it.send(
                                                            EventOuterClass.Event.EventID.NOTIFY_INFO,
                                                            String.format(
                                                                it.translateLanguage("bundleNotify"),
                                                                avatarName,
                                                                bundleName
                                                            )
                                                        )
                                                    }
                                                }
                                            }
                                        } else {
                                            send(
                                                EventOuterClass.Event.EventID.WARNING,
                                                translateLanguage("lowerCompetenceAsSave")
                                            )
                                        }
                                    }
                                }
                            }
                        } else {
                            send(EventOuterClass.Event.EventID.ALREADY_LOADING_BUNDLE, null)
                        }
                    }

                    EventOuterClass.Event.EventID.SAVE_DEFAULT_NOTE -> wantEstablished {
                        saveDefaultNote(eventText)
                    }

                    EventOuterClass.Event.EventID.SAVE_DEFAULT_UI -> wantEstablished {
                        saveDefaultUI(eventText)
                    }

                    EventOuterClass.Event.EventID.SAVING_BUNDLE -> wantEstablished {
                        savingBundleIOs[eventText]?.let { savingBundleIO ->
                            val savingBundleData = savingBundleIO.data
                            savingBundleIO.fileChannel.read(
                                savingBundleData,
                                savingBundleIO.filePosition.toLong(),
                                savingBundleIO,
                                object : CompletionHandler<Int, BundleIO> {
                                    override fun completed(result: Int, attachment: BundleIO) {
                                        attachment.saveFilePosition(result)
                                        if (result == savingBundleData.capacity()) {
                                            savingBundleIOs[eventText]?.let {
                                                send(
                                                    EventOuterClass.Event.EventID.SAVING_BUNDLE,
                                                    eventText,
                                                    ByteString.copyFrom(savingBundleData.flip())
                                                ).addListener { future ->
                                                    if (future.isSuccess) {
                                                        savingBundleData.flip()
                                                        it.fileChannel.read(
                                                            savingBundleData,
                                                            it.filePosition.toLong(),
                                                            attachment,
                                                            this
                                                        )
                                                    } else {
                                                        failed(future.cause(), attachment)
                                                    }
                                                }
                                            }
                                        } else {
                                            savingBundleIO.close()
                                            send(
                                                EventOuterClass.Event.EventID.SAVED_BUNDLE,
                                                object {
                                                    val bundleID = eventText
                                                    val bundleVariety = savingBundleIO.bundleVariety
                                                    val bundleName = savingBundleIO.bundleName
                                                    val etc = savingBundleIO.etc

                                                    @JvmField
                                                    val isLastDefault = when (savingBundleIO.bundleVariety) {
                                                        BundleIO.BundleVariety.DEFAULT_NOTE_FILE -> defaultNoteNames.isEmpty()
                                                        BundleIO.BundleVariety.DEFAULT_UI -> defaultUINames.isEmpty()
                                                        else -> false
                                                    }
                                                },
                                                ByteString.copyFrom(savingBundleData.flip())
                                            ).addListener { future ->
                                                if (future.isSuccess) {
                                                    savingBundleIOs.remove(eventText)?.let { savingBundleIO ->
                                                        when (savingBundleIO.bundleVariety) {
                                                            BundleIO.BundleVariety.DEFAULT_NOTE_FILE -> saveDefaultNote("")
                                                            BundleIO.BundleVariety.DEFAULT_UI -> saveDefaultUI(false.toString())
                                                            else -> Unit
                                                        }
                                                    }
                                                } else {
                                                    failed(future.cause(), attachment)
                                                }
                                            }
                                        }
                                    }

                                    override fun failed(exc: Throwable, attachment: BundleIO) {
                                        savingBundleIOs.remove(eventText)?.close()
                                        logFault(exc)
                                    }
                                })
                        }
                    }

                    EventOuterClass.Event.EventID.STOP_SAVING_BUNDLE -> wantEstablished {
                        savingBundleIOs.remove(eventText)?.use {
                            when (it.bundleVariety) {
                                BundleVariety.DEFAULT_NOTE_FILE -> defaultNoteNames.clear()
                                BundleVariety.DEFAULT_UI -> defaultUINames.clear()
                                else -> Unit
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.NEW_UBUNTU -> wantLoggedIn {
                        val avatarID = Utility.getDefaultAvatarID(it)
                        val ubuntuID = Utility.getDefaultAvatarID(eventText)
                        if (avatarID == ubuntuID) {
                            send(EventOuterClass.Event.EventID.WARNING, translateLanguage("ubuntuIsYou"))
                        } else {
                            DB.saveUbuntu(avatarID, ubuntuID).thenAccept { isOK ->
                                if (!isOK) {
                                    send(EventOuterClass.Event.EventID.WARNING, translateLanguage("alreadyUbuntu"))
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.CALL_BUNDLE -> wantEstablished {
                        doCallBundle(
                            Utility.getDefaultAvatarID(eventText), false
                        )
                    }

                    EventOuterClass.Event.EventID.CALL_CONFIGURE -> wantLoggedIn {
                        DB.getConfigure(it).thenAccept { twilightConfigure ->
                            send(
                                EventOuterClass.Event.EventID.CALL_CONFIGURE,
                                object {
                                    val silentSiteCompetence = twilightConfigure[0]
                                    val toNotifyUbuntuCompetence = twilightConfigure[1]
                                    val defaultBundleCompetence = twilightConfigure[2]
                                    val ioCompetence = twilightConfigure[3]
                                    val toNotifySaveBundle = twilightConfigure[4]
                                }
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.WIPE_BUNDLE -> wantLoggedIn {
                        DB.wipeBundle(it, eventText)
                    }

                    EventOuterClass.Event.EventID.WIPE_UBUNTU -> wantLoggedIn {
                        DB.wipeUbuntu(it, eventText)
                    }

                    EventOuterClass.Event.EventID.SET_BUNDLE -> wantLoggedIn {
                        DB.setBundle(
                            jm.readValue(eventText, JSON.QwilightSetBundle::class.java),
                            it
                        )
                    }

                    EventOuterClass.Event.EventID.SET_CONFIGURE -> wantLoggedIn {
                        DB.setConfigure(
                            jm.readValue(eventText, JSON.QwilightSetConfigure::class.java),
                            it
                        )
                    }

                    EventOuterClass.Event.EventID.NEW_SITE -> wantEstablished {
                        putSite(jm.readValue(eventText, JSON.QwilightNewSite::class.java))
                    }

                    EventOuterClass.Event.EventID.NEW_SILENT_SITE -> wantEstablished {
                        putSilentSite(
                            eventText
                        )
                    }

                    EventOuterClass.Event.EventID.COMMENT -> wantLoggedIn {
                        val text = jm.readValue(eventText, JSON.QwilightComment::class.java)
                        val dataID = text.dataID
                        val multiplier = text.multiplier
                        val autoMode = text.autoMode
                        val noteSaltMode = text.noteSaltMode
                        val audioMultiplier = text.audioMultiplier
                        val faintNoteMode = text.faintNoteMode
                        val judgmentMode = text.judgmentMode
                        val hitPointsMode = text.hitPointsMode
                        val noteMobilityMode = text.noteMobilityMode
                        val longNoteMode = text.longNoteMode
                        val inputFavorMode = text.inputFavorMode
                        val noteModifyMode = text.noteModifyMode
                        val lowestJudgmentConditionMode = text.lowestJudgmentConditionMode
                        val stand = text.stand
                        val band = text.band
                        val point = text.point
                        val salt = text.salt
                        val isPaused = text.isPaused
                        val inputFlags = text.inputFlags
                        val commentFileContents = eventData[1]
                        if (longNoteMode != Component.INPUT_LONG_NOTE_MODE && judgmentMode != Component.FAVOR_JUDGMENT_MODE && hitPointsMode != Component.FAVOR_HIT_POINTS_MODE && hitPointsMode != Component.TEST_HIT_POINTS_MODE && noteModifyMode != Component.LONG_NOTE_NOTE_MODIFY_MODE) {
                            val noteFileData = eventData[0].toByteArray()
                            val targetComputing = BaseCompiler.handleCompile(noteFileData, dataID).single()
                            val noteID512 = Utility.getID512(noteFileData)
                            if (!targetComputing.isBanned && !BannedNote.isBanned(targetComputing.noteID)) {
                                logFuture {
                                    Files.write(TwilightComponent.NOTE_ENTRY_PATH.resolve(noteID512), noteFileData)
                                    DB.setNote(
                                        targetComputing.noteID,
                                        Utility.getID128(noteFileData),
                                        Utility.getID256(noteFileData),
                                        targetComputing
                                    )

                                    val lastTitles = DB.getTitleItems(it)
                                    val inputMode = targetComputing.inputMode
                                    val lastAvatarAbility = when (inputMode) {
                                        Component.InputMode.INPUT_MODE_5_1 -> DB.getAvatarAbility5K(avatarID)
                                        Component.InputMode.INPUT_MODE_7_1 -> DB.getAvatarAbility7K(avatarID)
                                        Component.InputMode.INPUT_MODE_9 -> DB.getAvatarAbility9K(avatarID)
                                        else -> 0.0
                                    }
                                    val lastAvatarLevel = DB.getAvatarLevels(it)[0]

                                    val comment = CommentOuterClass.Comment.parseFrom(commentFileContents)
                                    val commentFileData = commentFileContents.toByteArray()
                                    val commentIDNew = Utility.getID512(commentFileData)
                                    val isBand1 = comment.lowestJudgment == 0
                                    val lowestAudioMultiplier = comment.audioMultipliersList.map { it.audioMultiplier }
                                        .plus(audioMultiplier).min()
                                    val commentID = DB.saveComment(
                                        multiplier,
                                        autoMode,
                                        noteSaltMode,
                                        audioMultiplier,
                                        faintNoteMode,
                                        judgmentMode,
                                        hitPointsMode,
                                        noteMobilityMode,
                                        longNoteMode,
                                        inputFavorMode,
                                        noteModifyMode,
                                        lowestJudgmentConditionMode,
                                        stand,
                                        band,
                                        isBand1,
                                        point,
                                        salt,
                                        commentIDNew,
                                        it,
                                        targetComputing.noteID,
                                        isPaused,
                                        inputFlags,
                                        lowestAudioMultiplier,
                                        comment.audioMultipliersList.map { it.audioMultiplier }
                                            .plus(audioMultiplier).max(),
                                    )
                                    if (commentID != null) {
                                        val commentEntryPath = TwilightComponent.COMMENT_ENTRY_PATH.resolve(noteID512)
                                        Files.createDirectories(commentEntryPath)
                                        if (commentID.isNotEmpty()) {
                                            Files.deleteIfExists(commentEntryPath.resolve("$commentID.xz"))
                                        }
                                        commentFileData.inputStream().use {
                                            XZCompressorOutputStream(Files.newOutputStream(commentEntryPath.resolve("$commentIDNew.xz"))).use { os ->
                                                IOUtils.copy(it, os)
                                            }
                                        }
                                    }
                                    DB.saveHandled(
                                        avatarID,
                                        targetComputing.noteID,
                                        isBand1,
                                        point == 1.0,
                                        autoMode,
                                        judgmentMode,
                                        hitPointsMode,
                                        longNoteMode,
                                        inputFavorMode,
                                        noteModifyMode,
                                        lowestAudioMultiplier
                                    )
                                    if (SiteHandler.hasAvatar(this, SiteHandler.commentSiteID)) {
                                        SiteHandler.putSiteYell(
                                            SiteHandler.commentSiteID,
                                            JSON.TwilightCommentSiteYell(
                                                it,
                                                avatarName,
                                                targetComputing.artist,
                                                targetComputing.title,
                                                targetComputing.genre,
                                                targetComputing.levelText,
                                                targetComputing.level,
                                                stand,
                                                hitPointsMode
                                            )
                                        )
                                    }

                                    val titles = DB.getTitleItems(it)
                                    val avatarAbility = when (inputMode) {
                                        Component.InputMode.INPUT_MODE_5_1 -> DB.getAvatarAbility5K(avatarID)
                                        Component.InputMode.INPUT_MODE_7_1 -> DB.getAvatarAbility7K(avatarID)
                                        Component.InputMode.INPUT_MODE_9 -> DB.getAvatarAbility9K(avatarID)
                                        else -> 0.0
                                    }
                                    val avatarLevel = DB.getAvatarLevels(it)[0]
                                    val distanceTitles = titles.filter {
                                        !lastTitles.contains(it)
                                    }.toList()
                                    if (distanceTitles.isNotEmpty()) {
                                        distanceTitles.forEach {
                                            send(
                                                EventOuterClass.Event.EventID.NEW_TITLE,
                                                translateTitleItem(it)
                                            )
                                        }
                                    }

                                    val distanceAvatarAbility = avatarAbility - lastAvatarAbility
                                    if (0 < distanceAvatarAbility) {
                                        send(EventOuterClass.Event.EventID.ABILITY_UP, object {
                                            val inputMode = inputMode
                                            val ability = distanceAvatarAbility
                                        })
                                        if (SiteHandler.hasAvatar(this, SiteHandler.commentSiteID)) {
                                            SiteHandler.putSiteYell(
                                                SiteHandler.commentSiteID,
                                                JSON.TwilightAbilitySiteYell(
                                                    it,
                                                    avatarName,
                                                    inputMode,
                                                    distanceAvatarAbility
                                                )
                                            )
                                        }
                                    }

                                    if (lastAvatarLevel < avatarLevel) {
                                        send(EventOuterClass.Event.EventID.LEVEL_UP, object {
                                            val from = lastAvatarLevel
                                            val to = avatarLevel
                                        })
                                    }
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.VALVE_COMMENT -> wantNotLoggedIn {
                        val text = jm.readValue(eventText, JSON.QwilightValveComment::class.java)
                        val targetComputing =
                            BaseCompiler.handleCompile(eventData[0].toByteArray(), text.dataID).single()
                        if (!targetComputing.isBanned && !BannedNote.isBanned(targetComputing.noteID)) {
                            logFuture {
                                if (SiteHandler.hasAvatar(this, SiteHandler.commentSiteID)) {
                                    SiteHandler.putSiteYell(
                                        SiteHandler.commentSiteID,
                                        JSON.TwilightCommentSiteYell(
                                            avatarID,
                                            avatarName,
                                            targetComputing.artist,
                                            targetComputing.title,
                                            targetComputing.genre,
                                            targetComputing.levelText,
                                            targetComputing.level,
                                            text.stand,
                                            text.hitPointsMode
                                        )
                                    )
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.QUIT_SITE -> wantEstablished {
                        SiteHandler.quitAvatar(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_SITUATION -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetSituation::class.java)
                        this.situationValue = text.situationValue
                        this.situationText = text.situationText
                    }

                    EventOuterClass.Event.EventID.EXILE_AVATAR -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightExileAvatar::class.java)
                        AvatarHandler.getAvatar(text.avatarID)?.let {
                            SiteHandler.exileAvatar(
                                UUID.fromString(
                                    text.siteID
                                ), this, it
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.LEVY_NET -> wantEstablished {
                        SiteHandler.setLevying(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.COMPILED -> wantEstablished {
                        SiteHandler.setCompiled(
                            this,
                            jm.readValue(eventText, JSON.QwilightCompiled::class.java)
                        )
                    }

                    EventOuterClass.Event.EventID.CALL_NET -> wantEstablished {
                        SiteHandler.setNetData(
                            this,
                            msg.qwilightCallNet,
                            if (eventData.isEmpty()) null else CommentOuterClass.Comment.parseFrom(
                                eventData[0]
                            )
                        )
                    }

                    EventOuterClass.Event.EventID.SET_MODE_COMPONENT -> wantEstablished {
                        SiteHandler.setModeComponent(
                            this,
                            jm.readValue(eventText, JSON.QwilightSetModeComponent::class.java)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_FAVOR_NOTE_FILE -> wantEstablished {
                        SiteHandler.setFavorNoteFile(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_FAVOR_MODE_COMPONENT -> wantEstablished {
                        SiteHandler.setFavorModeComponent(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_FAVOR_AUDIO_MULTIPLIER -> wantEstablished {
                        SiteHandler.setFavorAudioMultiplier(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_AVATAR_GROUP -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetAvatarGroup::class.java)
                        SiteHandler.setAvatarGroup(
                            this,
                            UUID.fromString(text.siteID),
                            text
                        )
                    }

                    EventOuterClass.Event.EventID.SET_AUTO_SITE_HAND -> wantEstablished {
                        SiteHandler.setAutoSiteHand(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_VALID_HUNTER_MODE -> wantEstablished {
                        SiteHandler.setValidHunterMode(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_VALID_NET_MODE -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetValidNetMode::class.java)
                        SiteHandler.setValidNetMode(
                            this,
                            UUID.fromString(text.siteID),
                            text
                        )
                    }

                    EventOuterClass.Event.EventID.SET_ALLOWED_POSTABLE_ITEMS -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetAllowedPostableItems::class.java)
                        SiteHandler.setAllowedPostableItems(
                            this,
                            UUID.fromString(text.siteID),
                            text
                        )
                    }

                    EventOuterClass.Event.EventID.SET_POSTABLE_ITEM_BAND -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetPostableItemBand::class.java)
                        SiteHandler.setPostableItemBand(
                            this,
                            UUID.fromString(text.siteID),
                            text
                        )
                    }

                    EventOuterClass.Event.EventID.CALL_NET_SITE_COMMENTS -> wantEstablished {
                        SiteHandler.doCallNetSiteComments(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.SET_NOTE_FILE -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetNoteFile::class.java)
                        SiteHandler.setNoteFileContents(this, UUID.fromString(text.siteID), text)
                    }

                    EventOuterClass.Event.EventID.SET_SITE_HAND -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetSiteHand::class.java)
                        AvatarHandler.getAvatar(text.avatarID)?.let {
                            SiteHandler.setSiteHand(
                                this,
                                UUID.fromString(text.siteID),
                                it
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.SET_SITE_NAME -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightSetSiteName::class.java)
                        setSiteName(text.siteID, text.siteName)
                    }

                    EventOuterClass.Event.EventID.POST_FILE -> wantEstablished {
                        logFuture {
                            val tmpPath = Configure.path.tmpPath.resolve(
                                RandomStringUtils.randomAlphanumeric(8) + "." + eventText
                            )
                            Files.write(Configure.path.wwwPath.resolve(tmpPath), eventData[0].toByteArray())
                            send(
                                EventOuterClass.Event.EventID.POST_FILE, "${Configure.www.remote}/${
                                    tmpPath.toString().replace(" ".toRegex(), "%20")
                                }"
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.WWW_LEVEL -> wantLoggedIn { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightWwwLevel::class.java)
                        DB.getClearedLevelIDs(avatarID).thenAccept {
                            val levelItems = LevelSystem.getSatisfiedLevelItems(text)
                            levelItems.filter { levelItem ->
                                !it.contains(
                                    levelItem.levelID
                                )
                            }
                                .forEach { levelItem ->
                                    val lastTitles = DB.getTitleItems(avatarID)
                                    DB.setWwwLevel(avatarID, levelItem.levelID)
                                    val titles = DB.getTitleItems(avatarID)
                                    val distanceTitles = titles.filter {
                                        !lastTitles.contains(it)
                                    }.toList()
                                    if (distanceTitles.isNotEmpty()) {
                                        distanceTitles.forEach {
                                            send(
                                                EventOuterClass.Event.EventID.NEW_TITLE,
                                                translateTitleItem(it)
                                            )
                                        }
                                    }
                                    send(EventOuterClass.Event.EventID.WWW_LEVEL, levelItem.title)
                                    if (SiteHandler.hasAvatar(this, SiteHandler.commentSiteID)) {
                                        SiteHandler.putSiteYell(
                                            SiteHandler.commentSiteID,
                                            JSON.TwilightLevelSiteYell(avatarID, avatarName, levelItem.title)
                                        )
                                    }
                                }
                        }
                    }

                    EventOuterClass.Event.EventID.CALL_IO -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightCallIO::class.java)
                        val targetAvatarID = Utility.getDefaultAvatarID(text.avatarID)
                        if (Utility.getDefaultAvatarID(avatarID) == targetAvatarID) {
                            send(EventOuterClass.Event.EventID.WARNING, translateLanguage("ioAvatarIsYou"))
                        } else {
                            AvatarHandler.getAvatar(text.avatarID)?.let { ioAvatar ->
                                DB.getIOCompetence(
                                    targetAvatarID
                                ).thenAccept {
                                    if (it == IO_CALLABLE || it == IO_AVATAR && isLoggedIn || it == IO_UBUNTU && DB.isCrossUbuntu(
                                            avatarID,
                                            targetAvatarID
                                        )
                                    ) {
                                        ioAvatar.send(
                                            EventOuterClass.Event.EventID.CALL_IO,
                                            object {
                                                val handlerID = text.handlerID
                                                val avatarID = avatarID
                                                val ioMillis = text.ioMillis
                                            }
                                        )
                                    } else {
                                        send(
                                            EventOuterClass.Event.EventID.WARNING,
                                            translateLanguage("lowerCompetenceAsIO")
                                        )
                                    }
                                }
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.IO_NOT -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightIONot::class.java)
                        AvatarHandler.getAvatar(text.avatarID)?.send(
                            EventOuterClass.Event.EventID.IO_NOT,
                            text.handlerID
                        )
                    }

                    EventOuterClass.Event.EventID.CALL_IO_COMPONENT -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightCallIOComponent::class.java)
                        AvatarHandler.getAvatar(text.avatarID)?.send(
                            EventOuterClass.Event.EventID.CALL_IO_COMPONENT,
                            object {
                                val avatarID = avatarID
                                val avatarName = this@QwilightAvatar.avatarName
                                val noteID = text.noteID
                                val handlerID = text.handlerID

                                @JvmField
                                val isFailMode = text.isFailMode
                                val data = text.data
                                val ioHandlerID = text.ioHandlerID
                                val ioMillis = text.ioMillis
                                val targetIOMillis = text.targetIOMillis
                            }
                        )
                    }

                    EventOuterClass.Event.EventID.COMPILED_IO -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightCompiledIO::class.java)
                        AvatarHandler.getAvatar(text.avatarID)?.send(
                            EventOuterClass.Event.EventID.COMPILED_IO,
                            object {
                                val handlerID = text.handlerID

                                @JvmField
                                val isCompiled = text.isCompiled
                                val avatarID = avatarID
                                val avatarName = this@QwilightAvatar.avatarName
                            }
                        )
                    }

                    EventOuterClass.Event.EventID.LEVY_IO -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightLevyIO::class.java)
                        text.avatarIDs.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.EventID.LEVY_IO,
                                object {
                                    val handlerID = text.handlerID
                                    val levyingWait = text.levyingWait
                                    val lastStand = text.lastStand

                                    @JvmField
                                    val isF = text.isF
                                    val multiplier = text.multiplier
                                    val audioMultiplier = text.audioMultiplier
                                    val ioMillis = text.ioMillis
                                },
                                eventData[0]
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_INPUT -> wantEstablished {
                        val qwilightIOInput = msg.qwilightIOInput
                        qwilightIOInput.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_INPUT
                                    twilightIOInput = EventOuterClass.Event.TwilightIOInput.newBuilder().apply {
                                        handlerID = qwilightIOInput.handlerID
                                        input = qwilightIOInput.input
                                        this.power = qwilightIOInput.power
                                    }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_JUDGE -> wantEstablished {
                        val qwilightIOJudge = msg.qwilightIOJudge
                        qwilightIOJudge.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_JUDGE
                                    twilightIOJudge = EventOuterClass.Event.TwilightIOJudge.newBuilder().apply {
                                        handlerID = qwilightIOJudge.handlerID
                                        noteID = qwilightIOJudge.noteID
                                        this.judged = qwilightIOJudge.judged
                                    }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_NOTE_VISIBILITY -> wantEstablished {
                        val qwilightIONoteVisibility = msg.qwilightIONoteVisibility
                        qwilightIONoteVisibility.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_NOTE_VISIBILITY
                                    twilightIONoteVisibility =
                                        EventOuterClass.Event.TwilightIONoteVisibility.newBuilder().apply {
                                            handlerID = qwilightIONoteVisibility.handlerID
                                            noteID = qwilightIONoteVisibility.noteID
                                            setValidJudgedNotes = qwilightIONoteVisibility.setValidJudgedNotes
                                            setNoteFailed = qwilightIONoteVisibility.setNoteFailed
                                        }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_JUDGMENT_METER -> wantEstablished {
                        val qwilightIOJudgmentMeter = msg.qwilightIOJudgmentMeter
                        qwilightIOJudgmentMeter.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_JUDGMENT_METER
                                    twilightIOJudgmentMeter =
                                        EventOuterClass.Event.TwilightIOJudgmentMeter.newBuilder().apply {
                                            handlerID = qwilightIOJudgmentMeter.handlerID
                                            input = qwilightIOJudgmentMeter.input
                                            judgmentMeter = qwilightIOJudgmentMeter.judgmentMeter
                                            assist = qwilightIOJudgmentMeter.assist
                                        }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_MULTIPLIER -> wantEstablished {
                        val qwilightIOMultiplier = msg.qwilightIOMultiplier
                        qwilightIOMultiplier.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_MULTIPLIER
                                    twilightIOMultiplier =
                                        EventOuterClass.Event.TwilightIOMultiplier.newBuilder().apply {
                                            handlerID = qwilightIOMultiplier.handlerID
                                            multiplier = qwilightIOMultiplier.multiplier
                                        }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_AUDIO_MULTIPLIER -> wantEstablished {
                        val qwilightIOAudioMultiplier = msg.qwilightIOAudioMultiplier
                        qwilightIOAudioMultiplier.avatarIDsList.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.newBuilder().apply {
                                    eventID = EventOuterClass.Event.EventID.IO_AUDIO_MULTIPLIER
                                    twilightIOAudioMultiplier =
                                        EventOuterClass.Event.TwilightIOAudioMultiplier.newBuilder().apply {
                                            handlerID = qwilightIOAudioMultiplier.handlerID
                                            audioMultiplier = qwilightIOAudioMultiplier.audioMultiplier
                                        }.build()
                                }.build()
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_PAUSE -> wantEstablished {
                        val text = jm.readValue(eventText, JSON.QwilightIOPause::class.java)
                        text.avatarIDs.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.EventID.IO_PAUSE,
                                object {
                                    val handlerID = text.handlerID

                                    @JvmField
                                    val isPaused = text.isPaused
                                }
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.IO_QUIT -> wantEstablished { avatarID ->
                        val text = jm.readValue(eventText, JSON.QwilightIOQuit::class.java)
                        text.avatarIDs.forEach {
                            AvatarHandler.getAvatar(it)?.send(
                                EventOuterClass.Event.EventID.IO_QUIT,
                                object {
                                    val avatarID = (if (text.isBanned) "" else avatarID)
                                    val handlerID = text.handlerID
                                }
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.AUDIO_INPUT -> wantEstablished {
                        SiteHandler.audioInput(
                            UUID.fromString(eventText),
                            this,
                            eventData[0]
                        )
                    }

                    EventOuterClass.Event.EventID.QUIT_NET -> wantEstablished {
                        SiteHandler.stopSiteNet(
                            this,
                            UUID.fromString(eventText)
                        )
                    }

                    EventOuterClass.Event.EventID.COMMENTARY -> wantLoggedIn {
                        val text = jm.readValue(eventText, JSON.QwilightCommentary::class.java)
                        DB.setCommentary(text.noteID, it, text.commentary).thenAccept { isOK ->
                            if (!isOK) {
                                send(EventOuterClass.Event.EventID.WARNING, translateLanguage("hasNotComment"))
                            }
                        }
                    }

                    EventOuterClass.Event.EventID.AVATAR_TITLE -> wantLoggedIn {
                        DB.setAvatarTitle(it, eventText).thenRun {
                            AvatarHandler.sendInvalidateAvatarTitle(
                                it
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.AVATAR_EDGE -> wantLoggedIn {
                        DB.setAvatarEdge(it, eventText).thenRun {
                            AvatarHandler.sendInvalidateAvatarEdge(
                                it
                            )
                        }
                    }

                    EventOuterClass.Event.EventID.SET_LANGUAGE -> wantEstablished {
                        language = eventText
                    }

                    EventOuterClass.Event.EventID.SET_FAVOR -> wantLoggedIn {
                        DB.setFavor(jm.readValue(eventText, JSON.QwilightSetFavor::class.java), avatarID)
                    }

                    EventOuterClass.Event.EventID.POST_ITEM -> wantEstablished {
                        val qwilightPostItem = msg.qwilightPostItem
                        SiteHandler.postItem(
                            UUID.fromString(qwilightPostItem.siteID),
                            this,
                            qwilightPostItem
                        )
                    }

                    else -> Unit
                }
            }
        }
    }

    override val loggerID: String
        get() = if (isEstablished) {
            if (isLoggedIn) {
                "{ID: $avatarID, Name: $avatarName, IP: $remote, Date: v$qwilightDate (${
                    qwilightHash.substring(
                        0,
                        8
                    )
                })}"
            } else {
                if (isAwilight) {
                    "{ID: $avatarID, Name: $avatarName}"
                } else if (avatarEstimatedID.isNotEmpty()) {
                    "{ID: $avatarID ($avatarEstimatedID?), Name: $avatarName, IP: $remote, Date: v$qwilightDate (${
                        qwilightHash.substring(
                            0,
                            8
                        )
                    })}"
                } else {
                    "{ID: $avatarID, Name: $avatarName, IP: $remote, Date: v$qwilightDate (${
                        qwilightHash.substring(
                            0,
                            8
                        )
                    })}"
                }
            }
        } else {
            if (qwilightDate.isEmpty() || qwilightHash.isEmpty()) {
                "{IP: $remote}}"
            } else {
                "{IP: $remote}, Date: v$qwilightDate (${qwilightHash.substring(0, 8)})}"
            }
        }

    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("[{}] {}", loggerID, Utility.getFaultText(e))
        }
    }

    companion object {
        const val NOT_UBUNTU = 0
        const val NOT_SIGNED_IN = 1
        const val NOTIFY_UBUNTU = 0
        const val NOTIFY_SAVE_BUNDLE_CALLABLE = 0
        const val NOTIFY_SAVE_BUNDLE_AVATAR = 1
        const val NOTIFY_SAVE_BUNDLE_UBUNTU = 2
        const val IO_CALLABLE = 0
        const val IO_AVATAR = 3
        const val IO_UBUNTU = 1
        const val BUNDLE_CALLABLE = 0
        const val BUNDLE_AVATAR = 3
        const val BUNDLE_UBUNTU = 1
        const val BUNDLE_VOID = 2
        const val SILENT_SITE_CALLABLE = 0
        const val SILENT_SITE_AVATAR = 3
        const val SILENT_SITE_UBUNTU = 1
        const val NOTE_FILE_MODE = 2
        const val DEFAULT_COMPUTING = 3
        const val COMMENT_COMPUTING = 4
        const val AUTO_COMPUTING = 5
        const val QUIT_MODE = 6
        const val NET_COMPUTING = 7
        const val IO_COMPUTING = 8
        const val BUNDLE_AVAILABLE_COUNT = 1
    }
}