feat: Реализация базового CalDav без to-do через REST API обёртку
feat: Информативное логгирование и ответы сервера
This commit is contained in:
188
API_DOCS.md
Normal file
188
API_DOCS.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
## Calendar API Documentation
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
Все эндпоинты требуют Basic авторизации. Используйте ваши учетные данные от CalDAV сервера.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Заголовки для всех запросов
|
||||||
|
Authorization: Basic base64(username:password)
|
||||||
|
CAL_ID: your_calendar_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обязательные заголовки
|
||||||
|
- `Authorization`: Basic авторизация с креденшлами CalDAV
|
||||||
|
- `CAL_ID`: Идентификатор календаря (например: "personal", "work", "family")
|
||||||
|
|
||||||
|
### Эндпоинты
|
||||||
|
|
||||||
|
#### 1. Получить все события
|
||||||
|
```bash
|
||||||
|
GET /calendar/events
|
||||||
|
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||||
|
CAL_ID: personal
|
||||||
|
|
||||||
|
# Ответ:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"uid": "12345-67890-abcdef",
|
||||||
|
"summary": "Важная встреча",
|
||||||
|
"description": "Обсуждение проекта",
|
||||||
|
"startDateTime": "2025-07-20T10:00:00",
|
||||||
|
"endDateTime": "2025-07-20T11:30:00",
|
||||||
|
"location": "Конференц-зал"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Создать новое событие
|
||||||
|
```bash
|
||||||
|
POST /calendar/events
|
||||||
|
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||||
|
CAL_ID: personal
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"summary": "Новая встреча",
|
||||||
|
"description": "Описание встречи",
|
||||||
|
"startDateTime": "2025-07-20T14:00:00",
|
||||||
|
"endDateTime": "2025-07-20T15:00:00",
|
||||||
|
"location": "Офис"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ответ (201 Created):
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"uid": "generated-uid-12345",
|
||||||
|
"summary": "Новая встреча",
|
||||||
|
"description": "Описание встречи",
|
||||||
|
"startDateTime": "2025-07-20T14:00:00",
|
||||||
|
"endDateTime": "2025-07-20T15:00:00",
|
||||||
|
"location": "Офис"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Получить событие по UID
|
||||||
|
```bash
|
||||||
|
GET /calendar/events/{uid}
|
||||||
|
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||||
|
CAL_ID: personal
|
||||||
|
|
||||||
|
# Ответ:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"uid": "12345-67890-abcdef",
|
||||||
|
"summary": "Важная встреча",
|
||||||
|
"description": "Обсуждение проекта",
|
||||||
|
"startDateTime": "2025-07-20T10:00:00",
|
||||||
|
"endDateTime": "2025-07-20T11:30:00",
|
||||||
|
"location": "Конференц-зал"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Обновить событие
|
||||||
|
```bash
|
||||||
|
PUT /calendar/events/{uid}
|
||||||
|
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||||
|
CAL_ID: personal
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"summary": "Обновленная встреча",
|
||||||
|
"description": "Новое описание",
|
||||||
|
"startDateTime": "2025-07-20T15:00:00",
|
||||||
|
"endDateTime": "2025-07-20T16:00:00",
|
||||||
|
"location": "Новый офис"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ответ:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"uid": "12345-67890-abcdef",
|
||||||
|
"summary": "Обновленная встреча",
|
||||||
|
"description": "Новое описание",
|
||||||
|
"startDateTime": "2025-07-20T15:00:00",
|
||||||
|
"endDateTime": "2025-07-20T16:00:00",
|
||||||
|
"location": "Новый офис"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Удалить событие
|
||||||
|
```bash
|
||||||
|
DELETE /calendar/events/{uid}
|
||||||
|
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||||
|
CAL_ID: personal
|
||||||
|
|
||||||
|
# Ответ:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Событие успешно удалено"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Примеры с curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Получить все события из календаря "work"
|
||||||
|
curl -X GET "http://localhost:8080/calendar/events" \
|
||||||
|
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
|
||||||
|
-H "CAL_ID: work"
|
||||||
|
|
||||||
|
# Создать событие в календаре "personal"
|
||||||
|
curl -X POST "http://localhost:8080/calendar/events" \
|
||||||
|
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
|
||||||
|
-H "CAL_ID: personal" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"summary": "Тестовая встреча",
|
||||||
|
"description": "Описание тестовой встречи",
|
||||||
|
"startDateTime": "2025-07-20T10:00:00",
|
||||||
|
"endDateTime": "2025-07-20T11:00:00",
|
||||||
|
"location": "Онлайн"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Обновить событие в календаре "family"
|
||||||
|
curl -X PUT "http://localhost:8080/calendar/events/your-event-uid" \
|
||||||
|
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
|
||||||
|
-H "CAL_ID: family" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"summary": "Семейный обед",
|
||||||
|
"startDateTime": "2025-07-20T14:00:00",
|
||||||
|
"endDateTime": "2025-07-20T15:00:00"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Удалить событие из календаря "personal"
|
||||||
|
curl -X DELETE "http://localhost:8080/calendar/events/your-event-uid" \
|
||||||
|
-H "Authorization: Basic $(echo -n 'username:password' | base64)" \
|
||||||
|
-H "CAL_ID: personal"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
При отсутствии заголовка `CAL_ID`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Заголовок CAL_ID обязателен"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Форматы дат
|
||||||
|
Все даты должны быть в формате ISO LocalDateTime: `YYYY-MM-DDTHH:mm:ss`
|
||||||
|
Например: `2025-07-20T14:30:00`
|
||||||
|
|
||||||
|
### Поддерживаемые календари
|
||||||
|
API поддерживает работу с любыми календарями, доступными пользователю в CalDAV:
|
||||||
|
- `personal` - личный календарь
|
||||||
|
- `work` - рабочий календарь
|
||||||
|
- `family` - семейный календарь
|
||||||
|
- Любые другие календари, созданные пользователем
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# webdav-service
|
||||||
|
|
||||||
|
This project was created using the [Ktor Project Generator](https://start.ktor.io).
|
||||||
|
|
||||||
|
Here are some useful links to get you started:
|
||||||
|
|
||||||
|
- [Ktor Documentation](https://ktor.io/docs/home.html)
|
||||||
|
- [Ktor GitHub page](https://github.com/ktorio/ktor)
|
||||||
|
- The [Ktor Slack chat](https://app.slack.com/client/T09229ZC6/C0A974TJ9). You'll need
|
||||||
|
to [request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up) to join.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Here's a list of features included in this project:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|------------------------------------------------------------------------|------------------------------------------------------------------------------------|
|
||||||
|
| [Routing](https://start.ktor.io/p/routing) | Provides a structured routing DSL |
|
||||||
|
| [Authentication](https://start.ktor.io/p/auth) | Provides extension point for handling the Authorization header |
|
||||||
|
| [Authentication Basic](https://start.ktor.io/p/auth-basic) | Handles 'Basic' username / password authentication scheme |
|
||||||
|
| [Call Logging](https://start.ktor.io/p/call-logging) | Logs client requests |
|
||||||
|
| [Content Negotiation](https://start.ktor.io/p/content-negotiation) | Provides automatic content conversion according to Content-Type and Accept headers |
|
||||||
|
| [kotlinx.serialization](https://start.ktor.io/p/kotlinx-serialization) | Handles JSON serialization using kotlinx.serialization library |
|
||||||
|
|
||||||
|
## Building & Running
|
||||||
|
|
||||||
|
To build or run the project, use one of the following tasks:
|
||||||
|
|
||||||
|
| Task | Description |
|
||||||
|
|-------------------------------|----------------------------------------------------------------------|
|
||||||
|
| `./gradlew test` | Run the tests |
|
||||||
|
| `./gradlew build` | Build everything |
|
||||||
|
| `buildFatJar` | Build an executable JAR of the server with all dependencies included |
|
||||||
|
| `buildImage` | Build the docker image to use with the fat JAR |
|
||||||
|
| `publishImageToLocalRegistry` | Publish the docker image locally |
|
||||||
|
| `run` | Run the server |
|
||||||
|
| `runDocker` | Run using the local docker image |
|
||||||
|
|
||||||
|
If the server starts successfully, you'll see the following output:
|
||||||
|
|
||||||
|
```
|
||||||
|
2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds.
|
||||||
|
2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080
|
||||||
|
```
|
||||||
|
|
||||||
40
build.gradle.kts
Normal file
40
build.gradle.kts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
val kotlin_version: String by project
|
||||||
|
val logback_version: String by project
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.1.10"
|
||||||
|
id("io.ktor.plugin") version "3.2.2"
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.nano"
|
||||||
|
version = "0.0.1"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass = "com.nano.ApplicationKt"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.ktor:ktor-server-core")
|
||||||
|
implementation("io.ktor:ktor-server-auth")
|
||||||
|
implementation("io.ktor:ktor-server-call-logging")
|
||||||
|
implementation("io.ktor:ktor-server-content-negotiation")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json")
|
||||||
|
implementation("io.ktor:ktor-server-netty")
|
||||||
|
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||||
|
implementation("com.github.bitfireAT:dav4jvm:2.2.1")
|
||||||
|
|
||||||
|
testImplementation("io.ktor:ktor-server-test-host")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
|
}
|
||||||
17
src/main/kotlin/Application.kt
Normal file
17
src/main/kotlin/Application.kt
Normal file
@@ -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()
|
||||||
|
}
|
||||||
18
src/main/kotlin/Monitoring.kt
Normal file
18
src/main/kotlin/Monitoring.kt
Normal file
@@ -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("/") }
|
||||||
|
}
|
||||||
|
}
|
||||||
347
src/main/kotlin/Routing.kt
Normal file
347
src/main/kotlin/Routing.kt
Normal file
@@ -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}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/main/kotlin/Security.kt
Normal file
31
src/main/kotlin/Security.kt
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/kotlin/Serialization.kt
Normal file
22
src/main/kotlin/Serialization.kt
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
368
src/main/kotlin/Service/Calendar.kt
Normal file
368
src/main/kotlin/Service/Calendar.kt
Normal file
@@ -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:")
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/test/kotlin/ApplicationTest.kt
Normal file
21
src/test/kotlin/ApplicationTest.kt
Normal file
@@ -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