Add gradle plugin and fix authenticator
This commit is contained in:
40 files changed
+266
-86
No files matched your search
+3
-2
@@ -2,5 +2,6 @@
|
|||||||
.kotlin/
|
.kotlin/
|
||||||
build/
|
build/
|
||||||
.gradle/
|
.gradle/
|
||||||
/core/.dev.vars
|
/worker-core/.dev.vars
|
||||||
/core/wrangler.toml
|
/worker-core/wrangler.toml
|
||||||
|
/worker-plugin-test/wrangler.toml
|
||||||
+6
-5
@@ -1,22 +1,23 @@
|
|||||||
plugins {
|
plugins {
|
||||||
|
kotlin("jvm") version "2.2.21" apply false
|
||||||
kotlin("multiplatform") version "2.2.21" apply false
|
kotlin("multiplatform") version "2.2.21" apply false
|
||||||
|
kotlin("plugin.serialization") version "2.2.21" apply false
|
||||||
id("maven-publish")
|
id("maven-publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val libVersion: String by extra
|
||||||
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "cn.rtast.kotlin-cfworker"
|
group = "kotlin-cloudflare-worker"
|
||||||
version = "1.0.2"
|
version = libVersion
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven("https://repo.maven.rtast.cn/releases")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
apply(plugin = "org.jetbrains.kotlin.multiplatform")
|
|
||||||
apply(plugin = "maven-publish")
|
apply(plugin = "maven-publish")
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
kotlin {
|
|
||||||
explicitApi()
|
|
||||||
js(IR) { nodejs { binaries.executable() } }
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
jsMain.dependencies {
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTest.dependencies {
|
|
||||||
implementation(kotlin("test"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val wranglerRunDir: Provider<Directory> = layout.buildDirectory.dir("wrangler-run")
|
|
||||||
tasks.register<Copy>("prepareWranglerRun") {
|
|
||||||
group = "wrangler"
|
|
||||||
dependsOn("compileDevelopmentExecutableKotlinJs")
|
|
||||||
val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/developmentExecutable/kotlin")
|
|
||||||
from(buildOutputDir)
|
|
||||||
into(wranglerRunDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
val wranglerDev by tasks.registering(Exec::class) {
|
|
||||||
group = "wrangler"
|
|
||||||
workingDir = layout.buildDirectory.dir("wrangler-run").get().asFile.apply { mkdirs() }
|
|
||||||
doFirst {
|
|
||||||
val sourceDir = project.layout.projectDirectory
|
|
||||||
mapOf(
|
|
||||||
sourceDir.file("wrangler.toml").asFile to File(workingDir, "wrangler.toml"),
|
|
||||||
sourceDir.file(".dev.vars").asFile to File(workingDir, ".dev.vars"),
|
|
||||||
).forEach { (s, d) -> s.copyTo(d, overwrite = true) }
|
|
||||||
}
|
|
||||||
commandLine(
|
|
||||||
if (System.getProperty("os.name").lowercase().contains("windows")) listOf(
|
|
||||||
"cmd", "/c", "wrangler dev --port 7071"
|
|
||||||
)
|
|
||||||
else listOf("sh", "-c", "wrangler dev --port 7071")
|
|
||||||
)
|
|
||||||
standardInput = System.`in`
|
|
||||||
isIgnoreExitValue = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val wranglerDeployDir: Provider<Directory> = layout.buildDirectory.dir("wrangler-deploy")
|
|
||||||
.apply { get().asFile.deleteRecursively() }
|
|
||||||
val prepareProductionDeploy by tasks.registering(Copy::class) {
|
|
||||||
group = "wrangler"
|
|
||||||
dependsOn("compileProductionExecutableKotlinJs")
|
|
||||||
val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/productionExecutable/kotlin")
|
|
||||||
from(buildOutputDir) { exclude("*.map") }
|
|
||||||
into(wranglerDeployDir)
|
|
||||||
from(layout.projectDirectory.file("wrangler.toml"))
|
|
||||||
into(wranglerDeployDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
val wranglerDeploy by tasks.registering(Exec::class) {
|
|
||||||
group = "wrangler"
|
|
||||||
dependsOn(prepareProductionDeploy)
|
|
||||||
workingDir = layout.buildDirectory.dir("wrangler-deploy").get().asFile.apply { mkdirs() }
|
|
||||||
commandLine(
|
|
||||||
if (System.getProperty("os.name").lowercase().contains("windows")) listOf("cmd", "/c", "wrangler deploy")
|
|
||||||
else listOf("sh", "-c", "wrangler deploy")
|
|
||||||
)
|
|
||||||
standardInput = System.`in`
|
|
||||||
}
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
libVersion=1.0.4
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
kotlin {
|
|
||||||
js(IR) {
|
|
||||||
nodejs { binaries.executable() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+22
-2
@@ -1,3 +1,23 @@
|
|||||||
include(":core")
|
@file:Suppress("UnstableApiUsage")
|
||||||
include(":serialization")
|
|
||||||
|
include(":worker-core")
|
||||||
|
include(":worker-serialization")
|
||||||
|
include(":worker-gradle-plugin")
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
maven("https://repo.maven.rtast.cn/releases")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
maven("https://repo.maven.rtast.cn/releases")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = "kotlin-cloudflare-worker"
|
rootProject.name = "kotlin-cloudflare-worker"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("kotlin-cloudflare-worker") version "1.0.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
explicitApi()
|
||||||
|
js(IR) { nodejs { binaries.executable() } }
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
jsMain.dependencies {
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTest.dependencies {
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrangler {
|
||||||
|
wranglerFile = file("wrangler.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
//val wranglerRunDir: Provider<Directory> = layout.buildDirectory.dir("wrangler-run")
|
||||||
|
//tasks.register<Copy>("prepareWranglerRun") {
|
||||||
|
// group = "wrangler"
|
||||||
|
// dependsOn("compileDevelopmentExecutableKotlinJs")
|
||||||
|
// val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/developmentExecutable/kotlin")
|
||||||
|
// from(buildOutputDir)
|
||||||
|
// into(wranglerRunDir)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//val wranglerDev by tasks.registering(Exec::class) {
|
||||||
|
// group = "wrangler"
|
||||||
|
// workingDir = layout.buildDirectory.dir("wrangler-run").get().asFile.apply { mkdirs() }
|
||||||
|
// doFirst {
|
||||||
|
// val sourceDir = project.layout.projectDirectory
|
||||||
|
// mapOf(
|
||||||
|
// sourceDir.file("wrangler.toml").asFile to File(workingDir, "wrangler.toml"),
|
||||||
|
// sourceDir.file(".dev.vars").asFile to File(workingDir, ".dev.vars"),
|
||||||
|
// ).forEach { (s, d) -> s.copyTo(d, overwrite = true) }
|
||||||
|
// }
|
||||||
|
// commandLine(
|
||||||
|
// if (System.getProperty("os.name").lowercase().contains("windows")) listOf(
|
||||||
|
// "cmd", "/c", "wrangler dev --port 7071"
|
||||||
|
// )
|
||||||
|
// else listOf("sh", "-c", "wrangler dev --port 7071")
|
||||||
|
// )
|
||||||
|
// standardInput = System.`in`
|
||||||
|
// isIgnoreExitValue = false
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//val wranglerDeployDir: Provider<Directory> = layout.buildDirectory.dir("wrangler-deploy")
|
||||||
|
// .apply { get().asFile.deleteRecursively() }
|
||||||
|
//val prepareProductionDeploy by tasks.registering(Copy::class) {
|
||||||
|
// group = "wrangler"
|
||||||
|
// dependsOn("compileProductionExecutableKotlinJs")
|
||||||
|
// val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/productionExecutable/kotlin")
|
||||||
|
// from(buildOutputDir) { exclude("*.map") }
|
||||||
|
// into(wranglerDeployDir)
|
||||||
|
// from(layout.projectDirectory.file("wrangler.toml"))
|
||||||
|
// into(wranglerDeployDir)
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//val wranglerDeploy by tasks.registering(Exec::class) {
|
||||||
|
// group = "wrangler"
|
||||||
|
// dependsOn(prepareProductionDeploy)
|
||||||
|
// workingDir = layout.buildDirectory.dir("wrangler-deploy").get().asFile.apply { mkdirs() }
|
||||||
|
// commandLine(
|
||||||
|
// if (System.getProperty("os.name").lowercase().contains("windows")) listOf("cmd", "/c", "wrangler deploy")
|
||||||
|
// else listOf("sh", "-c", "wrangler deploy")
|
||||||
|
// )
|
||||||
|
// standardInput = System.`in`
|
||||||
|
//}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -8,6 +8,6 @@ import org.w3c.fetch.Request
|
|||||||
* An interface for authenticating
|
* An interface for authenticating
|
||||||
* It's sealed
|
* It's sealed
|
||||||
*/
|
*/
|
||||||
public sealed interface Authenticator<in C : HttpCredential> {
|
public sealed interface Authenticator<out C : HttpCredential> {
|
||||||
public suspend fun authenticate(request: Request, credential: C?): AuthResult
|
public suspend fun authenticate(request: Request, credential: @UnsafeVariance C?): AuthResult
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
@@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
package cn.rtast.cfworker
|
package cn.rtast.cfworker
|
||||||
|
|
||||||
|
import cn.rtast.cfworker.auth.AuthResult
|
||||||
|
import cn.rtast.cfworker.auth.provider.BearerAuthenticator
|
||||||
|
import cn.rtast.cfworker.client.Url
|
||||||
import cn.rtast.cfworker.response.respondText
|
import cn.rtast.cfworker.response.respondText
|
||||||
import cn.rtast.cfworker.route.route
|
import cn.rtast.cfworker.route.route
|
||||||
import cn.rtast.cfworker.websocket.readText
|
import cn.rtast.cfworker.websocket.readText
|
||||||
@@ -50,6 +53,13 @@ public fun handleRequest(request: Request): Promise<Response> = GlobalScope.prom
|
|||||||
onClose {
|
onClose {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
route("/protected", setOf(HttpMethod.GET), BearerAuthenticator { request, credential ->
|
||||||
|
val name = request.Url.searchParams.get("name")
|
||||||
|
return@BearerAuthenticator if (name == "admin") AuthResult.OK else AuthResult.UNAUTHORIZED
|
||||||
|
}) {
|
||||||
|
respondText("Success")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return@promise server.handle(request)
|
return@promise server.handle(request)
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -28,7 +28,7 @@ import org.w3c.files.Blob
|
|||||||
public class WebsocketEventHandler(public val request: Request) {
|
public class WebsocketEventHandler(public val request: Request) {
|
||||||
private var onMessageBlock: (suspend (MessageEvent) -> Unit)? = null
|
private var onMessageBlock: (suspend (MessageEvent) -> Unit)? = null
|
||||||
private var onCloseBlock: (suspend (CloseEvent) -> Unit?)? = null
|
private var onCloseBlock: (suspend (CloseEvent) -> Unit?)? = null
|
||||||
private var onOpenBlock: (suspend (Event) -> Unit)? = null
|
private var onOpenBlock: (suspend () -> Unit)? = null
|
||||||
private var onErrorBlock: (suspend (Event) -> Unit)? = null
|
private var onErrorBlock: (suspend (Event) -> Unit)? = null
|
||||||
private var requireUpgradeHeaderBlock: (suspend (Request) -> Response)? =
|
private var requireUpgradeHeaderBlock: (suspend (Request) -> Response)? =
|
||||||
{ Response("Expected Upgrade: websocket", ResponseInit(426)) }
|
{ Response("Expected Upgrade: websocket", ResponseInit(426)) }
|
||||||
@@ -46,7 +46,7 @@ public class WebsocketEventHandler(public val request: Request) {
|
|||||||
onCloseBlock = block
|
onCloseBlock = block
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun onOpen(block: suspend (Event) -> Unit) {
|
public fun onOpen(block: suspend () -> Unit) {
|
||||||
onOpenBlock = block
|
onOpenBlock = block
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +66,9 @@ public class WebsocketEventHandler(public val request: Request) {
|
|||||||
val server = pair[1]
|
val server = pair[1]
|
||||||
server.accept()
|
server.accept()
|
||||||
clients.add(server)
|
clients.add(server)
|
||||||
|
GlobalScope.promise { onOpenBlock?.invoke() }
|
||||||
server.addEventListener("message", { e: MessageEvent -> GlobalScope.promise { onMessageBlock?.invoke(e) } })
|
server.addEventListener("message", { e: MessageEvent -> GlobalScope.promise { onMessageBlock?.invoke(e) } })
|
||||||
server.addEventListener("error", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } })
|
server.addEventListener("error", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } })
|
||||||
server.addEventListener("open", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } })
|
|
||||||
server.addEventListener("close", { e: CloseEvent ->
|
server.addEventListener("close", { e: CloseEvent ->
|
||||||
GlobalScope.promise {
|
GlobalScope.promise {
|
||||||
onCloseBlock?.invoke(e)
|
onCloseBlock?.invoke(e)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
id("java-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
website = "https://github.com/RTAkland/kotlin-cloudflare-worker"
|
||||||
|
vcsUrl = "https://github.com/RTAkland/kotlin-cloudflare-worker.git"
|
||||||
|
plugins {
|
||||||
|
create("kotlin-cloudflare-worker") {
|
||||||
|
id = "kotlin-cloudflare-worker"
|
||||||
|
displayName = "kotlin cloudflare worker"
|
||||||
|
description = "Kotlin cloudflare worker gradle plugin"
|
||||||
|
tags = listOf("tool", "tasks")
|
||||||
|
implementationClass = "cn.rtast.cfworker.plugin.KotlinCloudflareWorkerPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21")
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/1/17
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.cfworker.plugin
|
||||||
|
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.tasks.Copy
|
||||||
|
import org.gradle.api.tasks.Exec
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class KotlinCloudflareWorkerPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
val extension = target.extensions.create("wrangler", WorkerExtension::class.java)
|
||||||
|
val layout = target.layout
|
||||||
|
val wranglerRunDir = layout.buildDirectory.dir("wrangler-run")
|
||||||
|
target.tasks.register("prepareWranglerRun", Copy::class.java) {
|
||||||
|
it.group = "wrangler"
|
||||||
|
it.dependsOn("compileDevelopmentExecutableKotlinJs")
|
||||||
|
val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/developmentExecutable/kotlin")
|
||||||
|
it.from(buildOutputDir)
|
||||||
|
it.into(wranglerRunDir)
|
||||||
|
}
|
||||||
|
target.tasks.register("wranglerDev", Exec::class.java) { exec ->
|
||||||
|
exec.group = "wrangler"
|
||||||
|
exec.workingDir = layout.buildDirectory.dir("wrangler-run").get().asFile.apply { mkdirs() }
|
||||||
|
exec.doFirst {
|
||||||
|
val sourceDir = target.layout.projectDirectory
|
||||||
|
mutableMapOf(extension.wranglerFile.get() to File(exec.workingDir, "wrangler.toml")).apply {
|
||||||
|
val envFile = sourceDir.file(".dev.vars").asFile
|
||||||
|
if (envFile.exists()) this[envFile] = File(exec.workingDir, ".dev.vars")
|
||||||
|
}.forEach { (s, d) -> s.copyTo(d, overwrite = true) }
|
||||||
|
}
|
||||||
|
exec.commandLine(
|
||||||
|
if (System.getProperty("os.name").lowercase().contains("windows")) listOf(
|
||||||
|
"cmd", "/c", "wrangler dev --port ${extension.port.get()}"
|
||||||
|
)
|
||||||
|
else listOf("sh", "-c", "wrangler dev --port ${extension.port.get()}")
|
||||||
|
)
|
||||||
|
exec.standardInput = System.`in`
|
||||||
|
exec.isIgnoreExitValue = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val wranglerDeployDir = layout.buildDirectory.dir("wrangler-deploy")
|
||||||
|
.apply { get().asFile.deleteRecursively() }
|
||||||
|
target.tasks.register("prepareProductionDeploy", Copy::class.java) { copy ->
|
||||||
|
copy.group = "wrangler"
|
||||||
|
copy.dependsOn("compileProductionExecutableKotlinJs")
|
||||||
|
val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/productionExecutable/kotlin")
|
||||||
|
copy.from(buildOutputDir) { it.exclude("*.map") }
|
||||||
|
copy.into(wranglerDeployDir)
|
||||||
|
copy.from(layout.projectDirectory.file("wrangler.toml"))
|
||||||
|
copy.into(wranglerDeployDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.tasks.register("wranglerDeploy", Exec::class.java) {
|
||||||
|
it.group = "wrangler"
|
||||||
|
it.dependsOn("prepareProductionDeploy")
|
||||||
|
it.workingDir = layout.buildDirectory.dir("wrangler-deploy").get().asFile.apply { mkdirs() }
|
||||||
|
it.commandLine(
|
||||||
|
if (System.getProperty("os.name").lowercase().contains("windows")) listOf(
|
||||||
|
"cmd",
|
||||||
|
"/c",
|
||||||
|
"wrangler deploy"
|
||||||
|
)
|
||||||
|
else listOf("sh", "-c", "wrangler deploy")
|
||||||
|
)
|
||||||
|
it.standardInput = System.`in`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/1/17
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.cfworker.plugin
|
||||||
|
|
||||||
|
internal enum class RunMode {
|
||||||
|
Development, Production
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2026 RTAkland
|
||||||
|
* Author: RTAkland
|
||||||
|
* Date: 2026/1/17
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package cn.rtast.cfworker.plugin
|
||||||
|
|
||||||
|
import org.gradle.api.provider.Property
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class WorkerExtension {
|
||||||
|
@get:Input
|
||||||
|
abstract val port: Property<Int>
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
abstract val wranglerFile: Property<File>
|
||||||
|
|
||||||
|
init {
|
||||||
|
wranglerFile.convention(File("wrangler.toml"))
|
||||||
|
port.convention(7071)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
kotlin("plugin.serialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
js(IR) {
|
||||||
|
nodejs { binaries.executable() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user