Как энтузиаст в освоении технологий я не всегда следую трендам, а пытаюсь увидеть ценность там, куда люди могли не заглянуть. По этой причине исследовательская дорога привела меня к изучению вопроса, как создать встречу в Yandex Calendar и приложить в нее ссылку на Telemost используя доступный API и мой любимый Kotlin. Об этом опыте я и поделюсь в статье.

Моей целью является поделиться наработками, поиск которых оказался на данный момент не тривиальным. Бизнес ценность решений я оставлю за рамками данной статьи.

Как получить доступы для работы с календарем

Для этого идем в настройки Yandex аккаунта, Безопасность, Пароли приложений и выбираем Календарь CalDAV, внутри которого создаем приложение и сохраняем от него пароль. Данный пароль вместе с вашей почтой будет использоваться в API для управлением встречами.

Как создать встречу в Yandex Calendar

Для работы с календарем мне потребовалось ознакомиться с сетевым протоколом CalDAV (Calendaring Extensions to WebDAV), который предназначен для управления встречами в календаре и синхронизации их между устройствами. Данный протокол позволяет работать с календарем по HTTP, передавая в запросе особый формат данный iCalendar, где можно разместить все параметры создаваемой или редактируемой вами встречи. Пример таких настроек:

val uid = UUID.randomUUID().toString()

val ics = """
		BEGIN:VCALENDAR
		VERSION:2.0
		PRODID:-//ProjectName//EN
		CALSCALE:GREGORIAN
		BEGIN:VEVENT
		UID:$uid
		DTSTAMP:$startUtc
		DTSTART:$startUtc
		DTEND:$endUtc
		LOCATION:$location
		SUMMARY:$title
		DESCRIPTION:$description
		ORGANIZER;CN="Pavel":mailto:$username
		ATTENDEE;CN="Attendee 1";RSVP=TRUE:mailto:$attendeeEmail
		END:VEVENT
		END:VCALENDAR
""".trimIndent()

В моем случае уникальным идентификатором встречи будет являться UUID, который мы создадим. Далее созданный uid можно при необходимости сохранить, чтобы в будущем была возможность управлять данной встречей.

Дату встречи вы можете передать через формат

DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")

Для локации можно передать ссылку на ваш сервис видео конференций. Я в своей эксперименте использовал Telemost, интеграция с которым будет дальше в статье. В информацию о встрече можно так же передать необходимый заголовок и описание (убедитесь, что в описании нет спец символов, который могут сломать вам запрос, либо особые символы экранированы, например, запятая, точка с запятой, перевод строки и прочее). В качестве организатора указываем почтовый ящик вашего аккаунта, а ATTENDEE можете указать приглашенных на встречу гостей. Если гостей будет несколько, строку ATTENDEE нужно продублировать с почтой соответствующего гостя.

Далее нам нужно выполнить PUT запрос для создания встречи

val eventUrl = "<https://caldav.yandex.ru/calendars/$username/events-default/$uid.ics>"
val request = HttpPut(eventUrl)
request.addHeader("Content-Type", "text/calendar; charset=utf-8")
request.entity = StringEntity(ics, StandardCharsets.UTF_8)
request.addHeader("Authorization", basicAuth(username, appPassword))

closeableHttpClient.execute(request).use { response ->
		val statusCode = response.statusLine.statusCode
		val body = response.entity?.content?.bufferedReader()?.use { it.readText() }
		if (statusCode !in 200..299) {
				throw RuntimeException("Failed to create event. Status: $statusCode. Body: $body")
		}
}

После этого встреча встреча появится в календаре, а все участники получат уведомление по почте о новом событии. Код не является идеальным, но зато хорошо подходит для иллюстрации работы. Вспомогательные сущности, которые я использовал на случай, если вы захотите проверить работоспособность кода:

private fun basicAuth(user: String, password: String): String {
		val auth = "$user:$password"
		val encoded = Base64.getEncoder().encodeToString(auth.toByteArray(StandardCharsets.UTF_8))
		return "Basic $encoded"
}

fun poolingHttpClient(): CloseableHttpClient {
    val connectionManager = PoolingHttpClientConnectionManager().apply {
        maxTotal = 50
        defaultMaxPerRoute = 20
    }

    val requestConfig = RequestConfig.custom()
        .setConnectTimeout(5000)
        .setSocketTimeout(10000)
        .setConnectionRequestTimeout(2000)
        .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
        .build()

    return HttpClients.custom()
        .setConnectionManager(connectionManager)
        .setDefaultRequestConfig(requestConfig)
        .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
        .build()
}

Для того, чтобы обновить встречу, мы должны вызвать точно такой же PUT запрос с uid интересующего нас события, внутри которого изменить все необходимые параметры, например, состав участников.

Для удаления события необходимо выполнить запрос с передачей uid события, которое вы хотите удалить:

fun deleteEvent(uid: String) {
    val eventUrl = "<https://caldav.yandex.ru/calendars/$username/events-default/$uid.ics>"
    val request = HttpDelete(eventUrl)
    request.addHeader("Authorization", basicAuth(username, appPassword))

    return closeableHttpClient.execute(request).use { response ->
        val statusCode = response.statusLine.statusCode
        val body = response.entity?.content?.bufferedReader()?.use { it.readText() }
        if (statusCode !in 200..299) {
            throw RuntimeException("Failed to create event. Status: $statusCode. Body: $body")
        }
    }
}

Давайте теперь разберемся, как добавить во встречу ссылку на Telemost.

Создаем креды для доступа к Telemost

Для создания OAuth токена для работы с Telomost API нужно в https://oauth.yandex.ru/ создать приложение для доступа к API, после чего сохранить полученный token. Давайте теперь для примера создадим ссылку на будущую встречу в Telemost

Создание встречи

Подробное описание по работе с Telemost API находится в документации. Я лишь рассмотрю один интересующий меня кейс - создание встречи, а для разнообразия буду использовать использовать WebClient .

val monoMeetingResponse = webClient.post()
    .uri("<https://cloud-api.yandex.net/v1/telemost-api/conferences>")
    .bodyValue(meetingSettings)
    .accept(MediaType.APPLICATION_JSON)
    .header("Authorization", "OAuth $YANDEX_TOKEN")
    .retrieve()
    .bodyToMono(MeetingResponse::class.java)
    .doOnError { e ->
        logger.error("Telemost service error: ${e.message}")
    }
    .retryWhen(
        Retry.backoff(3, Duration.ofSeconds(2))
            .doBeforeRetry { signal ->
                logger.warn("Retrying Yandex API call. Attempt: ${signal.totalRetries() + 1}")
            }
    )

Настройка встречи достаточно простая

val meetingSettings = MeetingSettings(
		waitingRoomLevel = "PUBLIC",
		cohosts = listOf(Cohost("login@yandex.ru"))
)

После выполнения запроса мы получим join_url , который можно будет использовать внутри location нашего объекта для создания встречи.

Итоги

Таким образом мне удалось освоить новые API, заглянуть в очередной раз в недра документации и получить удовольствие от работоспособности написанной программы. Данный код в будущем может использоваться для автоматизации работы со встречами в календаре, если в кашей компании кто-то до сих пора занимается этим руками.