[Kotlin] Погружение в JetBrains Space Applicaitons
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр.Недавно JetBrains представили свой новый продукт под названиемSpace, о чем был своевременныйпост на хабре. Прошло немного времени и уже пора бы попробовать некоторые его особенности как платформы. В этой статье речь пойдет оSpace Applications.Space Applications - это расширения серверной и клиентской частей. Applications позволяют взаимодействовать с разными компонентами платформы и расширять её функциональность.Первым делом рассмотрим расширения серверной части, которые предоставляет Space. Основное ограничение - Application нельзя запустить непосредственно внутри окружения Space. Для предоставления доступа между Space и Application используется так называемый Endpoint, в котором мы указываем Endpoint URI - адрес нашего плагина, на который Space будет отправлять запросы и коммуницировать с помощью Space HTTP API.На данный момент известно несколько видов Applications:Chatbot
Cтандартный чатбот, с которым взаимодействие происходит в приватном с ним чате. На данный момент кастомизация сильно уступает, например, Telegram. Нельзя добавлять бота в канал (групповой чат), а из интерактивного интерфейса пока доступны только кнопки. Slash commands
Для каждого канала могут быть заданы разные команды, и когда пользователь вводит "/", ему показывается список доступных команд. На данный момент доступны только для чатботов. Client applicationsИспользуют функционал Space, чтобы получать от него разную информацию или взаимодействовать с разными модулями и компонентами, например, открывать/закрывать issues, писать сообщения, отправлять посты в канал и так далее. Custom menus
Позволяют расширить стандартные меню новыми элементами. На момент написания статьи api для них еще не доступен. Также можно свободно комбинировать сразу несколько типов Applications. Например, создать бота, у которого будет кнопка для запуска билда проекта на CI/CD сервере, и результат которого вернется в общий канал.Для начала Application требуется создать. Делается это через Administration menu. При создании можно указать, какими правами оно будет обладать, чтобы приложение имело доступ только к тем ресурсам, которые нами явно указаны.
После создания в табе Authentication требуется выбрать один из возможных Authentication flows, в зависимости от типа приложения. Кратко каждый из них можно описать так:
- Client Credentials Flow - самый простой способ. Работает через client id и client secret. Наше приложение будет работать от своего лица и не сможет получить доступ к некоторым компонентам платформы. Используется в полностью серверном приложении.
- Authorization Code Flow - логинимся в приложение через Space, получаем код авторизации, который приложение использует чтобы из него получить токен и работать от лица пользователя.
- Implicit Flow - идея таже, что и Authorization Code Flow, только клиент логинится на стороне браузера.
Client applicationsВ данном разделе мы используем функционал Space, чтобы получить от сервера разную информацию: каналы, приложения, пользователи, проекты, и т.д. build.gradle.kts
repositories {
mavenCentral()
maven("https://kotlin.bintray.com/kotlinx")
maven("https://maven.pkg.jetbrains.space/public/p/space/maven")
}
dependencies {
implementation(kotlin("stdlib"))
// Space api sdk
// https://www.jetbrains.com/help/space/space-sdk.html
val space_version = "61400-beta"
implementation("org.jetbrains:space-sdk-jvm:$space_version")
// Ktor (http client)
// https://github.com/ktorio/ktor
val ktor_version = "1.4.3"
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-core-jvm:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
}
Сначала нам требуется создать и настроить клиент. Мы будем использовать Client Credentials Flow:
private const val spaceInstanceUrl = "https://makentoshe.jetbrains.space"
val spaceClient = SpaceHttpClient(HttpClient(CIO)).withServiceAccountTokenSource(
ClientCredentialsFlow.clientId, ClientCredentialsFlow.clientSecret, spaceInstanceUrl
)
object ClientCredentialsFlow {
const val clientId: String = TODO(“Put your client_id here”)
const val clientSecret: String = TODO(“Put your client_secret here”)
}
Ниже приведены примеры некоторых расширений и запросов.
// There are some examples of retrieving several data from the Space instance.
// We can process this info as we want - create analytics, office dashboards, and so on.
fun main() = runBlocking {
val channels = spaceClient.chats.channels.listAllChannels("").data
println("Channels: ${channels.map { "${it.name}(${it.channelId})" }}")
// View application rights allows to see all applications
// If rights were not accepted - the application can see only itself.
val applications = spaceClient.applications.getAllApplications("")
println("Applications: ${applications.map { "${it.name}(${it.id})" }}")
// Works only with View member profile rights
val profiles = spaceClient.teamDirectory.profiles.getAllProfiles().data
println("Profiles: ${profiles.map { "${it.username}(${it.id})" }}")
// Works only with View project parameters rights
// These rights can be managed for selected projects or for whole projects at one time.
val projects = spaceClient.projects.getAllProjects().data
println("Projects: ${projects.map { "${it.name}(${it.id})" }}")
// Works only with Project Issues: View issues rights
val issues = projects.firstOrNull()?.let { getProjectIssues(it) }
println("Issues: ${issues?.map { "${it.title}(${it.id})" }} ")
}
private suspend fun getProjectIssues(project: PR_Project): List<Issue> {
return spaceClient.projects.planning.issues.getAllIssues(
project = ProjectIdentifier.Id(project.id),
assigneeId = emptyList(),
statuses = emptyList(),
sorting = IssuesSorting.CREATED,
descending = true
).data
}
Output
Channels: [Booruchan(4UHs4I3yyno1), general(10xDLp0yqy4w), Habrachan(4HyHLw3SnO9Y), Sipichan(15A2hA1RpAsp)]
Applications: [client(2czEkY3AIaV0), chatbot(d3Q8Z0UeVCF)]
Profiles: [Makentoshe(2iqI4p3gzufl)]
Projects: [Booruchan(qN0K31awqo6), Habrachan(1tJHqn2A76Yf), Sipichan(35BreB35gvdA)]
Issues: [Add custom Run Configuration and support ngrok startup(3S09oT4JHvpC), Add Client template for Space plugin with Gradle (atQRe1SIklB), Add Blank template for Space application plugin support for Gradle(4REln04HAo5k)]
В зависимости от настроенных прав полученные данные могут отличаться. Стоит обратить внимание, что почти все данные возвращаются через .data. Изначально возвращается объект Batch, который используется для пагинации и содержит текущий набор данных, ключ к следующему набору, и общее количество элементов. При запросах стоит это учитывать, однако для простоты в данной статье пагинация опущена.Исходники проекта доступны на githubи со временем будут пополняться.Chatbot + slash command ApplicationsСерверные Applications, по сравнению с клиентскими, требуют дополнительной подготовки. Первым делом нам нужно серверное приложение, на котором будет хоститься наш бот. Мы уже использовали HTTP клиент Ktor, так что можем создавать на нем и серверную часть. Создание конфигурационных файлов для Ktor здесь будет опущено, но естьдокументация, на которую можно опереться.Когда мы поднимем наш сервер, мы сможем получить его URL, чтобы Space мог его использовать. Покупать хост, или искать бесплатный не комильфо, поэтому мы пойдем обходным путем - будем использовать туннельный сервис. В официальной документации используется ngrok, однако лично я предпочитаю localtunnel, из-за его возможности указывать постоянный адрес.
// localtunnel
npx localtunnel --port 8080 --subdomain makentoshe
// ngrok
ngrok http 8080
В любом случае мы получим URL - это как раз то, что нам нужно.
Возвращаемся в настройки Applications в нашем Space. В табе Endpoint в поле Endpoint URI нужно будет указать наш url. Для меня это https://makentoshe.loca.lt/api/chatbot. Про то, зачем нужен /api/chatbot будет дальше.
В том же табе существует два способа верифицировать наши запросы:
- Verification token - этот токен кладется в каждый запрос от Space. Нам остается сравнить эти токены, и если они совпадают - мы общаемся с нашим Space.
Пример ответа с Verification token
{
"className": "ListCommandsPayload",
"accessToken": "",
"verificationToken": "d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3",
"userId": "2kawvQ4F6GM6"
}
- Signing key - более продвинутый метод. Для каждого запроса создается хеш, который кладется в его заголовок. Когда мы получаем запрос мы также вычисляем хеш, и если они совпадают - все ок. Подробнее об алгоритме - здесь.
Пример ответа с Signing key
POST /api/chatbot HTTP/1.1
Host: 12345abcdef.ngrok.io
User-Agent: Space (61355) Ktor http-client
Content-Length: 163
Accept: */*
Accept-Charset: UTF-8
Content-Type: application/json
X-Forwarded-For: 123.456.123.456
X-Forwarded-Proto: https
X-Space-Signature: 2aa8cba6217a28686de0ca8dcfe2a1d0795e343d744a0c5307308e43777593a5
X-Space-Timestamp: 1607623492912
Accept-Encoding: gzip
{"className":"ListCommandsPayload","accessToken":"","verificationToken":"d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3","userId":"2kawvQ4F6GM6"}
В качестве примера мы возьмем оба варианта и будем сверять и хеши, и токены.
object Endpoint {
const val verificationToken: String = TODO("Place your verification_token")
const val signingKey: String = TODO("Place your signing_key")
fun verify(payload: ApplicationPayload): Boolean {
return payload.verifyWithToken(verificationToken)
}
fun verify(timestamp: String, signature: String, body: String): Boolean {
val hmacSha256 = Mac.getInstance("HmacSHA256")
hmacSha256.init(SecretKeySpec(signingKey.toByteArray(), "HmacSHA256"))
val hash = hmacSha256.doFinal("$timestamp:$body".toByteArray()).toHexString()
return hash == signature
}
private fun ByteArray.toHexString() = joinToString("") { (0xFF and it.toInt()).toString(16).padStart(2, '0') }
}
Первым делом для сервера нам нужно указать Routing. Это тот самый Endpoint для Space, который он будет использовать, чтобы обращаться к нашему боту.
@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
install(Routing) {
chatbot()
}
}
fun Routing.chatbot() {
post("api/chatbot") {
val receiveBody = call.receiveText()
val timestamp = call.request.headers["x-space-timestamp"]
?: return@post call.respond(HttpStatusCode.BadRequest)
val signature = call.request.headers["x-space-signature"]
?: return@post call.respond(HttpStatusCode.BadRequest)
if (!Endpoint.verify(timestamp, signature, receiveBody)) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
val payload = readPayload(receiveBody)
if (!Endpoint.verify(payload)) {
return@post call.respond(HttpStatusCode.Unauthorized)
}
try {
processChatbotPayload(payload)
} catch (unknownCommand: IllegalStateException) {
LoggerFactory.getLogger("Chatbot").error(unknownCommand.message)
}
}
}
Как только к серверу происходит обращение по заданному адресу, первым делом мы проверяем, что запрос пришел именно от нашего Space. Если это не так - возвращаем 401 Unauthorized.Далее боту следует обработать полученный payload. На момент написания статьи существует 5 имплементаций ApplicationPayload:
- MessagePayload - передается нам, когда пользователь отправляет обычное сообщение.
- ListCommandsPayload - передается нам, когда пользователь начинает сообщение с "/" и затем вводит команду посимвольно. Здесь происходит запрос все существующих команд, которые нам нужно будет вернуть в виде json. На ввод каждого символа передается новый Payload.
- MessageActionPayload - передается, когда пользователь нажимает на интерактивный элемент сообщения, например, на кнопку. Корректно срабатывают только элементы из последнего сообщения. Все предыдущие элементы будут "стерты"(например, их actionId всегда будут пустой строкой).
- MenuActionPayload - передается, когда пользователь нажимает на кастомный элемент на одном из меню (ProjectMenu, LocationMenu, ChannelMessageMenu, ChannelAttachmentMenu и т.д.). На момент написания статьи все еще не доступен.
- ListMenuExtensionsPayload - имеет ту же идею, что и ListCommandsPayload, только для меню. На момент написания статьи все еще не доступен.
private suspend fun PipelineContext<*, ApplicationCall>.processChatbotPayload(payload: ApplicationPayload) {
when (payload) {
is MessagePayload -> {
processChatbotMessagePayload(payload)
}
is MessageActionPayload -> {
processChatbotMessageActionPayload(payload)
}
is ListCommandsPayload -> {
processChatbotListCommandsPayload(payload)
}
}
}
Когда пользователь отправляет нам сообщение - скорее всего это команда, которую боту надо выполнить. Команду можно описать одним дата классом:
data class Command(
val name: String,
val info: String,
val action: suspend (payload: MessagePayload) -> Unit
) {
fun toCommandDetail() = CommandDetail(name, info)
}
Предлагаю в качестве примера реализовать три простых команды:
- help - выводит сообщение со списком всех команд;
- echo - выводит переданное сообщение обратно пользователю;
- interactive <type> - выводит пример выбранного типа интерактивного элемента, например, button.
object Commands {
val help = Command(
"help",
"Show this help",
) { payload ->
val context = HelpContext.from(payload)
printToChat(context, message {
section {
text(
"""Help message:
name - Show this help
${echo.name} - ${echo.info}
${interactive.name} - ${interactive.info}
""".trimIndent()
)
}
})
}
val echo = Command(
"echo",
"Echoing the input string",
) { payload ->
val context = EchoContext.from(payload)
val body = context.message.body
printToChat(context, message = if (body is ChatMessage.Text) {
message { section { text(body.text) } }
} else {
message { section { text("Skip the Block body") } }
})
}
val interactive = Command(
"interactive",
"Displaying available message interactive elements"
) { payload ->
// TODO finish later
}
val list = listOf(help, echo, interactive)
val commands: Commands
get() = Commands(list.map { it.toCommandDetail() })
}
Сначала разберемся, что происходит в командах help и echo. Из ApplicationPayload каждой команды мы можем извлечь нужные данные для обработки и положить их в соответствующий Context, который мы сами определяем. Этот класс помогает нам аккумулировать данные в одном месте.Context.kt
interface UserContext {
val userId: String
}
fun userContext(applicationPayload: ApplicationPayload) = object: UserContext {
override val userId = applicationPayload.userId ?: throw IllegalArgumentException("Payload does not contains user id")
}
data class EchoContext(override val userId: String, val message: MessageContext) : UserContext {
companion object {
fun from(payload: ApplicationPayload): EchoContext? = when (payload) {
is MessagePayload -> from(payload)
else -> null
}
fun from(payload: MessagePayload): EchoContext {
return EchoContext(payload.userId, payload.message)
}
}
}
data class HelpContext(override val userId: String): UserContext {
companion object {
fun from(payload: ApplicationPayload): HelpContext {
return HelpContext(payload.userId!!)
}
}
}
data class InteractiveContext(override val userId: String): UserContext {
companion object {
fun from(payload: ApplicationPayload): InteractiveContext {
return InteractiveContext(payload.userId!!)
}
}
}
После того, как мы получили наш userId и остальные сопутствующие данные, нам нужно их вывести в чат пользователя, с которым в данный момент взаимодействует бот. Для этого напишем отдельную функцию printToChat.
suspend fun printToChat(context: UserContext, message: ChatMessage) {
val member = ProfileIdentifier.Id(context.userId)
spaceClient.chats.messages.sendMessage(MessageRecipient.Member(member), message)
}
Любое сообщение в Space представлено в виде класса ChatMessage. Этот класс является sealed и имеет 2 наследника: ChatMessage.Text и ChatMessage.Block. ChatMessage.Text является простым текстовым сообщением поддерживающим markdown.
ChatMessage.Text(
"""
**bold**
`code`
*italic*
@{2iqI4p3gzufl, Maksim Hvostov}
@{2iqI4p3gzufl, Makentoshe}
@{2iqI4p3gzufl, any string may be replaced with my name}
[\#general](im/group/10xDLp0yqy4w)
>quote
""".trimIndent()
)
Output
ChatMessage.Block существует для более сложных сообщений, которые могут быть разбиты на секции, иметь разделители, и другие продвинутые элементы форматирования.Для него существует специальный DSL, который мы и используем. Всё начинается с функции message - это корень нашего сообщения, в котором мы можем указать:
- MessageOutline - это дополнительная подпись под именем отправителя и, по желанию, иконка, которая задается через строку. Какой именно должна быть эта строка пока не понятно, поэтому вместо иконки передаем null.
- MessageStyle - изменяет некоторые цвета в сообщении, в соответствии со стилем.
- Для чего нужна messageData я пока так и не понял. Чтобы не было передано в сообщении нашему боту, это поле всегда будет null.
message {
this.outline = MessageOutline(null, "Outline text")
this.style = MessageStyle.PRIMARY
section {
this.text("Primary message")
}
}
Outputs
После этого мы можем либо определить новую секцию методом section, либо поставить разделитель методом divide.В секции нам доступны:
- обычное текстовое поле через метод text.
- текстовое поле с тегом, для которого можно добавить отдельный стиль.
- текстовое поле с изображением справа от секции.
- текстовое поле с иконкой, в которую опять же передается строка.
- footer и header.
- конструкция “поле значение” через fields
- интерактивные элементы, из которых на момент написания статьи доступна только кнопка.
Как это выглядит:
message {
this.outline = MessageOutline(null, "Outline text")
this.style = MessageStyle.PRIMARY
section {
this.header = "Section header"
this.footer = "Section footer"
this.text("Plain text")
}
divider()
section {
header = "This tag may indicate something not good"
this.textWithTag("Text with tag", "error tag", MessageStyle.ERROR)
}
section {
this.text("Plain text just to fill some space.")
this.textWithImage("Text with image", "https://www.jetbrains.com/space/img/feedback-section/video-preview.png")
}
section {
this.textWithIcon("Text with icon", "", MessageStyle.WARNING)
}
section {
header = "Fields"
this.fields {
this.field("field1", "value1")
this.field("field2", "value2")
this.field("field3", "value3")
this.field("field4", "value4")
}
this.controls {
this.button("Disabled button without any action", PostMessageAction("", ""), disabled = true)
}
}
}
Output
Осталось реализовать команду interaction и соединить все вместе.
val interactive = Command(
"interactive",
"Displaying available message interactive elements"
) { payload ->
val context = InteractiveContext.from(payload)
val arguments = payload.commandArguments()
if (arguments == null || arguments.isBlank()) {
return@Command printToChat(context, message {
section {
text("Specify one of the selected ui elements:\nbutton")
}
})
}
printToChat(context, message {
section {
header = "Available message interactive elements"
controls {
when (arguments) {
"button" -> {
val primaryAction = PostMessageAction("ButtonPrimaryActionId", "InteractiveButtonPayloadPrimary")
button("Primary", primaryAction, MessageButtonStyle.PRIMARY)
val secondaryAction = PostMessageAction("ButtonSecondaryActionId", "InteractiveButtonPayloadSecondary")
button("Secondary", secondaryAction, MessageButtonStyle.SECONDARY)
val regularAction = PostMessageAction("ButtonRegularActionId", "InteractiveButtonPayloadRegular")
button("Regular", regularAction, MessageButtonStyle.REGULAR)
val dangerAction = PostMessageAction("ButtonDangerActionId", "InteractiveButtonPayloadDanger")
button("Danger", dangerAction, MessageButtonStyle.DANGER)
}
}
}
}
})
}
Теперь мы можем подключать обработку разных ApplicationPayload's. Для простого сообщения от пользователя мы ищем команду из списка и выполняем её.
private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessagePayload(payload: MessagePayload) {
Commands.list.find { it.name == payload.command() }?.action?.invoke(payload)
?: return call.respond(HttpStatusCode.NotFound)
call.respond(HttpStatusCode.OK)
}
Для показа списка команд мы возвращаем список всех команд. Здесь же мы используем подключенный ранее Jackson.
private suspend fun PipelineContext<*, ApplicationCall>.processChatbotListCommandsPayload(payload: ListCommandsPayload) {
call.respondText(ObjectMapper().writeValueAsString(Commands.commands), ContentType.Application.Json)
}
Для нажатия на интерактивный элемент мы будем принтить actionId этой команды.
private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessageActionPayload(payload: MessageActionPayload) {
printToChat(userContext(payload), message { section { text(payload.actionId) } })
call.respond(HttpStatusCode.OK)
}
Исходники чатбота доступны по ссылке на github.
ИтогоПочти все, что может сделать пользователь используя Space через пользовательский интерфейс, можно сделать и через предоставленный Api(если на то будут предоставлены разрешения). На самом деле у Space гораздо больше способов расширения функционала. Мы рассмотрели лишь самый продвинутый уровень. Большую часть остальных расширений можно настроить имея вообще минимальные навыки программирования. Вот остальные способы расширения функционала платформы:
- Webhooks - оповещение внешних сервисов о событиях в Space.
- Authorizations - доступ к Space через OAuth 2.0.
- Custom fields - добавление кастомных полей к сущностям Space.
- Скрипты для импорта данных из других приложений.
- Сторонние интеграции с инструментами.
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Разработка мобильных приложений, Разработка под Android, GitHub, Kotlin] Легкий DataBinding для Android
- [Космонавтика, Транспорт] В доке впервые восстанавливают сразу две ступени Falcon 9
- [IT-компании] Инженер SpaceX признал вину в деле о продаже инсайдерской информации в дарквебе
- [Космонавтика, Транспорт] НАСА провело успешные огневые испытания первой ступени ракеты SLS
- [Программирование, Java, Kotlin, Интервью] Большой разговор с новым Kotlin Project Lead Романом Елизаровым
- [Финансы в IT, Космонавтика, Транспорт] НАСА начало исследование доступности ракеты SLS
- [Научно-популярное, Космонавтика] SpaceX объявил первый орбитальный запуск Starship летом 2021 года. Teslarati
- [Космонавтика, Транспорт] SpaceX поставила новый рекорд: 9 повторных использований первой ступени Falcon 9
- [Космонавтика] Роскосмос выбрал первого российского космонавта для полёта на Crew Dragon
- [Научно-популярное, Космонавтика] Starlink L21. Запуски года: 22 всего, 11 от США
Теги для поиска: #_kotlin, #_jetbrains_space, #_space, #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:22
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр.Недавно JetBrains представили свой новый продукт под названиемSpace, о чем был своевременныйпост на хабре. Прошло немного времени и уже пора бы попробовать некоторые его особенности как платформы. В этой статье речь пойдет оSpace Applications.Space Applications - это расширения серверной и клиентской частей. Applications позволяют взаимодействовать с разными компонентами платформы и расширять её функциональность.Первым делом рассмотрим расширения серверной части, которые предоставляет Space. Основное ограничение - Application нельзя запустить непосредственно внутри окружения Space. Для предоставления доступа между Space и Application используется так называемый Endpoint, в котором мы указываем Endpoint URI - адрес нашего плагина, на который Space будет отправлять запросы и коммуницировать с помощью Space HTTP API.На данный момент известно несколько видов Applications:Chatbot Cтандартный чатбот, с которым взаимодействие происходит в приватном с ним чате. На данный момент кастомизация сильно уступает, например, Telegram. Нельзя добавлять бота в канал (групповой чат), а из интерактивного интерфейса пока доступны только кнопки. Slash commands Для каждого канала могут быть заданы разные команды, и когда пользователь вводит "/", ему показывается список доступных команд. На данный момент доступны только для чатботов. Client applicationsИспользуют функционал Space, чтобы получать от него разную информацию или взаимодействовать с разными модулями и компонентами, например, открывать/закрывать issues, писать сообщения, отправлять посты в канал и так далее. Custom menus Позволяют расширить стандартные меню новыми элементами. На момент написания статьи api для них еще не доступен. Также можно свободно комбинировать сразу несколько типов Applications. Например, создать бота, у которого будет кнопка для запуска билда проекта на CI/CD сервере, и результат которого вернется в общий канал.Для начала Application требуется создать. Делается это через Administration menu. При создании можно указать, какими правами оно будет обладать, чтобы приложение имело доступ только к тем ресурсам, которые нами явно указаны. После создания в табе Authentication требуется выбрать один из возможных Authentication flows, в зависимости от типа приложения. Кратко каждый из них можно описать так:
repositories {
mavenCentral() maven("https://kotlin.bintray.com/kotlinx") maven("https://maven.pkg.jetbrains.space/public/p/space/maven") } dependencies { implementation(kotlin("stdlib")) // Space api sdk // https://www.jetbrains.com/help/space/space-sdk.html val space_version = "61400-beta" implementation("org.jetbrains:space-sdk-jvm:$space_version") // Ktor (http client) // https://github.com/ktorio/ktor val ktor_version = "1.4.3" implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-core-jvm:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") } private const val spaceInstanceUrl = "https://makentoshe.jetbrains.space"
val spaceClient = SpaceHttpClient(HttpClient(CIO)).withServiceAccountTokenSource( ClientCredentialsFlow.clientId, ClientCredentialsFlow.clientSecret, spaceInstanceUrl ) object ClientCredentialsFlow { const val clientId: String = TODO(“Put your client_id here”) const val clientSecret: String = TODO(“Put your client_secret here”) } // There are some examples of retrieving several data from the Space instance.
// We can process this info as we want - create analytics, office dashboards, and so on. fun main() = runBlocking { val channels = spaceClient.chats.channels.listAllChannels("").data println("Channels: ${channels.map { "${it.name}(${it.channelId})" }}") // View application rights allows to see all applications // If rights were not accepted - the application can see only itself. val applications = spaceClient.applications.getAllApplications("") println("Applications: ${applications.map { "${it.name}(${it.id})" }}") // Works only with View member profile rights val profiles = spaceClient.teamDirectory.profiles.getAllProfiles().data println("Profiles: ${profiles.map { "${it.username}(${it.id})" }}") // Works only with View project parameters rights // These rights can be managed for selected projects or for whole projects at one time. val projects = spaceClient.projects.getAllProjects().data println("Projects: ${projects.map { "${it.name}(${it.id})" }}") // Works only with Project Issues: View issues rights val issues = projects.firstOrNull()?.let { getProjectIssues(it) } println("Issues: ${issues?.map { "${it.title}(${it.id})" }} ") } private suspend fun getProjectIssues(project: PR_Project): List<Issue> { return spaceClient.projects.planning.issues.getAllIssues( project = ProjectIdentifier.Id(project.id), assigneeId = emptyList(), statuses = emptyList(), sorting = IssuesSorting.CREATED, descending = true ).data } Channels: [Booruchan(4UHs4I3yyno1), general(10xDLp0yqy4w), Habrachan(4HyHLw3SnO9Y), Sipichan(15A2hA1RpAsp)]
Applications: [client(2czEkY3AIaV0), chatbot(d3Q8Z0UeVCF)] Profiles: [Makentoshe(2iqI4p3gzufl)] Projects: [Booruchan(qN0K31awqo6), Habrachan(1tJHqn2A76Yf), Sipichan(35BreB35gvdA)] Issues: [Add custom Run Configuration and support ngrok startup(3S09oT4JHvpC), Add Client template for Space plugin with Gradle (atQRe1SIklB), Add Blank template for Space application plugin support for Gradle(4REln04HAo5k)] // localtunnel
npx localtunnel --port 8080 --subdomain makentoshe // ngrok ngrok http 8080 Возвращаемся в настройки Applications в нашем Space. В табе Endpoint в поле Endpoint URI нужно будет указать наш url. Для меня это https://makentoshe.loca.lt/api/chatbot. Про то, зачем нужен /api/chatbot будет дальше. В том же табе существует два способа верифицировать наши запросы:
{
"className": "ListCommandsPayload", "accessToken": "", "verificationToken": "d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3", "userId": "2kawvQ4F6GM6" }
POST /api/chatbot HTTP/1.1
Host: 12345abcdef.ngrok.io User-Agent: Space (61355) Ktor http-client Content-Length: 163 Accept: */* Accept-Charset: UTF-8 Content-Type: application/json X-Forwarded-For: 123.456.123.456 X-Forwarded-Proto: https X-Space-Signature: 2aa8cba6217a28686de0ca8dcfe2a1d0795e343d744a0c5307308e43777593a5 X-Space-Timestamp: 1607623492912 Accept-Encoding: gzip {"className":"ListCommandsPayload","accessToken":"","verificationToken":"d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3","userId":"2kawvQ4F6GM6"} object Endpoint {
const val verificationToken: String = TODO("Place your verification_token") const val signingKey: String = TODO("Place your signing_key") fun verify(payload: ApplicationPayload): Boolean { return payload.verifyWithToken(verificationToken) } fun verify(timestamp: String, signature: String, body: String): Boolean { val hmacSha256 = Mac.getInstance("HmacSHA256") hmacSha256.init(SecretKeySpec(signingKey.toByteArray(), "HmacSHA256")) val hash = hmacSha256.doFinal("$timestamp:$body".toByteArray()).toHexString() return hash == signature } private fun ByteArray.toHexString() = joinToString("") { (0xFF and it.toInt()).toString(16).padStart(2, '0') } } @Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { install(Routing) { chatbot() } } fun Routing.chatbot() { post("api/chatbot") { val receiveBody = call.receiveText() val timestamp = call.request.headers["x-space-timestamp"] ?: return@post call.respond(HttpStatusCode.BadRequest) val signature = call.request.headers["x-space-signature"] ?: return@post call.respond(HttpStatusCode.BadRequest) if (!Endpoint.verify(timestamp, signature, receiveBody)) { return@post call.respond(HttpStatusCode.Unauthorized) } val payload = readPayload(receiveBody) if (!Endpoint.verify(payload)) { return@post call.respond(HttpStatusCode.Unauthorized) } try { processChatbotPayload(payload) } catch (unknownCommand: IllegalStateException) { LoggerFactory.getLogger("Chatbot").error(unknownCommand.message) } } }
private suspend fun PipelineContext<*, ApplicationCall>.processChatbotPayload(payload: ApplicationPayload) {
when (payload) { is MessagePayload -> { processChatbotMessagePayload(payload) } is MessageActionPayload -> { processChatbotMessageActionPayload(payload) } is ListCommandsPayload -> { processChatbotListCommandsPayload(payload) } } } data class Command(
val name: String, val info: String, val action: suspend (payload: MessagePayload) -> Unit ) { fun toCommandDetail() = CommandDetail(name, info) }
object Commands {
val help = Command( "help", "Show this help", ) { payload -> val context = HelpContext.from(payload) printToChat(context, message { section { text( """Help message: name - Show this help ${echo.name} - ${echo.info} ${interactive.name} - ${interactive.info} """.trimIndent() ) } }) } val echo = Command( "echo", "Echoing the input string", ) { payload -> val context = EchoContext.from(payload) val body = context.message.body printToChat(context, message = if (body is ChatMessage.Text) { message { section { text(body.text) } } } else { message { section { text("Skip the Block body") } } }) } val interactive = Command( "interactive", "Displaying available message interactive elements" ) { payload -> // TODO finish later } val list = listOf(help, echo, interactive) val commands: Commands get() = Commands(list.map { it.toCommandDetail() }) } interface UserContext {
val userId: String } fun userContext(applicationPayload: ApplicationPayload) = object: UserContext { override val userId = applicationPayload.userId ?: throw IllegalArgumentException("Payload does not contains user id") } data class EchoContext(override val userId: String, val message: MessageContext) : UserContext { companion object { fun from(payload: ApplicationPayload): EchoContext? = when (payload) { is MessagePayload -> from(payload) else -> null } fun from(payload: MessagePayload): EchoContext { return EchoContext(payload.userId, payload.message) } } } data class HelpContext(override val userId: String): UserContext { companion object { fun from(payload: ApplicationPayload): HelpContext { return HelpContext(payload.userId!!) } } } data class InteractiveContext(override val userId: String): UserContext { companion object { fun from(payload: ApplicationPayload): InteractiveContext { return InteractiveContext(payload.userId!!) } } } suspend fun printToChat(context: UserContext, message: ChatMessage) {
val member = ProfileIdentifier.Id(context.userId) spaceClient.chats.messages.sendMessage(MessageRecipient.Member(member), message) } ChatMessage.Text(
""" **bold** `code` *italic* @{2iqI4p3gzufl, Maksim Hvostov} @{2iqI4p3gzufl, Makentoshe} @{2iqI4p3gzufl, any string may be replaced with my name} [\#general](im/group/10xDLp0yqy4w) >quote """.trimIndent() ) ChatMessage.Block существует для более сложных сообщений, которые могут быть разбиты на секции, иметь разделители, и другие продвинутые элементы форматирования.Для него существует специальный DSL, который мы и используем. Всё начинается с функции message - это корень нашего сообщения, в котором мы можем указать:
message {
this.outline = MessageOutline(null, "Outline text") this.style = MessageStyle.PRIMARY section { this.text("Primary message") } } После этого мы можем либо определить новую секцию методом section, либо поставить разделитель методом divide.В секции нам доступны:
message {
this.outline = MessageOutline(null, "Outline text") this.style = MessageStyle.PRIMARY section { this.header = "Section header" this.footer = "Section footer" this.text("Plain text") } divider() section { header = "This tag may indicate something not good" this.textWithTag("Text with tag", "error tag", MessageStyle.ERROR) } section { this.text("Plain text just to fill some space.") this.textWithImage("Text with image", "https://www.jetbrains.com/space/img/feedback-section/video-preview.png") } section { this.textWithIcon("Text with icon", "", MessageStyle.WARNING) } section { header = "Fields" this.fields { this.field("field1", "value1") this.field("field2", "value2") this.field("field3", "value3") this.field("field4", "value4") } this.controls { this.button("Disabled button without any action", PostMessageAction("", ""), disabled = true) } } } Осталось реализовать команду interaction и соединить все вместе. val interactive = Command(
"interactive", "Displaying available message interactive elements" ) { payload -> val context = InteractiveContext.from(payload) val arguments = payload.commandArguments() if (arguments == null || arguments.isBlank()) { return@Command printToChat(context, message { section { text("Specify one of the selected ui elements:\nbutton") } }) } printToChat(context, message { section { header = "Available message interactive elements" controls { when (arguments) { "button" -> { val primaryAction = PostMessageAction("ButtonPrimaryActionId", "InteractiveButtonPayloadPrimary") button("Primary", primaryAction, MessageButtonStyle.PRIMARY) val secondaryAction = PostMessageAction("ButtonSecondaryActionId", "InteractiveButtonPayloadSecondary") button("Secondary", secondaryAction, MessageButtonStyle.SECONDARY) val regularAction = PostMessageAction("ButtonRegularActionId", "InteractiveButtonPayloadRegular") button("Regular", regularAction, MessageButtonStyle.REGULAR) val dangerAction = PostMessageAction("ButtonDangerActionId", "InteractiveButtonPayloadDanger") button("Danger", dangerAction, MessageButtonStyle.DANGER) } } } } }) } private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessagePayload(payload: MessagePayload) {
Commands.list.find { it.name == payload.command() }?.action?.invoke(payload) ?: return call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.OK) } private suspend fun PipelineContext<*, ApplicationCall>.processChatbotListCommandsPayload(payload: ListCommandsPayload) {
call.respondText(ObjectMapper().writeValueAsString(Commands.commands), ContentType.Application.Json) } private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessageActionPayload(payload: MessageActionPayload) {
printToChat(userContext(payload), message { section { text(payload.actionId) } }) call.respond(HttpStatusCode.OK) } ИтогоПочти все, что может сделать пользователь используя Space через пользовательский интерфейс, можно сделать и через предоставленный Api(если на то будут предоставлены разрешения). На самом деле у Space гораздо больше способов расширения функционала. Мы рассмотрели лишь самый продвинутый уровень. Большую часть остальных расширений можно настроить имея вообще минимальные навыки программирования. Вот остальные способы расширения функционала платформы:
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:22
Часовой пояс: UTC + 5