feat: Реализация базового CalDav без to-do через REST API обёртку
feat: Информативное логгирование и ответы сервера
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package com.nano
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
|
||||
.start(wait = true)
|
||||
}
|
||||
|
||||
fun Application.module() {
|
||||
configureSecurity()
|
||||
configureMonitoring()
|
||||
configureSerialization()
|
||||
configureRouting()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.nano
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureMonitoring() {
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
filter { call -> call.request.path().startsWith("/") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package com.nano
|
||||
|
||||
import com.nano.Service.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.slf4j.event.*
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Serializable
|
||||
data class CreateEventRequest(
|
||||
val summary: String,
|
||||
val description: String? = null,
|
||||
val startDateTime: String, // ISO format: "2025-07-20T10:00:00"
|
||||
val endDateTime: String,
|
||||
val location: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EventResponse(
|
||||
val uid: String,
|
||||
val summary: String,
|
||||
val description: String? = null,
|
||||
val startDateTime: String,
|
||||
val endDateTime: String,
|
||||
val location: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiResponse<T>(
|
||||
val success: Boolean,
|
||||
val data: T? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
fun Application.configureRouting() {
|
||||
routing {
|
||||
authenticate("calendar-auth") {
|
||||
route("/calendar") {
|
||||
route("/events") {
|
||||
|
||||
// Получение всех событий
|
||||
get {
|
||||
try {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
val username = principal.name
|
||||
|
||||
// Получаем пароль из заголовка Authorization для создания DAV подключения
|
||||
val authHeader = call.request.headers["Authorization"]
|
||||
val password = authHeader?.let { header ->
|
||||
val encoded = header.substringAfter("Basic ")
|
||||
val decoded = java.util.Base64.getDecoder().decode(encoded).toString(Charsets.UTF_8)
|
||||
decoded.substringAfter(":")
|
||||
} ?: ""
|
||||
|
||||
// Получаем календарь ID из заголовка CAL_ID
|
||||
val calendarId = call.request.headers["CAL_ID"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "Заголовок CAL_ID обязателен")
|
||||
)
|
||||
|
||||
val dav = Dav(username, password, calendarId)
|
||||
val eventManager = dav.getEventManager()
|
||||
|
||||
eventManager.getAllEvents().fold(
|
||||
onSuccess = { events ->
|
||||
val eventResponses = events.map { event ->
|
||||
EventResponse(
|
||||
uid = event.uid,
|
||||
summary = event.summary,
|
||||
description = event.description,
|
||||
startDateTime = event.startDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
endDateTime = event.endDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
location = event.location
|
||||
)
|
||||
}
|
||||
call.respond(ApiResponse(success = true, data = eventResponses))
|
||||
},
|
||||
onFailure = { error ->
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = error.message)
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = "Неожиданная ошибка: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Создание нового события
|
||||
post {
|
||||
try {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
val username = principal.name
|
||||
|
||||
val authHeader = call.request.headers["Authorization"]
|
||||
val password = authHeader?.let { header ->
|
||||
val encoded = header.substringAfter("Basic ")
|
||||
val decoded = java.util.Base64.getDecoder().decode(encoded).toString(Charsets.UTF_8)
|
||||
decoded.substringAfter(":")
|
||||
} ?: ""
|
||||
|
||||
// Получаем календарь ID из заголовка CAL_ID
|
||||
val calendarId = call.request.headers["CAL_ID"] ?: return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "Заголовок CAL_ID обязателен")
|
||||
)
|
||||
|
||||
val request = call.receive<CreateEventRequest>()
|
||||
|
||||
val event = CalendarEvent(
|
||||
summary = request.summary,
|
||||
description = request.description,
|
||||
startDateTime = LocalDateTime.parse(request.startDateTime),
|
||||
endDateTime = LocalDateTime.parse(request.endDateTime),
|
||||
location = request.location
|
||||
)
|
||||
|
||||
val dav = Dav(username, password, calendarId)
|
||||
val eventManager = dav.getEventManager()
|
||||
|
||||
eventManager.addEvent(event).fold(
|
||||
onSuccess = { createdEvent ->
|
||||
val response = EventResponse(
|
||||
uid = createdEvent.uid,
|
||||
summary = createdEvent.summary,
|
||||
description = createdEvent.description,
|
||||
startDateTime = createdEvent.startDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
endDateTime = createdEvent.endDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
location = createdEvent.location
|
||||
)
|
||||
call.respond(HttpStatusCode.Created, ApiResponse(success = true, data = response))
|
||||
},
|
||||
onFailure = { error ->
|
||||
val statusCode = when {
|
||||
error.message?.contains("авторизации") == true -> HttpStatusCode.Unauthorized
|
||||
error.message?.contains("запрещен") == true -> HttpStatusCode.Forbidden
|
||||
error.message?.contains("не найден") == true -> HttpStatusCode.NotFound
|
||||
error.message?.contains("Конфликт") == true -> HttpStatusCode.Conflict
|
||||
else -> HttpStatusCode.BadRequest
|
||||
}
|
||||
call.respond(statusCode, ApiResponse<String>(success = false, message = error.message))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = "Ошибка создания события: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Получение события по UID
|
||||
get("/{uid}") {
|
||||
try {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
val username = principal.name
|
||||
val uid = call.parameters["uid"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "UID события не указан")
|
||||
)
|
||||
|
||||
val authHeader = call.request.headers["Authorization"]
|
||||
val password = authHeader?.let { header ->
|
||||
val encoded = header.substringAfter("Basic ")
|
||||
val decoded = java.util.Base64.getDecoder().decode(encoded).toString(Charsets.UTF_8)
|
||||
decoded.substringAfter(":")
|
||||
} ?: ""
|
||||
|
||||
// Получаем календарь ID из заголовка CAL_ID
|
||||
val calendarId = call.request.headers["CAL_ID"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "Заголовок CAL_ID обязателен")
|
||||
)
|
||||
|
||||
val dav = Dav(username, password, calendarId)
|
||||
val eventManager = dav.getEventManager()
|
||||
|
||||
eventManager.getEvent(uid).fold(
|
||||
onSuccess = { event ->
|
||||
if (event != null) {
|
||||
val response = EventResponse(
|
||||
uid = event.uid,
|
||||
summary = event.summary,
|
||||
description = event.description,
|
||||
startDateTime = event.startDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
endDateTime = event.endDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
location = event.location
|
||||
)
|
||||
call.respond(ApiResponse(success = true, data = response))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
ApiResponse<String>(success = false, message = "Событие не найдено")
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
val statusCode = when {
|
||||
error.message?.contains("авторизации") == true -> HttpStatusCode.Unauthorized
|
||||
error.message?.contains("запрещен") == true -> HttpStatusCode.Forbidden
|
||||
error.message?.contains("не найдено") == true -> HttpStatusCode.NotFound
|
||||
else -> HttpStatusCode.InternalServerError
|
||||
}
|
||||
call.respond(statusCode, ApiResponse<String>(success = false, message = error.message))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = "Ошибка получения события: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление события
|
||||
put("/{uid}") {
|
||||
try {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
val username = principal.name
|
||||
val uid = call.parameters["uid"] ?: return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "UID события не указан")
|
||||
)
|
||||
|
||||
val authHeader = call.request.headers["Authorization"]
|
||||
val password = authHeader?.let { header ->
|
||||
val encoded = header.substringAfter("Basic ")
|
||||
val decoded = java.util.Base64.getDecoder().decode(encoded).toString(Charsets.UTF_8)
|
||||
decoded.substringAfter(":")
|
||||
} ?: ""
|
||||
|
||||
// Получаем календарь ID из заголовка CAL_ID
|
||||
val calendarId = call.request.headers["CAL_ID"] ?: return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "Заголовок CAL_ID обязателен")
|
||||
)
|
||||
|
||||
val request = call.receive<CreateEventRequest>()
|
||||
|
||||
val event = CalendarEvent(
|
||||
uid = uid,
|
||||
summary = request.summary,
|
||||
description = request.description,
|
||||
startDateTime = LocalDateTime.parse(request.startDateTime),
|
||||
endDateTime = LocalDateTime.parse(request.endDateTime),
|
||||
location = request.location
|
||||
)
|
||||
|
||||
val dav = Dav(username, password, calendarId)
|
||||
val eventManager = dav.getEventManager()
|
||||
|
||||
eventManager.updateEvent(event).fold(
|
||||
onSuccess = { updatedEvent ->
|
||||
val response = EventResponse(
|
||||
uid = updatedEvent.uid,
|
||||
summary = updatedEvent.summary,
|
||||
description = updatedEvent.description,
|
||||
startDateTime = updatedEvent.startDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
endDateTime = updatedEvent.endDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
||||
location = updatedEvent.location
|
||||
)
|
||||
call.respond(ApiResponse(success = true, data = response))
|
||||
},
|
||||
onFailure = { error ->
|
||||
val statusCode = when {
|
||||
error.message?.contains("авторизации") == true -> HttpStatusCode.Unauthorized
|
||||
error.message?.contains("запрещен") == true -> HttpStatusCode.Forbidden
|
||||
error.message?.contains("не найдено") == true -> HttpStatusCode.NotFound
|
||||
error.message?.contains("Конфликт версий") == true -> HttpStatusCode.PreconditionFailed
|
||||
else -> HttpStatusCode.BadRequest
|
||||
}
|
||||
call.respond(statusCode, ApiResponse<String>(success = false, message = error.message))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = "Ошибка обновления события: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление события
|
||||
delete("/{uid}") {
|
||||
try {
|
||||
val principal = call.principal<UserIdPrincipal>()!!
|
||||
val username = principal.name
|
||||
val uid = call.parameters["uid"] ?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "UID события не указан")
|
||||
)
|
||||
|
||||
val authHeader = call.request.headers["Authorization"]
|
||||
val password = authHeader?.let { header ->
|
||||
val encoded = header.substringAfter("Basic ")
|
||||
val decoded = java.util.Base64.getDecoder().decode(encoded).toString(Charsets.UTF_8)
|
||||
decoded.substringAfter(":")
|
||||
} ?: ""
|
||||
|
||||
// Получаем календарь ID из заголовка CAL_ID
|
||||
val calendarId = call.request.headers["CAL_ID"] ?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<String>(success = false, message = "Заголовок CAL_ID обязателен")
|
||||
)
|
||||
|
||||
val dav = Dav(username, password, calendarId)
|
||||
val eventManager = dav.getEventManager()
|
||||
|
||||
eventManager.deleteEvent(uid).fold(
|
||||
onSuccess = {
|
||||
call.respond(ApiResponse<String>(success = true, message = "Событие успешно удалено"))
|
||||
},
|
||||
onFailure = { error ->
|
||||
val statusCode = when {
|
||||
error.message?.contains("авторизации") == true -> HttpStatusCode.Unauthorized
|
||||
error.message?.contains("запрещен") == true -> HttpStatusCode.Forbidden
|
||||
error.message?.contains("не найдено") == true -> HttpStatusCode.NotFound
|
||||
else -> HttpStatusCode.BadRequest
|
||||
}
|
||||
call.respond(statusCode, ApiResponse<String>(success = false, message = error.message))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<String>(success = false, message = "Ошибка удаления события: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.nano
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
data class CalendarCredentials(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
fun Application.configureSecurity() {
|
||||
authentication {
|
||||
basic(name = "calendar-auth") {
|
||||
realm = "Calendar Access"
|
||||
validate { credentials ->
|
||||
if (credentials.name.isNotEmpty() && credentials.password.isNotEmpty()) {
|
||||
UserIdPrincipal(credentials.name)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.nano
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.slf4j.event.*
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
routing {
|
||||
get("/json/kotlinx-serialization") {
|
||||
call.respond(mapOf("hello" to "world"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package com.nano.Service
|
||||
|
||||
import at.bitfire.dav4jvm.BasicDigestAuthHandler
|
||||
import at.bitfire.dav4jvm.DavCalendar
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.property.CalendarData
|
||||
import io.ktor.http.Url
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.slf4j.LoggerFactory.getLogger
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.math.log
|
||||
|
||||
enum class DAVHOST(val url: String) {
|
||||
BAIKAL("https://baikal.illegalfiles.icu/dav.php"),
|
||||
}
|
||||
|
||||
data class CalendarEvent(
|
||||
val uid: String = UUID.randomUUID().toString(),
|
||||
val summary: String,
|
||||
val description: String? = null,
|
||||
val startDateTime: LocalDateTime,
|
||||
val endDateTime: LocalDateTime,
|
||||
val location: String? = null
|
||||
)
|
||||
|
||||
class Dav(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val calendarName: String,
|
||||
server: DAVHOST = DAVHOST.BAIKAL,
|
||||
) {
|
||||
val baseUrl = Url(server.url)
|
||||
|
||||
val authHandler = BasicDigestAuthHandler(
|
||||
domain = null, // Убираем ограничение по домену
|
||||
username = username,
|
||||
password = password,
|
||||
)
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.followRedirects(false)
|
||||
.authenticator(authHandler)
|
||||
.addNetworkInterceptor(authHandler)
|
||||
.build()
|
||||
|
||||
val calDav = DavCalendar(
|
||||
client,
|
||||
location = baseUrl.toString().toHttpUrlOrNull()?.newBuilder()
|
||||
?.addPathSegment("calendars")
|
||||
?.build() ?: throw IllegalArgumentException("Invalid URL: $baseUrl"),
|
||||
)
|
||||
|
||||
/**
|
||||
* Получает менеджер для работы с событиями календаря
|
||||
*/
|
||||
fun getEventManager(): CalendarEventManager {
|
||||
return CalendarEventManager(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Менеджер для работы с событиями календаря.
|
||||
* Реализует основные CRUD операции с использованием библиотеки dav4jvm
|
||||
*/
|
||||
class CalendarEventManager(private val dav: Dav) {
|
||||
|
||||
private val logger = getLogger("Dav")
|
||||
|
||||
private fun createICalendarEvent(event: CalendarEvent): String {
|
||||
val dtFormatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")
|
||||
val now = LocalDateTime.now().format(dtFormatter)
|
||||
|
||||
return """BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//WebDAV Service//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
UID:${event.uid}
|
||||
DTSTAMP:${now}Z
|
||||
DTSTART:${event.startDateTime.format(dtFormatter)}Z
|
||||
DTEND:${event.endDateTime.format(dtFormatter)}Z
|
||||
SUMMARY:${event.summary}
|
||||
${if (!event.description.isNullOrEmpty()) "DESCRIPTION:${event.description}" else ""}
|
||||
${if (!event.location.isNullOrEmpty()) "LOCATION:${event.location}" else ""}
|
||||
END:VEVENT
|
||||
END:VCALENDAR""".trimIndent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет новое событие в календарь
|
||||
*/
|
||||
fun addEvent(event: CalendarEvent): Result<CalendarEvent> {
|
||||
return try {
|
||||
val iCalData = createICalendarEvent(event)
|
||||
val eventUrl = dav.calDav.location.newBuilder()
|
||||
.addPathSegment(dav.username)
|
||||
.addPathSegment(dav.calendarName)
|
||||
.addPathSegment("${event.uid}.ics")
|
||||
.build()
|
||||
|
||||
val davResource = DavResource(dav.client, eventUrl)
|
||||
val requestBody = iCalData.toRequestBody("text/calendar; charset=utf-8".toMediaType())
|
||||
|
||||
var responseCode: Int? = null
|
||||
var responseMessage: String? = null
|
||||
|
||||
davResource.put(requestBody) { response ->
|
||||
responseCode = response.code
|
||||
responseMessage = response.message
|
||||
}
|
||||
|
||||
when {
|
||||
responseCode == null -> Result.failure(Exception("Не удалось получить ответ от сервера"))
|
||||
responseCode in 200..299 -> Result.success(event)
|
||||
responseCode == 401 -> Result.failure(Exception("Ошибка авторизации: проверьте логин и пароль"))
|
||||
responseCode == 403 -> Result.failure(Exception("Доступ запрещен: недостаточно прав для записи в календарь '${dav.calendarName}'"))
|
||||
responseCode == 404 -> Result.failure(Exception("Календарь '${dav.calendarName}' не найден"))
|
||||
responseCode == 409 -> Result.failure(Exception("Конфликт: событие с UID '${event.uid}' уже существует"))
|
||||
responseCode in 500..599 -> Result.failure(Exception("Ошибка сервера ($responseCode): $responseMessage"))
|
||||
else -> Result.failure(Exception("HTTP $responseCode: $responseMessage"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("Ошибка при добавлении события: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает событие по UID
|
||||
*/
|
||||
fun getEvent(uid: String): Result<CalendarEvent?> {
|
||||
return try {
|
||||
val eventUrl = dav.calDav.location.newBuilder()
|
||||
.addPathSegment(dav.username)
|
||||
.addPathSegment(dav.calendarName)
|
||||
.addPathSegment("$uid.ics")
|
||||
.build()
|
||||
|
||||
val davResource = DavResource(dav.client, eventUrl)
|
||||
var eventData: String? = null
|
||||
var responseCode: Int? = null
|
||||
var responseMessage: String? = null
|
||||
|
||||
davResource.get("text/calendar", null) { response ->
|
||||
responseCode = response.code
|
||||
responseMessage = response.message
|
||||
if (response.isSuccessful) {
|
||||
eventData = response.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
responseCode == null -> Result.failure(Exception("Не удалось получить ответ от сервера"))
|
||||
responseCode == 200 -> {
|
||||
val event = eventData?.let { parseICalendarEvent(it, uid) }
|
||||
Result.success(event)
|
||||
}
|
||||
responseCode == 401 -> Result.failure(Exception("Ошибка авторизации: проверьте логин и пароль"))
|
||||
responseCode == 403 -> Result.failure(Exception("Доступ запрещен: недостаточно прав для чтения календаря '${dav.calendarName}'"))
|
||||
responseCode == 404 -> Result.failure(Exception("Событие с UID '$uid' не найдено в календаре '${dav.calendarName}'"))
|
||||
responseCode in 500..599 -> Result.failure(Exception("Ошибка сервера ($responseCode): $responseMessage"))
|
||||
else -> Result.failure(Exception("HTTP $responseCode: $responseMessage"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("Ошибка при получении события: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет существующее событие
|
||||
*/
|
||||
fun updateEvent(event: CalendarEvent): Result<CalendarEvent> {
|
||||
return try {
|
||||
val iCalData = createICalendarEvent(event)
|
||||
val eventUrl = dav.calDav.location.newBuilder()
|
||||
.addPathSegment(dav.username)
|
||||
.addPathSegment(dav.calendarName)
|
||||
.addPathSegment("${event.uid}.ics")
|
||||
.build()
|
||||
|
||||
val davResource = DavResource(dav.client, eventUrl)
|
||||
val requestBody = iCalData.toRequestBody("text/calendar; charset=utf-8".toMediaType())
|
||||
|
||||
var responseCode: Int? = null
|
||||
var responseMessage: String? = null
|
||||
|
||||
davResource.put(requestBody) { response ->
|
||||
responseCode = response.code
|
||||
responseMessage = response.message
|
||||
}
|
||||
|
||||
when {
|
||||
responseCode == null -> Result.failure(Exception("Не удалось получить ответ от сервера"))
|
||||
responseCode in 200..299 -> Result.success(event)
|
||||
responseCode == 401 -> Result.failure(Exception("Ошибка авторизации: проверьте логин и пароль"))
|
||||
responseCode == 403 -> Result.failure(Exception("Доступ запрещен: недостаточно прав для изменения календаря '${dav.calendarName}'"))
|
||||
responseCode == 404 -> Result.failure(Exception("Событие с UID '${event.uid}' не найдено в календаре '${dav.calendarName}'"))
|
||||
responseCode == 412 -> Result.failure(Exception("Конфликт версий: событие было изменено другим пользователем"))
|
||||
responseCode in 500..599 -> Result.failure(Exception("Ошибка сервера ($responseCode): $responseMessage"))
|
||||
else -> Result.failure(Exception("HTTP $responseCode: $responseMessage"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("Ошибка при обновлении события: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет событие по UID
|
||||
*/
|
||||
fun deleteEvent(uid: String): Result<Unit> {
|
||||
return try {
|
||||
val eventUrl = dav.calDav.location.newBuilder()
|
||||
.addPathSegment(dav.username)
|
||||
.addPathSegment(dav.calendarName)
|
||||
.addPathSegment("$uid.ics")
|
||||
.build()
|
||||
|
||||
val davResource = DavResource(dav.client, eventUrl)
|
||||
var responseCode: Int? = null
|
||||
var responseMessage: String? = null
|
||||
|
||||
davResource.delete { response ->
|
||||
responseCode = response.code
|
||||
responseMessage = response.message
|
||||
}
|
||||
|
||||
when {
|
||||
responseCode == null -> Result.failure(Exception("Не удалось получить ответ от сервера"))
|
||||
responseCode in 200..299 -> Result.success(Unit)
|
||||
responseCode == 401 -> Result.failure(Exception("Ошибка авторизации: проверьте логин и пароль"))
|
||||
responseCode == 403 -> Result.failure(Exception("Доступ запрещен: недостаточно прав для удаления из календаря '${dav.calendarName}'"))
|
||||
responseCode == 404 -> Result.failure(Exception("Событие с UID '$uid' не найдено в календаре '${dav.calendarName}'"))
|
||||
responseCode in 500..599 -> Result.failure(Exception("Ошибка сервера ($responseCode): $responseMessage"))
|
||||
else -> Result.failure(Exception("HTTP $responseCode: $responseMessage"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("Ошибка при удалении события: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список всех событий из календаря
|
||||
*/
|
||||
fun getAllEvents(): Result<List<CalendarEvent>> {
|
||||
return try {
|
||||
val calendarUrl = dav.calDav.location.newBuilder()
|
||||
.addPathSegment(dav.username)
|
||||
.addPathSegment(dav.calendarName)
|
||||
.build()
|
||||
|
||||
val calendar = DavCalendar(dav.client, calendarUrl)
|
||||
val events = mutableListOf<CalendarEvent>()
|
||||
var hasError = false
|
||||
var errorMessage = ""
|
||||
|
||||
// Простое получение всех ресурсов календаря
|
||||
calendar.propfind(1, CalendarData.NAME) { response, relation ->
|
||||
try {
|
||||
if (relation == at.bitfire.dav4jvm.Response.HrefRelation.MEMBER) {
|
||||
response[CalendarData::class.java]?.let { calData ->
|
||||
val iCalContent = calData.iCalendar ?: return@propfind
|
||||
val uid = extractUidFromICal(iCalContent)
|
||||
uid?.let {
|
||||
parseICalendarEvent(iCalContent, it)?.let { event ->
|
||||
events.add(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
hasError = true
|
||||
errorMessage = "Ошибка при обработке события: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
hasError -> Result.failure(Exception(errorMessage))
|
||||
events.isEmpty() -> Result.success(emptyList())
|
||||
else -> Result.success(events)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when {
|
||||
e.message?.contains("401") == true -> Result.failure(Exception("Ошибка авторизации: проверьте логин и пароль"))
|
||||
e.message?.contains("403") == true -> Result.failure(Exception("Доступ запрещен: недостаточно прав для чтения календаря '${dav.calendarName}'"))
|
||||
e.message?.contains("404") == true -> Result.failure(Exception("Календарь '${dav.calendarName}' не найден"))
|
||||
else -> Result.failure(Exception("Ошибка при получении списка событий: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает события за определенный период
|
||||
*/
|
||||
fun getEventsByDateRange(startDate: LocalDateTime, endDate: LocalDateTime): List<CalendarEvent> {
|
||||
return getAllEvents().getOrDefault(emptyList()).filter { event ->
|
||||
event.startDateTime.isAfter(startDate.minusDays(1)) &&
|
||||
event.endDateTime.isBefore(endDate.plusDays(1))
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseICalendarEvent(iCalData: String, uid: String): CalendarEvent? {
|
||||
return try {
|
||||
val lines = iCalData.lines()
|
||||
var summary = ""
|
||||
var description: String? = null
|
||||
var location: String? = null
|
||||
var startDateTime: LocalDateTime? = null
|
||||
var endDateTime: LocalDateTime? = null
|
||||
|
||||
val dtFormatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
|
||||
val dtFormatterLocal = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")
|
||||
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.startsWith("SUMMARY:") -> summary = line.substringAfter("SUMMARY:")
|
||||
line.startsWith("DESCRIPTION:") -> description = line.substringAfter("DESCRIPTION:")
|
||||
line.startsWith("LOCATION:") -> location = line.substringAfter("LOCATION:")
|
||||
line.startsWith("DTSTART:") -> {
|
||||
val dtStart = line.substringAfter("DTSTART:")
|
||||
startDateTime = try {
|
||||
if (dtStart.endsWith("Z")) {
|
||||
LocalDateTime.parse(dtStart, dtFormatter)
|
||||
} else {
|
||||
LocalDateTime.parse(dtStart, dtFormatterLocal)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
line.startsWith("DTEND:") -> {
|
||||
val dtEnd = line.substringAfter("DTEND:")
|
||||
endDateTime = try {
|
||||
if (dtEnd.endsWith("Z")) {
|
||||
LocalDateTime.parse(dtEnd, dtFormatter)
|
||||
} else {
|
||||
LocalDateTime.parse(dtEnd, dtFormatterLocal)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startDateTime != null && endDateTime != null) {
|
||||
CalendarEvent(
|
||||
uid = uid,
|
||||
summary = summary,
|
||||
description = description,
|
||||
startDateTime = startDateTime,
|
||||
endDateTime = endDateTime,
|
||||
location = location
|
||||
)
|
||||
} else null
|
||||
} catch (_: Exception) {
|
||||
logger.info("Ошибка при парсинге события")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractUidFromICal(iCalData: String): String? {
|
||||
return iCalData.lines().find { it.startsWith("UID:") }?.substringAfter("UID:")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.nano
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.testing.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ApplicationTest {
|
||||
|
||||
@Test
|
||||
fun testRoot() = testApplication {
|
||||
application {
|
||||
module()
|
||||
}
|
||||
client.get("/").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user