Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/entity/player/dialogue/dialogue.ifaces.toml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ id = 5
[.close_quick_chat]
id = 4

[.chat_line0]
id = "180-279"

[dialogue_select2_models]
id = 140
type = "dialogue_box"
Expand Down
3 changes: 3 additions & 0 deletions data/entity/player/modal/chat_box/chat_box.ifaces.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ type = "chat_box"
id = 754
type = "private_chat"

[.line1]
id = "1-5"

9 changes: 9 additions & 0 deletions data/social/report/report.ifaces.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ id = 110
[report_abuse]
id = 594

[.mute_confirm]
id = 8

[.mute_entry]
id = 52

[.mute_select]
id = 66

10 changes: 10 additions & 0 deletions data/social/report/report.vars.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[muted_until]
format = "int"
persist = true

[banned_until]
format = "int"
persist = true

[black_marks]
persist = true
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.transactions.transaction
import world.gregs.voidps.engine.data.AbuseReport
import world.gregs.voidps.engine.data.PlayerSave
import world.gregs.voidps.engine.data.Storage
import world.gregs.voidps.engine.data.config.AccountDefinition
Expand Down Expand Up @@ -200,6 +201,19 @@ class DatabaseStorage : Storage {
saveHistories(accounts, playerIds)
}

override fun saveReport(report: AbuseReport): Unit = transaction {
ReportsTable.insert {
it[reporter] = report.reporter
it[reported] = report.reported
it[rule] = report.rule
it[ruleName] = report.ruleName
it[mute] = report.mute
it[suggestion] = report.suggestion
it[time] = report.time
it[evidence] = report.evidence
}
}

override fun exists(accountName: String): Boolean = transaction {
val lower = accountName.lowercase()
AccountsTable
Expand Down Expand Up @@ -565,7 +579,7 @@ class DatabaseStorage : Storage {
}
}

internal val tables = arrayOf(AccountsTable, ExperienceTable, LevelsTable, VariablesTable, InventoriesTable, OffersTable, ActiveOffersTable, PlayerHistoryTable, ClaimsTable, ItemHistoryTable)
internal val tables = arrayOf(AccountsTable, ExperienceTable, LevelsTable, VariablesTable, InventoriesTable, OffersTable, ActiveOffersTable, PlayerHistoryTable, ClaimsTable, ItemHistoryTable, ReportsTable)

private const val TYPE_STRING = 0.toByte()
private const val TYPE_INT = 1.toByte()
Expand Down
14 changes: 14 additions & 0 deletions database/src/main/kotlin/world/gregs/voidps/storage/Tables.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ internal object ClaimsTable : Table("grand_exchange_claims") {
val coins = integer("coins")
}

internal object ReportsTable : Table("abuse_reports") {
val id = integer("id").autoIncrement().uniqueIndex()
val reporter = varchar("reporter", 12)
val reported = text("reported")
val rule = integer("rule")
val ruleName = text("rule_name")
val mute = bool("mute")
val suggestion = text("suggestion")
val time = integer("time")
val evidence = array<String>("evidence")

override val primaryKey = PrimaryKey(id, name = "pk_report_id")
}

internal object ItemHistoryTable : Table("grand_exchange_item_history") {
val item = text("item")
val timestamp = long("timestamp")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,41 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import world.gregs.voidps.engine.data.AbuseReport
import kotlin.test.assertEquals

class DatabaseStorageTest : StorageTest(), DatabaseTest {

override val storage = DatabaseStorage()

@Test
fun `Store an abuse report`() {
val report = AbuseReport(
reporter = "mod_steve",
reported = "Durial321",
rule = 6,
ruleName = "Macroing",
mute = true,
suggestion = "extra info",
time = 1234567890,
evidence = listOf("[00:00:01] public: free armour trimming", "[00:00:02] public: selling gf"),
)

storage.saveReport(report)

transaction {
val row = ReportsTable.selectAll().single()
assertEquals(report.reporter, row[ReportsTable.reporter])
assertEquals(report.reported, row[ReportsTable.reported])
assertEquals(report.rule, row[ReportsTable.rule])
assertEquals(report.ruleName, row[ReportsTable.ruleName])
assertEquals(report.mute, row[ReportsTable.mute])
assertEquals(report.suggestion, row[ReportsTable.suggestion])
assertEquals(report.time, row[ReportsTable.time])
assertEquals(report.evidence, row[ReportsTable.evidence])
}
}

@Test
fun `Saving variable with invalid format throws exception`() {
assertThrows<IllegalArgumentException> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ class PlayerAccountLoader(
client.disconnect(Response.GAME_UPDATE)
return null
}
var player = storage.load(username)?.toPlayer()
val save = storage.load(username)
if (save != null && banned(save.variables)) {
client.disconnect(Response.ACCOUNT_DISABLED)
return null
}
var player = save?.toPlayer()
if (player == null) {
if (!Settings["development.accountCreation", false]) {
client.disconnect(Response.INVALID_CREDENTIALS)
Expand All @@ -73,6 +78,11 @@ class PlayerAccountLoader(
}
}

private fun banned(variables: Map<String, Any>): Boolean {
val until = (variables["banned_until"] as? Number)?.toInt() ?: return false
return until > (System.currentTimeMillis() / 1000).toInt()
}

suspend fun connect(player: Player, client: Client, displayMode: Int = 0, viewport: Boolean = true) {
if (!accounts.setup(player, client, displayMode, viewport)) {
logger.warn { "Error setting up account" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class InstructionHandlers(
var chatTypeChangeHandler: ChatTypeChange.(Player) -> Unit = empty()
var clanChatKickHandler: ClanChatKick.(Player) -> Unit = empty()
var clanChatRankHandler: ClanChatRank.(Player) -> Unit = empty()
var reportAbuseHandler: ReportAbuse.(Player) -> Unit = empty()

private fun <I : Instruction> empty(): I.(Player) -> Unit {
val logger = InlineLogger("InstructionHandler")
Expand Down Expand Up @@ -98,6 +99,7 @@ class InstructionHandlers(
is ChatTypeChange -> chatTypeChangeHandler.invoke(instruction, player)
is ClanChatKick -> clanChatKickHandler.invoke(instruction, player)
is ClanChatRank -> clanChatRankHandler.invoke(instruction, player)
is ReportAbuse -> reportAbuseHandler.invoke(instruction, player)
is SongEnd -> songEndHandler.invoke(instruction, player)
else -> return false
}
Expand Down Expand Up @@ -132,6 +134,7 @@ inline fun <reified I : Instruction> instruction(noinline handler: I.(Player) ->
ChatTypeChange::class -> get<InstructionHandlers>().chatTypeChangeHandler = handler as ChatTypeChange.(Player) -> Unit
ClanChatKick::class -> get<InstructionHandlers>().clanChatKickHandler = handler as ClanChatKick.(Player) -> Unit
ClanChatRank::class -> get<InstructionHandlers>().clanChatRankHandler = handler as ClanChatRank.(Player) -> Unit
ReportAbuse::class -> get<InstructionHandlers>().reportAbuseHandler = handler as ReportAbuse.(Player) -> Unit
else -> throw UnsupportedOperationException("Unknown Instruction type: ${I::class}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package world.gregs.voidps.engine.data

/**
* A player submitted report about another player breaking a rule
* @param reporter Account name of the player filing the report
* @param reported Display name of the accused player as submitted
* @param rule Identifier of the rule broken
* @param ruleName Readable name of the rule broken
* @param mute Whether a moderator requested the accused be muted
* @param suggestion Additional text submitted with the report
* @param time Epoch second timestamp the report was received
* @param evidence Recent chat messages sent by the accused player
*/
data class AbuseReport(
val reporter: String,
val reported: String,
val rule: Int,
val ruleName: String,
val mute: Boolean,
val suggestion: String,
val time: Int,
val evidence: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ class AccountManager(
return
}
player["logged_out"] = true
player.client?.logout()
if (safely) {
player.client?.logout()
player.strongQueue("logout") {
// Make sure nothing else starts
}
Expand Down
43 changes: 43 additions & 0 deletions engine/src/main/kotlin/world/gregs/voidps/engine/data/Reports.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package world.gregs.voidps.engine.data

import com.github.michaelbull.logging.InlineLogger
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* Saves abuse reports off the game thread, falling back to [fallback] storage on failure
*/
class Reports(
private val storage: Storage,
private val fallback: Storage = storage,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) {
private val logger = InlineLogger()

private val fallbackHandler = CoroutineExceptionHandler { _, exception ->
logger.error(exception) { "Fallback report save failed!" }
}

fun queue(report: AbuseReport) {
if (Settings["storage.disabled", false]) {
return
}
val handler = CoroutineExceptionHandler { _, exception ->
logger.error(exception) { "Error saving abuse report!" }
scope.launch(fallbackHandler) {
withContext(NonCancellable) {
fallback.saveReport(report)
}
}
}
scope.launch(handler) {
withContext(NonCancellable) {
storage.saveReport(report)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ class SafeStorage(
}
}

override fun saveReport(report: AbuseReport) {
val parent = directory.resolve("reports/")
parent.mkdirs()
val file = parent.resolve("${report.time}-${report.reporter}.toml")
file.writeText(
buildString {
appendLine("reporter = \"${report.reporter}\"")
appendLine("reported = \"${report.reported}\"")
appendLine("rule = ${report.rule}")
appendLine("rule_name = \"${report.ruleName}\"")
appendLine("mute = ${report.mute}")
appendLine("suggestion = \"${report.suggestion}\"")
appendLine("time = ${report.time}")
appendLine("evidence = [${report.evidence.joinToString(", ") { "\"${it}\"" }}]")
},
)
}

override fun saveOffers(offers: OpenOffers) {
val buy = directory.resolve(Settings["storage.grand.exchange.offers.buy.path"])
buy.mkdirs()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ interface Storage {
*/
fun save(accounts: List<PlayerSave>)

/**
* Saves an abuse report
*/
fun saveReport(report: AbuseReport)

/**
* Checks if an account exists
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package world.gregs.voidps.engine.data.file
import com.github.michaelbull.logging.InlineLogger
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import world.gregs.config.*
import world.gregs.voidps.engine.data.AbuseReport
import world.gregs.voidps.engine.data.PlayerSave
import world.gregs.voidps.engine.data.Settings
import world.gregs.voidps.engine.data.Storage
Expand Down Expand Up @@ -91,6 +92,22 @@ class FileStorage(
return offers
}

override fun saveReport(report: AbuseReport) {
val parent = directory.resolve(Settings["storage.reports.path", "reports/"])
parent.mkdirs()
val file = parent.resolve("${report.time}-${report.reporter}.toml")
Config.fileWriter(file) {
writePair("reporter", report.reporter)
writePair("reported", report.reported)
writePair("rule", report.rule)
writePair("rule_name", report.ruleName)
writePair("mute", report.mute)
writePair("suggestion", report.suggestion)
writePair("time", report.time)
writePair("evidence", report.evidence)
}
}

override fun saveOffers(offers: OpenOffers) {
val buy = directory.resolve(Settings["storage.grand.exchange.offers.buy.path"])
if (buy.deleteRecursively()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ internal class PlayerAccountLoaderTest : KoinMock() {
override fun savePriceHistory(history: Map<String, PriceHistory>) {
}

override fun saveReport(report: AbuseReport) {
}

override fun exists(accountName: String): Boolean = false

override fun load(accountName: String): PlayerSave? = playerSave
Expand All @@ -89,6 +92,26 @@ internal class PlayerAccountLoaderTest : KoinMock() {
assertNotNull(instructions)
}

@Test
fun `Can't login if banned`() = runTest {
val client: Client = mockk(relaxed = true)
playerSave = PlayerSave("name", "hash", Tile.EMPTY, intArrayOf(), emptyList(), intArrayOf(), true, intArrayOf(), intArrayOf(), mapOf("banned_until" to Int.MAX_VALUE), emptyMap(), emptyMap(), emptyList(), arrayOf(), emptyList())

val instructions = loader.load(client, "name", "pass", 2)
assertNull(instructions)
coVerify { client.disconnect(Response.ACCOUNT_DISABLED) }
}

@Test
fun `Can login once ban expires`() = runTest {
val client: Client = mockk(relaxed = true)
playerSave = PlayerSave("name", "hash", Tile.EMPTY, intArrayOf(), emptyList(), intArrayOf(), true, intArrayOf(), intArrayOf(), mapOf("banned_until" to 1), emptyMap(), emptyMap(), emptyList(), arrayOf(), emptyList())
coEvery { queue.await() } just Runs

val instructions = loader.load(client, "name", "pass", 2)
assertNotNull(instructions)
}

@Test
fun `Can't login if account is being saved`() = runTest {
saveQueue.save(Player(accountName = "name"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ class AccountManagerTest : KoinMock() {
override fun save(accounts: List<PlayerSave>) {
}

override fun saveReport(report: AbuseReport) {
}

override fun exists(accountName: String): Boolean = false

override fun load(accountName: String): PlayerSave? = null
Expand Down
Loading
Loading