Added basic follows

This commit is contained in:
nise.moe 2024-03-08 08:18:44 +01:00
parent e14367edaf
commit 0ca65307b5
28 changed files with 801 additions and 43 deletions

1
mari/.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
patreon: nise_moe

View File

@ -1,7 +1,32 @@
package org.nisemoe.mari.judgements package org.nisemoe.mari.judgements
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.math.roundToInt
/**
* Backwards compatibility with time values that were persisted as doubles.
*/
object TimeSerializer : KSerializer<Int> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Time", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Int) {
encoder.encodeInt(value)
}
override fun deserialize(decoder: Decoder): Int {
val doubleValue = decoder.decodeDouble()
return doubleValue.roundToInt()
}
}
/** /**
* Represents a judgement on a hit object. * Represents a judgement on a hit object.
@ -9,7 +34,9 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
data class Judgement( data class Judgement(
@Serializable(with = TimeSerializer::class)
val time: Int, val time: Int,
val x: Double, val x: Double,
val y: Double, val y: Double,
val type: Type, val type: Type,

View File

@ -22,6 +22,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserFollows
import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
@ -87,6 +88,11 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
*/ */
val UPDATE_USER_QUEUE: UpdateUserQueue get() = UpdateUserQueue.UPDATE_USER_QUEUE val UPDATE_USER_QUEUE: UpdateUserQueue get() = UpdateUserQueue.UPDATE_USER_QUEUE
/**
* The table <code>public.user_follows</code>.
*/
val USER_FOLLOWS: UserFollows get() = UserFollows.USER_FOLLOWS
/** /**
* The table <code>public.user_scores</code>. * The table <code>public.user_scores</code>.
*/ */
@ -126,6 +132,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
ScoresJudgements.SCORES_JUDGEMENTS, ScoresJudgements.SCORES_JUDGEMENTS,
ScoresSimilarity.SCORES_SIMILARITY, ScoresSimilarity.SCORES_SIMILARITY,
UpdateUserQueue.UPDATE_USER_QUEUE, UpdateUserQueue.UPDATE_USER_QUEUE,
UserFollows.USER_FOLLOWS,
UserScores.USER_SCORES, UserScores.USER_SCORES,
UserScoresSimilarity.USER_SCORES_SIMILARITY, UserScoresSimilarity.USER_SCORES_SIMILARITY,
Users.USERS Users.USERS

View File

@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserFollows
import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
import com.nisemoe.generated.tables.records.BeatmapsRecord import com.nisemoe.generated.tables.records.BeatmapsRecord
@ -22,6 +23,7 @@ import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
import com.nisemoe.generated.tables.records.ScoresRecord import com.nisemoe.generated.tables.records.ScoresRecord
import com.nisemoe.generated.tables.records.ScoresSimilarityRecord import com.nisemoe.generated.tables.records.ScoresSimilarityRecord
import com.nisemoe.generated.tables.records.UpdateUserQueueRecord import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
import com.nisemoe.generated.tables.records.UserFollowsRecord
import com.nisemoe.generated.tables.records.UserScoresSimilarityRecord import com.nisemoe.generated.tables.records.UserScoresSimilarityRecord
import com.nisemoe.generated.tables.records.UsersRecord import com.nisemoe.generated.tables.records.UsersRecord
@ -46,6 +48,7 @@ val SCORES_JUDGEMENTS_PKEY: UniqueKey<ScoresJudgementsRecord> = Internal.createU
val SCORES_SIMILARITY_PKEY: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true) val SCORES_SIMILARITY_PKEY: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true)
val UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("unique_beatmap_replay_ids"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.BEATMAP_ID, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_1, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_2), true) val UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("unique_beatmap_replay_ids"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.BEATMAP_ID, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_1, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_2), true)
val UPDATE_USER_QUEUE_PKEY: UniqueKey<UpdateUserQueueRecord> = Internal.createUniqueKey(UpdateUserQueue.UPDATE_USER_QUEUE, DSL.name("update_user_queue_pkey"), arrayOf(UpdateUserQueue.UPDATE_USER_QUEUE.ID), true) val UPDATE_USER_QUEUE_PKEY: UniqueKey<UpdateUserQueueRecord> = Internal.createUniqueKey(UpdateUserQueue.UPDATE_USER_QUEUE, DSL.name("update_user_queue_pkey"), arrayOf(UpdateUserQueue.UPDATE_USER_QUEUE.ID), true)
val USER_FOLLOWS_PKEY: UniqueKey<UserFollowsRecord> = Internal.createUniqueKey(UserFollows.USER_FOLLOWS, DSL.name("user_follows_pkey"), arrayOf(UserFollows.USER_FOLLOWS.USER_ID, UserFollows.USER_FOLLOWS.FOLLOWS_USER_ID), true)
val USER_SCORES_SIMILARITY_PKEY: UniqueKey<UserScoresSimilarityRecord> = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_similarity_pkey"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.ID), true) val USER_SCORES_SIMILARITY_PKEY: UniqueKey<UserScoresSimilarityRecord> = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_similarity_pkey"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.ID), true)
val USER_SCORES_UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<UserScoresSimilarityRecord> = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_unique_beatmap_replay_ids"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.BEATMAP_ID, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_USER, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_OSU), true) val USER_SCORES_UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<UserScoresSimilarityRecord> = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_unique_beatmap_replay_ids"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.BEATMAP_ID, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_USER, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_OSU), true)
val USERS_PKEY: UniqueKey<UsersRecord> = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true) val USERS_PKEY: UniqueKey<UsersRecord> = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true)
@ -55,3 +58,5 @@ val USERS_PKEY: UniqueKey<UsersRecord> = Internal.createUniqueKey(Users.USERS, D
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
val SCORES_JUDGEMENTS__SCORES_JUDGEMENTS_SCORE_ID_FKEY: ForeignKey<ScoresJudgementsRecord, ScoresRecord> = Internal.createForeignKey(ScoresJudgements.SCORES_JUDGEMENTS, DSL.name("scores_judgements_score_id_fkey"), arrayOf(ScoresJudgements.SCORES_JUDGEMENTS.SCORE_ID), com.nisemoe.generated.keys.SCORES_PKEY, arrayOf(Scores.SCORES.ID), true) val SCORES_JUDGEMENTS__SCORES_JUDGEMENTS_SCORE_ID_FKEY: ForeignKey<ScoresJudgementsRecord, ScoresRecord> = Internal.createForeignKey(ScoresJudgements.SCORES_JUDGEMENTS, DSL.name("scores_judgements_score_id_fkey"), arrayOf(ScoresJudgements.SCORES_JUDGEMENTS.SCORE_ID), com.nisemoe.generated.keys.SCORES_PKEY, arrayOf(Scores.SCORES.ID), true)
val USER_FOLLOWS__USER_FOLLOWS_FOLLOWS_USER_ID_FKEY: ForeignKey<UserFollowsRecord, UsersRecord> = Internal.createForeignKey(UserFollows.USER_FOLLOWS, DSL.name("user_follows_follows_user_id_fkey"), arrayOf(UserFollows.USER_FOLLOWS.FOLLOWS_USER_ID), com.nisemoe.generated.keys.USERS_PKEY, arrayOf(Users.USERS.USER_ID), true)
val USER_FOLLOWS__USER_FOLLOWS_USER_ID_FKEY: ForeignKey<UserFollowsRecord, UsersRecord> = Internal.createForeignKey(UserFollows.USER_FOLLOWS, DSL.name("user_follows_user_id_fkey"), arrayOf(UserFollows.USER_FOLLOWS.USER_ID), com.nisemoe.generated.keys.USERS_PKEY, arrayOf(Users.USERS.USER_ID), true)

View File

@ -0,0 +1,167 @@
/*
* This file is generated by jOOQ.
*/
package com.nisemoe.generated.tables
import com.nisemoe.generated.Public
import com.nisemoe.generated.keys.USER_FOLLOWS_PKEY
import com.nisemoe.generated.keys.USER_FOLLOWS__USER_FOLLOWS_FOLLOWS_USER_ID_FKEY
import com.nisemoe.generated.keys.USER_FOLLOWS__USER_FOLLOWS_USER_ID_FKEY
import com.nisemoe.generated.tables.records.UserFollowsRecord
import java.util.function.Function
import kotlin.collections.List
import org.jooq.Field
import org.jooq.ForeignKey
import org.jooq.Identity
import org.jooq.Name
import org.jooq.Record
import org.jooq.Records
import org.jooq.Row2
import org.jooq.Schema
import org.jooq.SelectField
import org.jooq.Table
import org.jooq.TableField
import org.jooq.TableOptions
import org.jooq.UniqueKey
import org.jooq.impl.DSL
import org.jooq.impl.Internal
import org.jooq.impl.SQLDataType
import org.jooq.impl.TableImpl
/**
* This class is generated by jOOQ.
*/
@Suppress("UNCHECKED_CAST")
open class UserFollows(
alias: Name,
child: Table<out Record>?,
path: ForeignKey<out Record, UserFollowsRecord>?,
aliased: Table<UserFollowsRecord>?,
parameters: Array<Field<*>?>?
): TableImpl<UserFollowsRecord>(
alias,
Public.PUBLIC,
child,
path,
aliased,
parameters,
DSL.comment(""),
TableOptions.table()
) {
companion object {
/**
* The reference instance of <code>public.user_follows</code>
*/
val USER_FOLLOWS: UserFollows = UserFollows()
}
/**
* The class holding records for this type
*/
override fun getRecordType(): Class<UserFollowsRecord> = UserFollowsRecord::class.java
/**
* The column <code>public.user_follows.user_id</code>.
*/
val USER_ID: TableField<UserFollowsRecord, Long?> = createField(DSL.name("user_id"), SQLDataType.BIGINT.nullable(false).identity(true), this, "")
/**
* The column <code>public.user_follows.follows_user_id</code>.
*/
val FOLLOWS_USER_ID: TableField<UserFollowsRecord, Long?> = createField(DSL.name("follows_user_id"), SQLDataType.BIGINT.nullable(false).identity(true), this, "")
private constructor(alias: Name, aliased: Table<UserFollowsRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<UserFollowsRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
/**
* Create an aliased <code>public.user_follows</code> table reference
*/
constructor(alias: String): this(DSL.name(alias))
/**
* Create an aliased <code>public.user_follows</code> table reference
*/
constructor(alias: Name): this(alias, null)
/**
* Create a <code>public.user_follows</code> table reference
*/
constructor(): this(DSL.name("user_follows"), null)
constructor(child: Table<out Record>, key: ForeignKey<out Record, UserFollowsRecord>): this(Internal.createPathAlias(child, key), child, key, USER_FOLLOWS, null)
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
override fun getIdentity(): Identity<UserFollowsRecord, Long?> = super.getIdentity() as Identity<UserFollowsRecord, Long?>
override fun getPrimaryKey(): UniqueKey<UserFollowsRecord> = USER_FOLLOWS_PKEY
override fun getReferences(): List<ForeignKey<UserFollowsRecord, *>> = listOf(USER_FOLLOWS__USER_FOLLOWS_USER_ID_FKEY, USER_FOLLOWS__USER_FOLLOWS_FOLLOWS_USER_ID_FKEY)
private lateinit var _userFollowsUserIdFkey: Users
private lateinit var _userFollowsFollowsUserIdFkey: Users
/**
* Get the implicit join path to the <code>public.users</code> table, via
* the <code>user_follows_user_id_fkey</code> key.
*/
fun userFollowsUserIdFkey(): Users {
if (!this::_userFollowsUserIdFkey.isInitialized)
_userFollowsUserIdFkey = Users(this, USER_FOLLOWS__USER_FOLLOWS_USER_ID_FKEY)
return _userFollowsUserIdFkey;
}
val userFollowsUserIdFkey: Users
get(): Users = userFollowsUserIdFkey()
/**
* Get the implicit join path to the <code>public.users</code> table, via
* the <code>user_follows_follows_user_id_fkey</code> key.
*/
fun userFollowsFollowsUserIdFkey(): Users {
if (!this::_userFollowsFollowsUserIdFkey.isInitialized)
_userFollowsFollowsUserIdFkey = Users(this, USER_FOLLOWS__USER_FOLLOWS_FOLLOWS_USER_ID_FKEY)
return _userFollowsFollowsUserIdFkey;
}
val userFollowsFollowsUserIdFkey: Users
get(): Users = userFollowsFollowsUserIdFkey()
override fun `as`(alias: String): UserFollows = UserFollows(DSL.name(alias), this)
override fun `as`(alias: Name): UserFollows = UserFollows(alias, this)
override fun `as`(alias: Table<*>): UserFollows = UserFollows(alias.getQualifiedName(), this)
/**
* Rename this table
*/
override fun rename(name: String): UserFollows = UserFollows(DSL.name(name), null)
/**
* Rename this table
*/
override fun rename(name: Name): UserFollows = UserFollows(name, null)
/**
* Rename this table
*/
override fun rename(name: Table<*>): UserFollows = UserFollows(name.getQualifiedName(), null)
// -------------------------------------------------------------------------
// Row2 type methods
// -------------------------------------------------------------------------
override fun fieldsRow(): Row2<Long?, Long?> = super.fieldsRow() as Row2<Long?, Long?>
/**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/
fun <U> mapping(from: (Long?, Long?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
/**
* Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}.
*/
fun <U> mapping(toType: Class<U>, from: (Long?, Long?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
}

View File

@ -17,7 +17,7 @@ import org.jooq.ForeignKey
import org.jooq.Name import org.jooq.Name
import org.jooq.Record import org.jooq.Record
import org.jooq.Records import org.jooq.Records
import org.jooq.Row18 import org.jooq.Row20
import org.jooq.Schema import org.jooq.Schema
import org.jooq.SelectField import org.jooq.SelectField
import org.jooq.Table import org.jooq.Table
@ -153,6 +153,16 @@ open class Users(
*/ */
val COUNT_MISS: TableField<UsersRecord, Long?> = createField(DSL.name("count_miss"), SQLDataType.BIGINT, this, "") val COUNT_MISS: TableField<UsersRecord, Long?> = createField(DSL.name("count_miss"), SQLDataType.BIGINT, this, "")
/**
* The column <code>public.users.is_banned</code>.
*/
val IS_BANNED: TableField<UsersRecord, Boolean?> = createField(DSL.name("is_banned"), SQLDataType.BOOLEAN.defaultValue(DSL.field(DSL.raw("false"), SQLDataType.BOOLEAN)), this, "")
/**
* The column <code>public.users.approx_ban_date</code>.
*/
val APPROX_BAN_DATE: TableField<UsersRecord, OffsetDateTime?> = createField(DSL.name("approx_ban_date"), SQLDataType.TIMESTAMPWITHTIMEZONE(6), this, "")
private constructor(alias: Name, aliased: Table<UsersRecord>?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table<UsersRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<UsersRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters) private constructor(alias: Name, aliased: Table<UsersRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
@ -194,18 +204,18 @@ open class Users(
override fun rename(name: Table<*>): Users = Users(name.getQualifiedName(), null) override fun rename(name: Table<*>): Users = Users(name.getQualifiedName(), null)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Row18 type methods // Row20 type methods
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> = super.fieldsRow() as Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> override fun fieldsRow(): Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?> = super.fieldsRow() as Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?>
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}. * Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/ */
fun <U> mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?) -> U): SelectField<U> = convertFrom(Records.mapping(from)) fun <U> mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Class, * Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}. * Function)}.
*/ */
fun <U> mapping(toType: Class<U>, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from)) fun <U> mapping(toType: Class<U>, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
} }

View File

@ -0,0 +1,72 @@
/*
* This file is generated by jOOQ.
*/
package com.nisemoe.generated.tables.records
import com.nisemoe.generated.tables.UserFollows
import org.jooq.Field
import org.jooq.Record2
import org.jooq.Row2
import org.jooq.impl.UpdatableRecordImpl
/**
* This class is generated by jOOQ.
*/
@Suppress("UNCHECKED_CAST")
open class UserFollowsRecord private constructor() : UpdatableRecordImpl<UserFollowsRecord>(UserFollows.USER_FOLLOWS), Record2<Long?, Long?> {
open var userId: Long?
set(value): Unit = set(0, value)
get(): Long? = get(0) as Long?
open var followsUserId: Long?
set(value): Unit = set(1, value)
get(): Long? = get(1) as Long?
// -------------------------------------------------------------------------
// Primary key information
// -------------------------------------------------------------------------
override fun key(): Record2<Long?, Long?> = super.key() as Record2<Long?, Long?>
// -------------------------------------------------------------------------
// Record2 type implementation
// -------------------------------------------------------------------------
override fun fieldsRow(): Row2<Long?, Long?> = super.fieldsRow() as Row2<Long?, Long?>
override fun valuesRow(): Row2<Long?, Long?> = super.valuesRow() as Row2<Long?, Long?>
override fun field1(): Field<Long?> = UserFollows.USER_FOLLOWS.USER_ID
override fun field2(): Field<Long?> = UserFollows.USER_FOLLOWS.FOLLOWS_USER_ID
override fun component1(): Long? = userId
override fun component2(): Long? = followsUserId
override fun value1(): Long? = userId
override fun value2(): Long? = followsUserId
override fun value1(value: Long?): UserFollowsRecord {
set(0, value)
return this
}
override fun value2(value: Long?): UserFollowsRecord {
set(1, value)
return this
}
override fun values(value1: Long?, value2: Long?): UserFollowsRecord {
this.value1(value1)
this.value2(value2)
return this
}
/**
* Create a detached, initialised UserFollowsRecord
*/
constructor(userId: Long? = null, followsUserId: Long? = null): this() {
this.userId = userId
this.followsUserId = followsUserId
resetChangedOnNotNull()
}
}

View File

@ -11,8 +11,8 @@ import java.time.OffsetDateTime
import org.jooq.Field import org.jooq.Field
import org.jooq.Record1 import org.jooq.Record1
import org.jooq.Record18 import org.jooq.Record20
import org.jooq.Row18 import org.jooq.Row20
import org.jooq.impl.UpdatableRecordImpl import org.jooq.impl.UpdatableRecordImpl
@ -20,7 +20,7 @@ import org.jooq.impl.UpdatableRecordImpl
* This class is generated by jOOQ. * This class is generated by jOOQ.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(Users.USERS), Record18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> { open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(Users.USERS), Record20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?> {
open var userId: Long? open var userId: Long?
set(value): Unit = set(0, value) set(value): Unit = set(0, value)
@ -96,6 +96,16 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
set(value): Unit = set(17, value) set(value): Unit = set(17, value)
get(): Long? = get(17) as Long? get(): Long? = get(17) as Long?
@Suppress("INAPPLICABLE_JVM_NAME")
@set:JvmName("setIsBanned")
open var isBanned: Boolean?
set(value): Unit = set(18, value)
get(): Boolean? = get(18) as Boolean?
open var approxBanDate: OffsetDateTime?
set(value): Unit = set(19, value)
get(): OffsetDateTime? = get(19) as OffsetDateTime?
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Primary key information // Primary key information
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -103,11 +113,11 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun key(): Record1<Long?> = super.key() as Record1<Long?> override fun key(): Record1<Long?> = super.key() as Record1<Long?>
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Record18 type implementation // Record20 type implementation
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> = super.fieldsRow() as Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> override fun fieldsRow(): Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?> = super.fieldsRow() as Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?>
override fun valuesRow(): Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> = super.valuesRow() as Row18<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?> override fun valuesRow(): Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?> = super.valuesRow() as Row20<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?>
override fun field1(): Field<Long?> = Users.USERS.USER_ID override fun field1(): Field<Long?> = Users.USERS.USER_ID
override fun field2(): Field<String?> = Users.USERS.USERNAME override fun field2(): Field<String?> = Users.USERS.USERNAME
override fun field3(): Field<LocalDateTime?> = Users.USERS.JOIN_DATE override fun field3(): Field<LocalDateTime?> = Users.USERS.JOIN_DATE
@ -126,6 +136,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun field16(): Field<OffsetDateTime?> = Users.USERS.SYS_LAST_UPDATE override fun field16(): Field<OffsetDateTime?> = Users.USERS.SYS_LAST_UPDATE
override fun field17(): Field<Boolean?> = Users.USERS.IS_ADMIN override fun field17(): Field<Boolean?> = Users.USERS.IS_ADMIN
override fun field18(): Field<Long?> = Users.USERS.COUNT_MISS override fun field18(): Field<Long?> = Users.USERS.COUNT_MISS
override fun field19(): Field<Boolean?> = Users.USERS.IS_BANNED
override fun field20(): Field<OffsetDateTime?> = Users.USERS.APPROX_BAN_DATE
override fun component1(): Long? = userId override fun component1(): Long? = userId
override fun component2(): String? = username override fun component2(): String? = username
override fun component3(): LocalDateTime? = joinDate override fun component3(): LocalDateTime? = joinDate
@ -144,6 +156,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun component16(): OffsetDateTime? = sysLastUpdate override fun component16(): OffsetDateTime? = sysLastUpdate
override fun component17(): Boolean? = isAdmin override fun component17(): Boolean? = isAdmin
override fun component18(): Long? = countMiss override fun component18(): Long? = countMiss
override fun component19(): Boolean? = isBanned
override fun component20(): OffsetDateTime? = approxBanDate
override fun value1(): Long? = userId override fun value1(): Long? = userId
override fun value2(): String? = username override fun value2(): String? = username
override fun value3(): LocalDateTime? = joinDate override fun value3(): LocalDateTime? = joinDate
@ -162,6 +176,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun value16(): OffsetDateTime? = sysLastUpdate override fun value16(): OffsetDateTime? = sysLastUpdate
override fun value17(): Boolean? = isAdmin override fun value17(): Boolean? = isAdmin
override fun value18(): Long? = countMiss override fun value18(): Long? = countMiss
override fun value19(): Boolean? = isBanned
override fun value20(): OffsetDateTime? = approxBanDate
override fun value1(value: Long?): UsersRecord { override fun value1(value: Long?): UsersRecord {
set(0, value) set(0, value)
@ -253,7 +269,17 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
return this return this
} }
override fun values(value1: Long?, value2: String?, value3: LocalDateTime?, value4: String?, value5: Long?, value6: Long?, value7: Double?, value8: Double?, value9: Long?, value10: Long?, value11: Long?, value12: Long?, value13: Long?, value14: Long?, value15: Long?, value16: OffsetDateTime?, value17: Boolean?, value18: Long?): UsersRecord { override fun value19(value: Boolean?): UsersRecord {
set(18, value)
return this
}
override fun value20(value: OffsetDateTime?): UsersRecord {
set(19, value)
return this
}
override fun values(value1: Long?, value2: String?, value3: LocalDateTime?, value4: String?, value5: Long?, value6: Long?, value7: Double?, value8: Double?, value9: Long?, value10: Long?, value11: Long?, value12: Long?, value13: Long?, value14: Long?, value15: Long?, value16: OffsetDateTime?, value17: Boolean?, value18: Long?, value19: Boolean?, value20: OffsetDateTime?): UsersRecord {
this.value1(value1) this.value1(value1)
this.value2(value2) this.value2(value2)
this.value3(value3) this.value3(value3)
@ -272,13 +298,15 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
this.value16(value16) this.value16(value16)
this.value17(value17) this.value17(value17)
this.value18(value18) this.value18(value18)
this.value19(value19)
this.value20(value20)
return this return this
} }
/** /**
* Create a detached, initialised UsersRecord * Create a detached, initialised UsersRecord
*/ */
constructor(userId: Long? = null, username: String? = null, joinDate: LocalDateTime? = null, country: String? = null, countryRank: Long? = null, rank: Long? = null, ppRaw: Double? = null, accuracy: Double? = null, playcount: Long? = null, totalScore: Long? = null, rankedScore: Long? = null, secondsPlayed: Long? = null, count_100: Long? = null, count_300: Long? = null, count_50: Long? = null, sysLastUpdate: OffsetDateTime? = null, isAdmin: Boolean? = null, countMiss: Long? = null): this() { constructor(userId: Long? = null, username: String? = null, joinDate: LocalDateTime? = null, country: String? = null, countryRank: Long? = null, rank: Long? = null, ppRaw: Double? = null, accuracy: Double? = null, playcount: Long? = null, totalScore: Long? = null, rankedScore: Long? = null, secondsPlayed: Long? = null, count_100: Long? = null, count_300: Long? = null, count_50: Long? = null, sysLastUpdate: OffsetDateTime? = null, isAdmin: Boolean? = null, countMiss: Long? = null, isBanned: Boolean? = null, approxBanDate: OffsetDateTime? = null): this() {
this.userId = userId this.userId = userId
this.username = username this.username = username
this.joinDate = joinDate this.joinDate = joinDate
@ -297,6 +325,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
this.sysLastUpdate = sysLastUpdate this.sysLastUpdate = sysLastUpdate
this.isAdmin = isAdmin this.isAdmin = isAdmin
this.countMiss = countMiss this.countMiss = countMiss
this.isBanned = isBanned
this.approxBanDate = approxBanDate
resetChangedOnNotNull() resetChangedOnNotNull()
} }
} }

View File

@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserFollows
import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
@ -58,6 +59,11 @@ val SCORES_SIMILARITY: ScoresSimilarity = ScoresSimilarity.SCORES_SIMILARITY
*/ */
val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE
/**
* The table <code>public.user_follows</code>.
*/
val USER_FOLLOWS: UserFollows = UserFollows.USER_FOLLOWS
/** /**
* The table <code>public.user_scores</code>. * The table <code>public.user_scores</code>.
*/ */

View File

@ -1,14 +1,15 @@
package com.nisemoe.nise package com.nisemoe.nise
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.annotation.EnableCaching
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession
@SpringBootApplication @SpringBootApplication
@EnableCaching @EnableCaching
@EnableScheduling @EnableScheduling
@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 2592000)
class NiseApplication class NiseApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@ -0,0 +1,34 @@
package com.nisemoe.nise.controller
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.generated.tables.references.USER_FOLLOWS
import com.nisemoe.nise.service.AuthService
import jakarta.validation.Valid
import jakarta.validation.constraints.Size
import org.jooq.DSLContext
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime
@RestController
class BanlistController(
private val dslContext: DSLContext
) {
data class BanStatisticsResponse(
val totalUsersBanned: Int
)
@GetMapping("banlist/statistics")
fun getBanStatistics(): BanStatisticsResponse {
val totalUsersBanned = dslContext.fetchCount(USERS, USERS.IS_BANNED.eq(true))
return BanStatisticsResponse(totalUsersBanned)
}
}

View File

@ -0,0 +1,157 @@
package com.nisemoe.nise.controller
import com.nisemoe.generated.tables.references.USERS
import com.nisemoe.generated.tables.references.USER_FOLLOWS
import com.nisemoe.nise.service.AuthService
import jakarta.validation.Valid
import jakarta.validation.constraints.Size
import org.jooq.DSLContext
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime
@RestController
class FollowsController(
private val dslContext: DSLContext,
private val authService: AuthService
) {
companion object {
const val MAX_FOLLOWS_PER_USER = 1000
}
data class FollowsBanStatusResponse(
val follows: List<FollowsBanStatusEntry>
)
data class FollowsBanStatusEntry(
val userId: Long,
val username: String,
val isBanned: Boolean,
val lastUpdate: OffsetDateTime
)
@GetMapping("follows")
fun getFollowsBanStatus(): ResponseEntity<FollowsBanStatusResponse> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val follows = dslContext.select(
USERS.USER_ID,
USERS.USERNAME,
USERS.IS_BANNED,
USERS.SYS_LAST_UPDATE
)
.from(USER_FOLLOWS)
.join(USERS).on(USER_FOLLOWS.FOLLOWS_USER_ID.eq(USERS.USER_ID))
.where(USER_FOLLOWS.USER_ID.eq(authService.getCurrentUser().userId))
.fetch()
.map {
FollowsBanStatusEntry(
it[USERS.USER_ID]!!,
it[USERS.USERNAME]!!,
it[USERS.IS_BANNED]!!,
it[USERS.SYS_LAST_UPDATE]!!
)
}
return ResponseEntity.ok(FollowsBanStatusResponse(follows))
}
// TODO: CSRF
@GetMapping("follows/{followsUserId}")
fun getFollowsBanStatusByUserId(@PathVariable followsUserId: Long): ResponseEntity<FollowsBanStatusEntry> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val userId = authService.getCurrentUser().userId
val follows = dslContext.select(
USERS.USER_ID,
USERS.USERNAME,
USERS.IS_BANNED,
USERS.SYS_LAST_UPDATE
)
.from(USER_FOLLOWS)
.join(USERS).on(USER_FOLLOWS.FOLLOWS_USER_ID.eq(USERS.USER_ID))
.where(USER_FOLLOWS.USER_ID.eq(userId).and(USER_FOLLOWS.FOLLOWS_USER_ID.eq(followsUserId)))
.fetch()
.map {
FollowsBanStatusEntry(
it[USERS.USER_ID]!!,
it[USERS.USERNAME]!!,
it[USERS.IS_BANNED]!!,
it[USERS.SYS_LAST_UPDATE]!!
)
}
return if(follows.isEmpty()) {
ResponseEntity.status(404).build()
} else {
ResponseEntity.ok(follows.first())
}
}
data class UpdateFollowsBanStatusRequest(
@Valid @field:Size(max = 200)
val userIds: List<Long>,
)
@PutMapping("follows")
fun updateFollowsBanStatus(@RequestBody @Valid request: UpdateFollowsBanStatusRequest): ResponseEntity<Void> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
// Check if the user already has MAX_FOLLOWS_PER_USER or more
if(dslContext.fetchCount(USER_FOLLOWS, USER_FOLLOWS.USER_ID.eq(authService.getCurrentUser().userId)) >= MAX_FOLLOWS_PER_USER) {
return ResponseEntity.status(400).build()
}
val userId = authService.getCurrentUser().userId
for(userIdToBan in request.userIds) {
dslContext.insertInto(USER_FOLLOWS)
.columns(USER_FOLLOWS.USER_ID, USER_FOLLOWS.FOLLOWS_USER_ID)
.values(userId, userIdToBan)
.onDuplicateKeyIgnore()
.execute()
}
return ResponseEntity.ok().build()
}
data class DeleteFollowsBanStatusRequest(
@Valid @field:Size(max = 200)
val userIds: List<Long>,
)
@DeleteMapping("follows")
fun deleteFollowsBanStatus(@RequestBody @Valid request: DeleteFollowsBanStatusRequest): ResponseEntity<Void> {
if(!authService.isLoggedIn()) {
return ResponseEntity.status(401).build()
}
val userId = authService.getCurrentUser().userId
for(userIdToUnban in request.userIds) {
dslContext.deleteFrom(USER_FOLLOWS)
.where(USER_FOLLOWS.USER_ID.eq(userId))
.and(USER_FOLLOWS.FOLLOWS_USER_ID.eq(userIdToUnban))
.execute()
}
return ResponseEntity.ok().build()
}
}

View File

@ -532,7 +532,7 @@ class ScoreService(
error = it.error!!, error = it.error!!,
distanceToCenter = it.distanceCenter!!, distanceToCenter = it.distanceCenter!!,
distanceToEdge = it.distanceEdge!!, distanceToEdge = it.distanceEdge!!,
time = it.time!!, time = it.time!!.toInt(),
type = mapLegacyJudgement(it.type!!) type = mapLegacyJudgement(it.type!!)
) )
} }

View File

@ -348,6 +348,11 @@ class ImportScores(
.set(SCORES.IS_BANNED, true) .set(SCORES.IS_BANNED, true)
.where(SCORES.USER_ID.eq(userId)) .where(SCORES.USER_ID.eq(userId))
.execute() .execute()
dslContext.update(USERS)
.set(USERS.IS_BANNED, true)
.set(USERS.APPROX_BAN_DATE, OffsetDateTime.now())
.where(USERS.USER_ID.eq(userId))
.execute()
this.logger.info("User $userId is banned.") this.logger.info("User $userId is banned.")
} }
Thread.sleep(SLEEP_AFTER_API_CALL) Thread.sleep(SLEEP_AFTER_API_CALL)
@ -724,7 +729,7 @@ class ImportScores(
replayData = scoreReplay.content, beatmapData = beatmapFile, mods = Mod.combineModStrings(score.mods) replayData = scoreReplay.content, beatmapData = beatmapFile, mods = Mod.combineModStrings(score.mods)
).get() ).get()
} catch (e: Exception) { } catch (e: Exception) {
this.logger.error("Circleguard failed to process replay with score_id: ${score.id}") this.logger.error("Circleguard failed to process replay with score_id: ${score.id}", e)
return return
} }

View File

@ -107,6 +107,11 @@ class ImportUsers(
.set(SCORES.IS_BANNED, true) .set(SCORES.IS_BANNED, true)
.where(SCORES.USER_ID.eq(missingId)) .where(SCORES.USER_ID.eq(missingId))
.execute() .execute()
dslContext.update(USERS)
.set(USERS.IS_BANNED, true)
.set(USERS.APPROX_BAN_DATE, OffsetDateTime.now())
.where(USERS.USER_ID.eq(missingId))
.execute()
} }
} }

View File

@ -17,6 +17,10 @@ spring.data.redis.port=${REDIS_PORT:6379}
spring.data.redis.repositories.enabled=false spring.data.redis.repositories.enabled=false
spring.data.redis.database=${REDIS_DB:2} spring.data.redis.database=${REDIS_DB:2}
# session
server.servlet.session.timeout=2592000
spring.session.store-type=redis
# osu!auth # osu!auth
spring.security.oauth2.client.registration.osu.clientId=${OSU_CLIENT_ID} spring.security.oauth2.client.registration.osu.clientId=${OSU_CLIENT_ID}

View File

@ -0,0 +1,11 @@
alter table public.users
add column is_banned boolean default false,
add column approx_ban_date timestamp with time zone;
update public.users
set is_banned = true,
approx_ban_date = now()
from (select distinct user_id
from scores
where is_banned = true) banned_users
where public.users.user_id = banned_users.user_id;

View File

@ -0,0 +1,8 @@
create table public.user_follows (
user_id bigserial not null,
follows_user_id bigserial not null,
primary key (user_id, follows_user_id),
foreign key (user_id) references public.users (user_id),
foreign key (follows_user_id) references public.users (user_id)
);

View File

@ -8,6 +8,7 @@ import {ViewUserComponent} from "./view-user/view-user.component";
import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component"; import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component";
import {SearchComponent} from "./search/search.component"; import {SearchComponent} from "./search/search.component";
import {ContributeComponent} from "./contribute/contribute.component"; import {ContributeComponent} from "./contribute/contribute.component";
import {BanlistComponent} from "./banlist/banlist.component";
const routes: Routes = [ const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -23,6 +24,7 @@ const routes: Routes = [
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent}, {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: 'banlist', component: BanlistComponent, title: '/ban/'},
{path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'}, {path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'},
{path: '**', component: HomeComponent, title: '/nise.moe/'}, {path: '**', component: HomeComponent, title: '/nise.moe/'},
]; ];

View File

@ -7,3 +7,11 @@
overflow: hidden; overflow: hidden;
vertical-align: bottom; vertical-align: bottom;
} }
.link-pink {
color: #dd8fdcf7
}
.link-pink:hover {
color: rgba(234, 78, 179, 0.97)
}

View File

@ -11,7 +11,7 @@
<li><a [routerLink]="['/sus']">./suspicious-scores</a></li> <li><a [routerLink]="['/sus']">./suspicious-scores</a></li>
<li><a [routerLink]="['/stolen']">./stolen-replays</a></li> <li><a [routerLink]="['/stolen']">./stolen-replays</a></li>
<li><a [routerLink]="['/search']">./advanced-search</a></li> <li><a [routerLink]="['/search']">./advanced-search</a></li>
<li><a style="color: #dd8fdcf7" [routerLink]="['/contribute']">./contribute <3</a></li> <li><a class="link-pink" [routerLink]="['/contribute']">./contribute <3</a></li>
</ul> </ul>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<input style="width: 100%" type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users..."> <input style="width: 100%" type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users...">

View File

@ -0,0 +1,3 @@
table td {
text-align: center;
}

View File

@ -0,0 +1,31 @@
<div class="main term mb-2">
<div class="fade-stuff">
<h1 class="mb-4"># follow-list</h1>
<table *ngIf="this.follows">
<thead>
<tr>
<th colspan="2">Username</th>
<th>Is banned?</th>
<th>Last check</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of this.follows.follows">
<td>
<img [src]="'https://a.ppy.sh/' + user.userId" class="avatar" style="width: 16px; min-height: 16px; height: 16px;">
</td>
<td>
<a [routerLink]="['/u', user.username]">
{{ user.username }}
</a>
</td>
<td>{{ user.isBanned }}</td>
<td>{{ calculateTimeAgo(user.lastUpdate) }}</td>
<td>
</td>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,61 @@
import {Component, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {JsonPipe, NgForOf, NgIf} from "@angular/common";
import {calculateTimeAgo} from "../format";
import {RouterLink} from "@angular/router";
interface BanStatisticsResponse {
totalUsersBanned: number;
}
interface FollowsBanStatusResponse {
follows: FollowsBanStatusEntry[];
}
interface FollowsBanStatusEntry {
userId: number;
username: string;
isBanned: boolean;
lastUpdate: string;
}
@Component({
selector: 'app-banlist',
standalone: true,
imports: [
JsonPipe,
NgForOf,
NgIf,
RouterLink
],
templateUrl: './banlist.component.html',
styleUrl: './banlist.component.css'
})
export class BanlistComponent implements OnInit {
banStatistics: BanStatisticsResponse | null = null
follows: FollowsBanStatusResponse | null = null;
constructor(private httpClient: HttpClient) { }
ngOnInit(): void {
this.getBanStatistics();
this.getFollows();
}
getBanStatistics() {
this.httpClient.get<BanStatisticsResponse>(`${environment.apiUrl}/banlist/statistics`).subscribe(response => {
this.banStatistics = response;
});
}
getFollows(): void {
this.httpClient.get<FollowsBanStatusResponse>(`${environment.apiUrl}/follows`).subscribe(response => {
this.follows = response;
});
}
protected readonly calculateTimeAgo = calculateTimeAgo;
}

View File

@ -1,4 +1,5 @@
import {ReplayData} from "./replays"; import {ReplayData} from "./replays";
import {differenceInDays, differenceInHours} from "date-fns/fp";
export function formatDuration(seconds: number): string | null { export function formatDuration(seconds: number): string | null {
if(!seconds) { if(!seconds) {
@ -50,3 +51,24 @@ export function calculateAccuracy(replayData: ReplayData): number {
const accuracy = (300 * hit300 + 100 * hit100 + 50 * hit50) / (300 * totalHits); const accuracy = (300 * hit300 + 100 * hit100 + 50 * hit50) / (300 * totalHits);
return accuracy * 100; return accuracy * 100;
} }
export function calculateTimeAgo(dateStr: string): string {
const inputDate = new Date(dateStr);
const now = new Date();
if (isNaN(inputDate.getTime())) {
return "???";
}
const difference = Math.abs(differenceInHours(now, inputDate));
if (difference < 1) {
return "recently";
} else if (difference < 24) {
return `${difference}h ago`;
} else {
const days = Math.abs(differenceInDays(now, inputDate));
const hours = difference % 24;
return `${days}d ${hours}h ago`;
}
}

View File

@ -12,6 +12,11 @@
<h1> <h1>
<img [src]="'https://a.ppy.sh/' + this.userInfo.user_details.user_id" class="avatar"> <img [src]="'https://a.ppy.sh/' + this.userInfo.user_details.user_id" class="avatar">
{{ this.userInfo.user_details.username }} {{ this.userInfo.user_details.username }}
<ng-container *ngIf="this.userService.isUserLoggedIn() && this.isFollowing != null">
<button *ngIf="!this.followService.userFollowStatus.get(this.userInfo.user_details.user_id)" (click)="this.followService.addNewUserFollow(this.userInfo.user_details.user_id)" class="btn btn-outline-secondary btn-sm">(+) Add Follow</button>
<button *ngIf="this.followService.userFollowStatus.get(this.userInfo.user_details.user_id)" (click)="this.followService.removeUserFollow(this.userInfo.user_details.user_id)" class="btn btn-outline-secondary btn-sm">(-) Remove follow</button>
</ng-container>
</h1> </h1>
<div class="mb-2 mt-2 btn-group"> <div class="mb-2 mt-2 btn-group">
@ -51,7 +56,7 @@
<span class="btn-warning">wait a bit to force update</span> <span class="btn-warning">wait a bit to force update</span>
</ng-container> </ng-container>
<span style="margin-left: 4px">|</span> <span style="margin-left: 4px">|</span>
last update: {{ this.userInfo.queue_details.lastCompletedUpdate ? this.calculateTimeAgo(this.userInfo.queue_details.lastCompletedUpdate) : 'never'}} last update: {{ this.userInfo.queue_details.lastCompletedUpdate ? calculateTimeAgo(this.userInfo.queue_details.lastCompletedUpdate) : 'never'}}
</ng-container> </ng-container>
<ng-template #updateProgress> <ng-template #updateProgress>
<div class="progress"> <div class="progress">

View File

@ -6,14 +6,14 @@ import {environment} from "../../environments/environment";
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {ActivatedRoute, RouterLink} from "@angular/router"; import {ActivatedRoute, RouterLink} from "@angular/router";
import {UserDetails, UserQueueDetails} from "../userDetails"; import {UserDetails, UserQueueDetails} from "../userDetails";
import {countryCodeToFlag, formatDuration} from "../format"; import {calculateTimeAgo, countryCodeToFlag, formatDuration} from "../format";
import {Title} from "@angular/platform-browser"; import {Title} from "@angular/platform-browser";
import {RxStompService} from "../../corelib/stomp/stomp.service"; import {RxStompService} from "../../corelib/stomp/stomp.service";
import {Message} from "@stomp/stompjs/esm6"; import {Message} from "@stomp/stompjs/esm6";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component"; import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
import {differenceInDays, differenceInHours} from "date-fns/fp";
import {FilterManagerService} from "../filter-manager.service"; import {FilterManagerService} from "../filter-manager.service";
import {UserService} from "../../corelib/service/user.service"; import {UserService} from "../../corelib/service/user.service";
import {FollowService} from "../../corelib/service/follow.service";
interface UserInfo { interface UserInfo {
user_details: UserDetails; user_details: UserDetails;
@ -49,6 +49,7 @@ interface UserScoresFilter {
}) })
export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
isFollowing: boolean | null = null;
isLoading = false; isLoading = false;
notFound = false; notFound = false;
userId: string | null = null; userId: string | null = null;
@ -64,7 +65,8 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private title: Title, private title: Title,
private rxStompService: RxStompService, private rxStompService: RxStompService,
public userService: UserService public userService: UserService,
public followService: FollowService
) { } ) { }
getUserInfo(): Observable<UserInfo> { getUserInfo(): Observable<UserInfo> {
@ -93,6 +95,14 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
}); });
} }
private checkIfUserIsFollowed(): void {
if(this.userService.isUserLoggedIn()) {
this.followService.checkIfUserIsFollowed(this.userInfo!.user_details.user_id).then(isFollowing => {
this.isFollowing = isFollowing;
});
}
}
private loadUser(isScoreUpdate = false) { private loadUser(isScoreUpdate = false) {
this.getUserInfo().pipe( this.getUserInfo().pipe(
catchError(error => { catchError(error => {
@ -121,6 +131,7 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.title.setTitle(`${this.userInfo.user_details.username}`); this.title.setTitle(`${this.userInfo.user_details.username}`);
this.subscribeToUser(); this.subscribeToUser();
} }
this.checkIfUserIsFollowed();
} }
); );
} }
@ -129,27 +140,6 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.liveUserSub?.unsubscribe(); this.liveUserSub?.unsubscribe();
} }
calculateTimeAgo(dateStr: string): string {
const inputDate = new Date(dateStr);
const now = new Date();
if (isNaN(inputDate.getTime())) {
return "???";
}
const difference = Math.abs(differenceInHours(now, inputDate));
if (difference < 1) {
return "recently";
} else if (difference < 24) {
return `${difference}h ago`;
} else {
const days = Math.abs(differenceInDays(now, inputDate));
const hours = difference % 24;
return `${days}d ${hours}h ago`;
}
}
addUserToQueue(): void { addUserToQueue(): void {
const body = { const body = {
userId: this.userInfo?.user_details.user_id userId: this.userInfo?.user_details.user_id
@ -202,6 +192,6 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
protected readonly formatDuration = formatDuration; protected readonly formatDuration = formatDuration;
protected readonly countryCodeToFlag = countryCodeToFlag; protected readonly countryCodeToFlag = countryCodeToFlag;
protected readonly calculateTimeAgo = calculateTimeAgo;
} }

View File

@ -0,0 +1,86 @@
import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {UserService} from "./user.service";
@Injectable({
providedIn: 'root'
})
export class FollowService {
userFollowStatus: Map<number, boolean> = new Map<number, boolean>();
constructor(
private userService: UserService,
private httpClient: HttpClient
) { }
removeUserFollow(userId: number): Promise<boolean> {
if(!this.userService.isUserLoggedIn()) {
return Promise.resolve(false);
}
const body = {
userIds: [userId]
}
return new Promise((resolve, reject) => {
this.httpClient.delete(`${environment.apiUrl}/follows`, { body: body })
.subscribe({
next: () => {
this.userFollowStatus.set(userId, false);
resolve(true);
},
error: (err) => {
reject(err);
}
});
});
}
addNewUserFollow(userId: number): Promise<boolean> {
if(!this.userService.isUserLoggedIn()) {
return Promise.resolve(false);
}
const body = {
userIds: [userId]
}
return new Promise((resolve, reject) => {
this.httpClient.put(`${environment.apiUrl}/follows`, body)
.subscribe({
next: () => {
this.userFollowStatus.set(userId, true);
resolve(true);
},
error: (err) => {
reject(err);
}
});
});
}
checkIfUserIsFollowed(userId: number): Promise<boolean> {
if(!this.userService.isUserLoggedIn()) {
return Promise.resolve(false);
}
return new Promise<boolean>((resolve, reject) => {
this.httpClient.get<boolean>(`${environment.apiUrl}/follows/${userId}`)
.subscribe({
next: (isFollowing) => {
this.userFollowStatus.set(userId, isFollowing);
resolve(isFollowing);
},
error: (err) => {
if (err.status === 404) {
this.userFollowStatus.set(userId, false);
resolve(false);
} else {
reject(err);
}
}
});
});
}
}