package ani.dantotsu.connections.comments import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.snackString import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.Requests import eu.kanade.tachiyomi.network.NetworkHelper 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 kotlinx.serialization.json.Json import okhttp3.FormBody import okhttp3.OkHttpClient import okio.IOException import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object CommentsAPI { val address: String = "https://1224665.xyz:443" var authToken: String? = null var userId: String? = null var isBanned: Boolean = false var isAdmin: Boolean = false var isMod: Boolean = false var totalVotes: Int = 0 suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? { var url = "$address/comments/$id/$page" val request = requestBuilder() tag?.let { url += "?tag=$it" } sort?.let { url += if (tag != null) "&sort=$it" else "?sort=$it" } val json = try { request.get(url) } catch (e: IOException) { snackString("Failed to fetch comments") return null } if (!json.text.startsWith("{")) return null val res = json.code == 200 if (!res && json.code != 404) { errorReason(json.code, json.text) } val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { return null } return parsed } suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? { val url = "$address/comments/parent/$id/$page" val request = requestBuilder() val json = try { request.get(url) } catch (e: IOException) { snackString("Failed to fetch comments") return null } if (!json.text.startsWith("{")) return null val res = json.code == 200 if (!res && json.code != 404) { errorReason(json.code, json.text) } val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { return null } return parsed } suspend fun getSingleComment(id: Int): Comment? { val url = "$address/comments/$id" val request = requestBuilder() val json = try { request.get(url) } catch (e: IOException) { snackString("Failed to fetch comment") return null } if (!json.text.startsWith("{")) return null val res = json.code == 200 if (!res && json.code != 404) { errorReason(json.code, json.text) } val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { return null } return parsed } suspend fun vote(commentId: Int, voteType: Int): Boolean { val url = "$address/comments/vote/$commentId/$voteType" val request = requestBuilder() val json = try { request.post(url) } catch (e: IOException) { snackString("Failed to vote") return false } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) } return res } suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? { val url = "$address/comments" val body = FormBody.Builder() .add("user_id", userId ?: return null) .add("media_id", mediaId.toString()) .add("content", content) if (tag != null) { body.add("tag", tag.toString()) } parentCommentId?.let { body.add("parent_comment_id", it.toString()) } val request = requestBuilder() val json = try { request.post(url, requestBody = body.build()) } catch (e: IOException) { snackString("Failed to comment") return null } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) return null } val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { snackString("Failed to parse comment") return null } return Comment( parsed.id, parsed.userId, parsed.mediaId, parsed.parentCommentId, parsed.content, parsed.timestamp, parsed.deleted, parsed.tag, 0, 0, null, Anilist.username ?: "", Anilist.avatar, totalVotes = totalVotes ) } suspend fun deleteComment(commentId: Int): Boolean { val url = "$address/comments/$commentId" val request = requestBuilder() val json = try { request.delete(url) } catch (e: IOException) { snackString("Failed to delete comment") return false } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) } return res } suspend fun editComment(commentId: Int, content: String): Boolean { val url = "$address/comments/$commentId" val body = FormBody.Builder() .add("content", content) .build() val request = requestBuilder() val json = try { request.put(url, requestBody = body) } catch (e: IOException) { snackString("Failed to edit comment") return false } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) } return res } suspend fun banUser(userId: String): Boolean { val url = "$address/ban/$userId" val request = requestBuilder() val json = try { request.post(url) } catch (e: IOException) { snackString("Failed to ban user") return false } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) } return res } suspend fun reportComment( commentId: Int, username: String, mediaTitle: String, reportedId: String ): Boolean { val url = "$address/report/$commentId" val body = FormBody.Builder() .add("username", username) .add("mediaName", mediaTitle) .add("reporter", Anilist.username ?: "unknown") .add("reportedId", reportedId) .build() val request = requestBuilder() val json = try { request.post(url, requestBody = body) } catch (e: IOException) { snackString("Failed to report comment") return false } val res = json.code == 200 if (!res) { errorReason(json.code, json.text) } return res } suspend fun getNotifications(client: OkHttpClient): NotificationResponse? { val url = "$address/notification/reply" val request = requestBuilder(client) val json = try { request.get(url) } catch (e: IOException) { return null } if (!json.text.startsWith("{")) return null val res = json.code == 200 if (!res) { return null } val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { return null } return parsed } private suspend fun getUserDetails(client: OkHttpClient? = null): User? { val url = "$address/user" val request = if (client != null) requestBuilder(client) else requestBuilder() val json = try { request.get(url) } catch (e: IOException) { return null } if (json.code == 200) { val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { e.printStackTrace() return null } isBanned = parsed.user.isBanned ?: false isAdmin = parsed.user.isAdmin ?: false isMod = parsed.user.isMod ?: false totalVotes = parsed.user.totalVotes return parsed.user } return null } suspend fun fetchAuthToken(client: OkHttpClient? = null) { if (authToken != null) return val MAX_RETRIES = 5 val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days val tokenExpiry = PrefManager.getVal(PrefName.CommentTokenExpiry) if (tokenExpiry < System.currentTimeMillis() + tokenLifetime) { val commentResponse = PrefManager.getNullableVal(PrefName.CommentAuthResponse, null) if (commentResponse != null) { authToken = commentResponse.authToken userId = commentResponse.user.id isBanned = commentResponse.user.isBanned ?: false isAdmin = commentResponse.user.isAdmin ?: false isMod = commentResponse.user.isMod ?: false totalVotes = commentResponse.user.totalVotes if (getUserDetails(client) != null) return } } val url = "$address/authenticate" val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return repeat(MAX_RETRIES) { try { val json = authRequest(token, url, client) if (json.code == 200) { if (!json.text.startsWith("{")) throw IOException("Invalid response") val parsed = try { Json.decodeFromString(json.text) } catch (e: Exception) { snackString("Failed to login to comments API: ${e.printStackTrace()}") return } PrefManager.setVal(PrefName.CommentAuthResponse, parsed) PrefManager.setVal( PrefName.CommentTokenExpiry, System.currentTimeMillis() + tokenLifetime ) authToken = parsed.authToken userId = parsed.user.id isBanned = parsed.user.isBanned ?: false isAdmin = parsed.user.isAdmin ?: false isMod = parsed.user.isMod ?: false totalVotes = parsed.user.totalVotes return } else if (json.code != 429) { errorReason(json.code, json.text) return } } catch (e: IOException) { snackString("Failed to login to comments API") return } kotlinx.coroutines.delay(60000) } snackString("Failed to login after multiple attempts") } private suspend fun authRequest( token: String, url: String, client: OkHttpClient? = null ): NiceResponse { val body: FormBody = FormBody.Builder() .add("token", token) .build() val request = if (client != null) requestBuilder(client) else requestBuilder() return request.post(url, requestBody = body) } private fun headerBuilder(): Map { val map = mutableMapOf( "appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n" ) if (authToken != null) { map["Authorization"] = authToken!! } return map } private fun requestBuilder(client: OkHttpClient = Injekt.get().client): Requests { return Requests( client, headerBuilder() ) } private fun errorReason(code: Int, reason: String? = null) { val error = when (code) { 429 -> "Rate limited. :(" else -> "Failed to connect" } val parsed = try { Json.decodeFromString(reason!!) } catch (e: Exception) { null } val message = parsed?.message ?: reason ?: error snackString("Error $code: $message") } } @Serializable data class ErrorResponse( @SerialName("message") val message: String ) @Serializable data class NotificationResponse( @SerialName("notifications") val notifications: List ) @Serializable data class Notification( @SerialName("username") val username: String, @SerialName("media_id") val mediaId: Int, @SerialName("comment_id") val commentId: Int, @SerialName("type") val type: Int? = null, @SerialName("content") val content: String? = null, @SerialName("notification_id") val notificationId: Int ) @Serializable data class AuthResponse( @SerialName("authToken") val authToken: String, @SerialName("user") val user: User ) : java.io.Serializable { companion object { private const val serialVersionUID: Long = 1 } } @Serializable data class UserResponse( @SerialName("user") val user: User ) @Serializable data class User( @SerialName("user_id") val id: String, @SerialName("username") val username: String, @SerialName("profile_picture_url") val profilePictureUrl: String? = null, @SerialName("is_banned") @Serializable(with = NumericBooleanSerializer::class) val isBanned: Boolean? = null, @SerialName("is_mod") @Serializable(with = NumericBooleanSerializer::class) val isAdmin: Boolean? = null, @SerialName("is_admin") @Serializable(with = NumericBooleanSerializer::class) val isMod: Boolean? = null, @SerialName("total_votes") val totalVotes: Int, @SerialName("warnings") val warnings: Int ) : java.io.Serializable { companion object { private const val serialVersionUID: Long = 1 } } @Serializable data class CommentResponse( @SerialName("comments") val comments: List, @SerialName("totalPages") val totalPages: Int ) @Serializable data class Comment( @SerialName("comment_id") val commentId: Int, @SerialName("user_id") val userId: String, @SerialName("media_id") val mediaId: Int, @SerialName("parent_comment_id") val parentCommentId: Int?, @SerialName("content") var content: String, @SerialName("timestamp") var timestamp: String, @SerialName("deleted") @Serializable(with = NumericBooleanSerializer::class) val deleted: Boolean?, @SerialName("tag") val tag: Int?, @SerialName("upvotes") var upvotes: Int, @SerialName("downvotes") var downvotes: Int, @SerialName("user_vote_type") var userVoteType: Int?, @SerialName("username") val username: String, @SerialName("profile_picture_url") val profilePictureUrl: String?, @SerialName("is_mod") @Serializable(with = NumericBooleanSerializer::class) val isMod: Boolean? = null, @SerialName("is_admin") @Serializable(with = NumericBooleanSerializer::class) val isAdmin: Boolean? = null, @SerialName("reply_count") val replyCount: Int? = null, @SerialName("total_votes") val totalVotes: Int ) @Serializable data class ReturnedComment( @SerialName("id") var id: Int, @SerialName("comment_id") var commentId: Int?, @SerialName("user_id") val userId: String, @SerialName("media_id") val mediaId: Int, @SerialName("parent_comment_id") val parentCommentId: Int? = null, @SerialName("content") val content: String, @SerialName("timestamp") val timestamp: String, @SerialName("deleted") @Serializable(with = NumericBooleanSerializer::class) val deleted: Boolean?, @SerialName("tag") val tag: Int? = null, ) object NumericBooleanSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT) override fun serialize(encoder: Encoder, value: Boolean) { encoder.encodeInt(if (value) 1 else 0) } override fun deserialize(decoder: Decoder): Boolean { return decoder.decodeInt() != 0 } }