diff --git a/mari/.github/FUNDING.yml b/mari/.github/FUNDING.yml new file mode 100644 index 0000000..05ab66b --- /dev/null +++ b/mari/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: nise_moe \ No newline at end of file diff --git a/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt b/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt index 175c11c..327fcba 100644 --- a/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt +++ b/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt @@ -1,7 +1,32 @@ package org.nisemoe.mari.judgements +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName 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 { + + 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. @@ -9,7 +34,9 @@ import kotlinx.serialization.Serializable */ @Serializable data class Judgement( + @Serializable(with = TimeSerializer::class) val time: Int, + val x: Double, val y: Double, val type: Type, diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt index 65669ad..bf5dfc6 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt @@ -22,6 +22,7 @@ import com.nisemoe.generated.tables.Scores import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue +import com.nisemoe.generated.tables.UserFollows import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScoresSimilarity 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 + /** + * The table public.user_follows. + */ + val USER_FOLLOWS: UserFollows get() = UserFollows.USER_FOLLOWS + /** * The table public.user_scores. */ @@ -126,6 +132,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) { ScoresJudgements.SCORES_JUDGEMENTS, ScoresSimilarity.SCORES_SIMILARITY, UpdateUserQueue.UPDATE_USER_QUEUE, + UserFollows.USER_FOLLOWS, UserScores.USER_SCORES, UserScoresSimilarity.USER_SCORES_SIMILARITY, Users.USERS diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt index 197ac91..ff1cc96 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt @@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue +import com.nisemoe.generated.tables.UserFollows import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.Users 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.ScoresSimilarityRecord 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.UsersRecord @@ -46,6 +48,7 @@ val SCORES_JUDGEMENTS_PKEY: UniqueKey = Internal.createU val SCORES_SIMILARITY_PKEY: UniqueKey = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true) val UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey = 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 = Internal.createUniqueKey(UpdateUserQueue.UPDATE_USER_QUEUE, DSL.name("update_user_queue_pkey"), arrayOf(UpdateUserQueue.UPDATE_USER_QUEUE.ID), true) +val USER_FOLLOWS_PKEY: UniqueKey = 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 = 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 = 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 = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true) @@ -55,3 +58,5 @@ val USERS_PKEY: UniqueKey = Internal.createUniqueKey(Users.USERS, D // ------------------------------------------------------------------------- val SCORES_JUDGEMENTS__SCORES_JUDGEMENTS_SCORE_ID_FKEY: ForeignKey = 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 = 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 = 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) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserFollows.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserFollows.kt new file mode 100644 index 0000000..565380f --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserFollows.kt @@ -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?, + path: ForeignKey?, + aliased: Table?, + parameters: Array?>? +): TableImpl( + alias, + Public.PUBLIC, + child, + path, + aliased, + parameters, + DSL.comment(""), + TableOptions.table() +) { + companion object { + + /** + * The reference instance of public.user_follows + */ + val USER_FOLLOWS: UserFollows = UserFollows() + } + + /** + * The class holding records for this type + */ + override fun getRecordType(): Class = UserFollowsRecord::class.java + + /** + * The column public.user_follows.user_id. + */ + val USER_ID: TableField = createField(DSL.name("user_id"), SQLDataType.BIGINT.nullable(false).identity(true), this, "") + + /** + * The column public.user_follows.follows_user_id. + */ + val FOLLOWS_USER_ID: TableField = createField(DSL.name("follows_user_id"), SQLDataType.BIGINT.nullable(false).identity(true), this, "") + + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) + private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) + + /** + * Create an aliased public.user_follows table reference + */ + constructor(alias: String): this(DSL.name(alias)) + + /** + * Create an aliased public.user_follows table reference + */ + constructor(alias: Name): this(alias, null) + + /** + * Create a public.user_follows table reference + */ + constructor(): this(DSL.name("user_follows"), null) + + constructor(child: Table, key: ForeignKey): this(Internal.createPathAlias(child, key), child, key, USER_FOLLOWS, null) + override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC + override fun getIdentity(): Identity = super.getIdentity() as Identity + override fun getPrimaryKey(): UniqueKey = USER_FOLLOWS_PKEY + override fun getReferences(): List> = 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 public.users table, via + * the user_follows_user_id_fkey 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 public.users table, via + * the user_follows_follows_user_id_fkey 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 = super.fieldsRow() as Row2 + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Function)}. + */ + fun mapping(from: (Long?, Long?) -> U): SelectField = convertFrom(Records.mapping(from)) + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Class, + * Function)}. + */ + fun mapping(toType: Class, from: (Long?, Long?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Users.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Users.kt index 5d21d4d..b239023 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Users.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Users.kt @@ -17,7 +17,7 @@ import org.jooq.ForeignKey import org.jooq.Name import org.jooq.Record import org.jooq.Records -import org.jooq.Row18 +import org.jooq.Row20 import org.jooq.Schema import org.jooq.SelectField import org.jooq.Table @@ -153,6 +153,16 @@ open class Users( */ val COUNT_MISS: TableField = createField(DSL.name("count_miss"), SQLDataType.BIGINT, this, "") + /** + * The column public.users.is_banned. + */ + val IS_BANNED: TableField = createField(DSL.name("is_banned"), SQLDataType.BOOLEAN.defaultValue(DSL.field(DSL.raw("false"), SQLDataType.BOOLEAN)), this, "") + + /** + * The column public.users.approx_ban_date. + */ + val APPROX_BAN_DATE: TableField = createField(DSL.name("approx_ban_date"), SQLDataType.TIMESTAMPWITHTIMEZONE(6), this, "") + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) @@ -194,18 +204,18 @@ open class Users( override fun rename(name: Table<*>): Users = Users(name.getQualifiedName(), null) // ------------------------------------------------------------------------- - // Row18 type methods + // Row20 type methods // ------------------------------------------------------------------------- - override fun fieldsRow(): Row18 = super.fieldsRow() as Row18 + override fun fieldsRow(): Row20 = super.fieldsRow() as Row20 /** * Convenience mapping calling {@link SelectField#convertFrom(Function)}. */ - fun mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?) -> U): SelectField = convertFrom(Records.mapping(from)) + fun mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?) -> U): SelectField = convertFrom(Records.mapping(from)) /** * Convenience mapping calling {@link SelectField#convertFrom(Class, * Function)}. */ - fun mapping(toType: Class, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) + fun mapping(toType: Class, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, OffsetDateTime?, Boolean?, Long?, Boolean?, OffsetDateTime?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserFollowsRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserFollowsRecord.kt new file mode 100644 index 0000000..0f8d034 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserFollowsRecord.kt @@ -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(UserFollows.USER_FOLLOWS), Record2 { + + 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 = super.key() as Record2 + + // ------------------------------------------------------------------------- + // Record2 type implementation + // ------------------------------------------------------------------------- + + override fun fieldsRow(): Row2 = super.fieldsRow() as Row2 + override fun valuesRow(): Row2 = super.valuesRow() as Row2 + override fun field1(): Field = UserFollows.USER_FOLLOWS.USER_ID + override fun field2(): Field = 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() + } +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UsersRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UsersRecord.kt index b67cf84..adfb36c 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UsersRecord.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UsersRecord.kt @@ -11,8 +11,8 @@ import java.time.OffsetDateTime import org.jooq.Field import org.jooq.Record1 -import org.jooq.Record18 -import org.jooq.Row18 +import org.jooq.Record20 +import org.jooq.Row20 import org.jooq.impl.UpdatableRecordImpl @@ -20,7 +20,7 @@ import org.jooq.impl.UpdatableRecordImpl * This class is generated by jOOQ. */ @Suppress("UNCHECKED_CAST") -open class UsersRecord private constructor() : UpdatableRecordImpl(Users.USERS), Record18 { +open class UsersRecord private constructor() : UpdatableRecordImpl(Users.USERS), Record20 { open var userId: Long? set(value): Unit = set(0, value) @@ -96,6 +96,16 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( set(value): Unit = set(17, value) 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 // ------------------------------------------------------------------------- @@ -103,11 +113,11 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( override fun key(): Record1 = super.key() as Record1 // ------------------------------------------------------------------------- - // Record18 type implementation + // Record20 type implementation // ------------------------------------------------------------------------- - override fun fieldsRow(): Row18 = super.fieldsRow() as Row18 - override fun valuesRow(): Row18 = super.valuesRow() as Row18 + override fun fieldsRow(): Row20 = super.fieldsRow() as Row20 + override fun valuesRow(): Row20 = super.valuesRow() as Row20 override fun field1(): Field = Users.USERS.USER_ID override fun field2(): Field = Users.USERS.USERNAME override fun field3(): Field = Users.USERS.JOIN_DATE @@ -126,6 +136,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( override fun field16(): Field = Users.USERS.SYS_LAST_UPDATE override fun field17(): Field = Users.USERS.IS_ADMIN override fun field18(): Field = Users.USERS.COUNT_MISS + override fun field19(): Field = Users.USERS.IS_BANNED + override fun field20(): Field = Users.USERS.APPROX_BAN_DATE override fun component1(): Long? = userId override fun component2(): String? = username override fun component3(): LocalDateTime? = joinDate @@ -144,6 +156,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( override fun component16(): OffsetDateTime? = sysLastUpdate override fun component17(): Boolean? = isAdmin override fun component18(): Long? = countMiss + override fun component19(): Boolean? = isBanned + override fun component20(): OffsetDateTime? = approxBanDate override fun value1(): Long? = userId override fun value2(): String? = username override fun value3(): LocalDateTime? = joinDate @@ -162,6 +176,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( override fun value16(): OffsetDateTime? = sysLastUpdate override fun value17(): Boolean? = isAdmin override fun value18(): Long? = countMiss + override fun value19(): Boolean? = isBanned + override fun value20(): OffsetDateTime? = approxBanDate override fun value1(value: Long?): UsersRecord { set(0, value) @@ -253,7 +269,17 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( 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.value2(value2) this.value3(value3) @@ -272,13 +298,15 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( this.value16(value16) this.value17(value17) this.value18(value18) + this.value19(value19) + this.value20(value20) return this } /** * 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.username = username this.joinDate = joinDate @@ -297,6 +325,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl( this.sysLastUpdate = sysLastUpdate this.isAdmin = isAdmin this.countMiss = countMiss + this.isBanned = isBanned + this.approxBanDate = approxBanDate resetChangedOnNotNull() } } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt index 484dc09..50aaed0 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt @@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue +import com.nisemoe.generated.tables.UserFollows import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScoresSimilarity 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 +/** + * The table public.user_follows. + */ +val USER_FOLLOWS: UserFollows = UserFollows.USER_FOLLOWS + /** * The table public.user_scores. */ diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/NiseApplication.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/NiseApplication.kt index e922e21..9eddbc3 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/NiseApplication.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/NiseApplication.kt @@ -1,14 +1,15 @@ package com.nisemoe.nise import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication import org.springframework.cache.annotation.EnableCaching import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession @SpringBootApplication @EnableCaching @EnableScheduling +@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 2592000) class NiseApplication fun main(args: Array) { diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt new file mode 100644 index 0000000..5ee8c4a --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt @@ -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) + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt new file mode 100644 index 0000000..503d8cd --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt @@ -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 + ) + + data class FollowsBanStatusEntry( + val userId: Long, + val username: String, + val isBanned: Boolean, + val lastUpdate: OffsetDateTime + ) + + @GetMapping("follows") + fun getFollowsBanStatus(): ResponseEntity { + 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 { + 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, + ) + + @PutMapping("follows") + fun updateFollowsBanStatus(@RequestBody @Valid request: UpdateFollowsBanStatusRequest): ResponseEntity { + 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, + ) + + @DeleteMapping("follows") + fun deleteFollowsBanStatus(@RequestBody @Valid request: DeleteFollowsBanStatusRequest): ResponseEntity { + 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() + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt index 3b85cf8..f95ca1a 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt @@ -532,7 +532,7 @@ class ScoreService( error = it.error!!, distanceToCenter = it.distanceCenter!!, distanceToEdge = it.distanceEdge!!, - time = it.time!!, + time = it.time!!.toInt(), type = mapLegacyJudgement(it.type!!) ) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt index d7a5b16..e7d896a 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt @@ -348,6 +348,11 @@ class ImportScores( .set(SCORES.IS_BANNED, true) .where(SCORES.USER_ID.eq(userId)) .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.") } Thread.sleep(SLEEP_AFTER_API_CALL) @@ -724,7 +729,7 @@ class ImportScores( replayData = scoreReplay.content, beatmapData = beatmapFile, mods = Mod.combineModStrings(score.mods) ).get() } 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 } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt index 86f7fce..9708525 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt @@ -107,6 +107,11 @@ class ImportUsers( .set(SCORES.IS_BANNED, true) .where(SCORES.USER_ID.eq(missingId)) .execute() + dslContext.update(USERS) + .set(USERS.IS_BANNED, true) + .set(USERS.APPROX_BAN_DATE, OffsetDateTime.now()) + .where(USERS.USER_ID.eq(missingId)) + .execute() } } diff --git a/nise-backend/src/main/resources/application.properties b/nise-backend/src/main/resources/application.properties index 422e9b1..94714d4 100644 --- a/nise-backend/src/main/resources/application.properties +++ b/nise-backend/src/main/resources/application.properties @@ -17,6 +17,10 @@ spring.data.redis.port=${REDIS_PORT:6379} spring.data.redis.repositories.enabled=false spring.data.redis.database=${REDIS_DB:2} +# session +server.servlet.session.timeout=2592000 +spring.session.store-type=redis + # osu!auth spring.security.oauth2.client.registration.osu.clientId=${OSU_CLIENT_ID} diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.031__alter_users.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.031__alter_users.sql new file mode 100644 index 0000000..2bf9b1c --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.031__alter_users.sql @@ -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; diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.032__create_user_follows.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.032__create_user_follows.sql new file mode 100644 index 0000000..874bede --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.032__create_user_follows.sql @@ -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) +); \ No newline at end of file diff --git a/nise-frontend/src/app/app-routing.module.ts b/nise-frontend/src/app/app-routing.module.ts index 4266e83..2f1ad20 100644 --- a/nise-frontend/src/app/app-routing.module.ts +++ b/nise-frontend/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import {ViewUserComponent} from "./view-user/view-user.component"; import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component"; import {SearchComponent} from "./search/search.component"; import {ContributeComponent} from "./contribute/contribute.component"; +import {BanlistComponent} from "./banlist/banlist.component"; const routes: Routes = [ {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, @@ -23,6 +24,7 @@ const routes: Routes = [ {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent}, + {path: 'banlist', component: BanlistComponent, title: '/ban/'}, {path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'}, {path: '**', component: HomeComponent, title: '/nise.moe/'}, ]; diff --git a/nise-frontend/src/app/app.component.css b/nise-frontend/src/app/app.component.css index 8164a73..a04bd0a 100644 --- a/nise-frontend/src/app/app.component.css +++ b/nise-frontend/src/app/app.component.css @@ -7,3 +7,11 @@ overflow: hidden; vertical-align: bottom; } + +.link-pink { + color: #dd8fdcf7 +} + +.link-pink:hover { + color: rgba(234, 78, 179, 0.97) +} diff --git a/nise-frontend/src/app/app.component.html b/nise-frontend/src/app/app.component.html index 27afc8f..a2639a4 100644 --- a/nise-frontend/src/app/app.component.html +++ b/nise-frontend/src/app/app.component.html @@ -11,7 +11,7 @@
  • ./suspicious-scores
  • ./stolen-replays
  • ./advanced-search
  • -
  • ./contribute <3
  • +
  • ./contribute <3
  • diff --git a/nise-frontend/src/app/banlist/banlist.component.css b/nise-frontend/src/app/banlist/banlist.component.css new file mode 100644 index 0000000..a54a5f1 --- /dev/null +++ b/nise-frontend/src/app/banlist/banlist.component.css @@ -0,0 +1,3 @@ +table td { + text-align: center; +} diff --git a/nise-frontend/src/app/banlist/banlist.component.html b/nise-frontend/src/app/banlist/banlist.component.html new file mode 100644 index 0000000..93ac4d0 --- /dev/null +++ b/nise-frontend/src/app/banlist/banlist.component.html @@ -0,0 +1,31 @@ +
    +
    +

    # follow-list

    + + + + + + + + + + + + + + + + + +
    UsernameIs banned?Last check
    + + + + {{ user.username }} + + {{ user.isBanned }}{{ calculateTimeAgo(user.lastUpdate) }} +
    + +
    +
    diff --git a/nise-frontend/src/app/banlist/banlist.component.ts b/nise-frontend/src/app/banlist/banlist.component.ts new file mode 100644 index 0000000..6b1c029 --- /dev/null +++ b/nise-frontend/src/app/banlist/banlist.component.ts @@ -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(`${environment.apiUrl}/banlist/statistics`).subscribe(response => { + this.banStatistics = response; + }); + } + + getFollows(): void { + this.httpClient.get(`${environment.apiUrl}/follows`).subscribe(response => { + this.follows = response; + }); + } + + protected readonly calculateTimeAgo = calculateTimeAgo; +} diff --git a/nise-frontend/src/app/format.ts b/nise-frontend/src/app/format.ts index 1c13b88..b88dee0 100644 --- a/nise-frontend/src/app/format.ts +++ b/nise-frontend/src/app/format.ts @@ -1,4 +1,5 @@ import {ReplayData} from "./replays"; +import {differenceInDays, differenceInHours} from "date-fns/fp"; export function formatDuration(seconds: number): string | null { if(!seconds) { @@ -50,3 +51,24 @@ export function calculateAccuracy(replayData: ReplayData): number { const accuracy = (300 * hit300 + 100 * hit100 + 50 * hit50) / (300 * totalHits); 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`; + } +} diff --git a/nise-frontend/src/app/view-user/view-user.component.html b/nise-frontend/src/app/view-user/view-user.component.html index fa44613..554105b 100644 --- a/nise-frontend/src/app/view-user/view-user.component.html +++ b/nise-frontend/src/app/view-user/view-user.component.html @@ -12,6 +12,11 @@

    {{ this.userInfo.user_details.username }} + + + + +

    @@ -51,7 +56,7 @@ wait a bit to force update | - 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'}}
    diff --git a/nise-frontend/src/app/view-user/view-user.component.ts b/nise-frontend/src/app/view-user/view-user.component.ts index 371bace..1c8e4d4 100644 --- a/nise-frontend/src/app/view-user/view-user.component.ts +++ b/nise-frontend/src/app/view-user/view-user.component.ts @@ -6,14 +6,14 @@ import {environment} from "../../environments/environment"; import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {ActivatedRoute, RouterLink} from "@angular/router"; import {UserDetails, UserQueueDetails} from "../userDetails"; -import {countryCodeToFlag, formatDuration} from "../format"; +import {calculateTimeAgo, countryCodeToFlag, formatDuration} from "../format"; import {Title} from "@angular/platform-browser"; import {RxStompService} from "../../corelib/stomp/stomp.service"; import {Message} from "@stomp/stompjs/esm6"; import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component"; -import {differenceInDays, differenceInHours} from "date-fns/fp"; import {FilterManagerService} from "../filter-manager.service"; import {UserService} from "../../corelib/service/user.service"; +import {FollowService} from "../../corelib/service/follow.service"; interface UserInfo { user_details: UserDetails; @@ -49,6 +49,7 @@ interface UserScoresFilter { }) export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { + isFollowing: boolean | null = null; isLoading = false; notFound = false; userId: string | null = null; @@ -64,7 +65,8 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { private activatedRoute: ActivatedRoute, private title: Title, private rxStompService: RxStompService, - public userService: UserService + public userService: UserService, + public followService: FollowService ) { } getUserInfo(): Observable { @@ -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) { this.getUserInfo().pipe( catchError(error => { @@ -121,6 +131,7 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { this.title.setTitle(`${this.userInfo.user_details.username}`); this.subscribeToUser(); } + this.checkIfUserIsFollowed(); } ); } @@ -129,27 +140,6 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { 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 { const body = { userId: this.userInfo?.user_details.user_id @@ -202,6 +192,6 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { protected readonly formatDuration = formatDuration; protected readonly countryCodeToFlag = countryCodeToFlag; - + protected readonly calculateTimeAgo = calculateTimeAgo; } diff --git a/nise-frontend/src/corelib/service/follow.service.ts b/nise-frontend/src/corelib/service/follow.service.ts new file mode 100644 index 0000000..7e0bc45 --- /dev/null +++ b/nise-frontend/src/corelib/service/follow.service.ts @@ -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 = new Map(); + + constructor( + private userService: UserService, + private httpClient: HttpClient + ) { } + + removeUserFollow(userId: number): Promise { + 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 { + 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 { + if(!this.userService.isUserLoggedIn()) { + return Promise.resolve(false); + } + + return new Promise((resolve, reject) => { + this.httpClient.get(`${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); + } + } + }); + }); + } + +}