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
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<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.
@ -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,

View File

@ -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 <code>public.user_follows</code>.
*/
val USER_FOLLOWS: UserFollows get() = UserFollows.USER_FOLLOWS
/**
* The table <code>public.user_scores</code>.
*/
@ -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

View File

@ -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<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 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 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_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)
@ -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 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.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<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>?, 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)
// -------------------------------------------------------------------------
// 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)}.
*/
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,
* 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.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<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?
set(value): Unit = set(0, value)
@ -96,6 +96,16 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
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<UsersRecord>(
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 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 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(): 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 field2(): Field<String?> = Users.USERS.USERNAME
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 field17(): Field<Boolean?> = Users.USERS.IS_ADMIN
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 component2(): String? = username
override fun component3(): LocalDateTime? = joinDate
@ -144,6 +156,8 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
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<UsersRecord>(
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<UsersRecord>(
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<UsersRecord>(
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<UsersRecord>(
this.sysLastUpdate = sysLastUpdate
this.isAdmin = isAdmin
this.countMiss = countMiss
this.isBanned = isBanned
this.approxBanDate = approxBanDate
resetChangedOnNotNull()
}
}

View File

@ -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 <code>public.user_follows</code>.
*/
val USER_FOLLOWS: UserFollows = UserFollows.USER_FOLLOWS
/**
* The table <code>public.user_scores</code>.
*/

View File

@ -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<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!!,
distanceToCenter = it.distanceCenter!!,
distanceToEdge = it.distanceEdge!!,
time = it.time!!,
time = it.time!!.toInt(),
type = mapLegacyJudgement(it.type!!)
)
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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}

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 {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/'},
];

View File

@ -7,3 +7,11 @@
overflow: hidden;
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]="['/stolen']">./stolen-replays</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>
<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...">

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 {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`;
}
}

View File

@ -12,6 +12,11 @@
<h1>
<img [src]="'https://a.ppy.sh/' + this.userInfo.user_details.user_id" class="avatar">
{{ 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>
<div class="mb-2 mt-2 btn-group">
@ -51,7 +56,7 @@
<span class="btn-warning">wait a bit to force update</span>
</ng-container>
<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-template #updateProgress>
<div class="progress">

View File

@ -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<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) {
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;
}

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);
}
}
});
});
}
}