JetBrains Space ヘルプ

(Kotlin) リンクを展開する方法

リンクプレビューまたはリンク展開は、ユーザーが実際にリンクをたどらなくても、リンクの背後にあるコンテンツを確認できる優れた機能です。プレビューは Space UI に直接表示され、外部リソースから取得したコンテンツが含まれています。

Space では、チャットメッセージ、ドキュメント、課題の説明、コミットおよびコードレビューのタイトルなど、さまざまなコンテキストでリンクプレビューを表示できます。プレビューを構築するために、Space はページのソーシャルメタタグからの情報を使用します。要件の詳細については、このページを参照してください。

しかし、リンクの背後にあるページにソーシャルタグが提供されていないか、ユーザー認証が必要な場合はどうなるでしょうか ? もちろん、Space ではプレビューを表示できませんが、Space アプリケーションを作成することでこれを修正できます。アプリケーションの目的は、これらすべてをカーテンの裏で実行すること、つまり、外部システムで認証し、必要なコンテンツを取得して、それを Space に送信することです。

何をしたらいいでしょう

Slack リンクを展開するアプリケーションを作成します。参照された Slack メッセージのプレビューは、Space チャット、ドキュメント、リンク展開をサポートするその他の場所に表示されます。これは読み取り専用のプレビューとなり、Slack で直接応答したり、メッセージスレッドに移動したりすることはできません。このチュートリアルを段階的に進めるのではなく、結果のコードだけを見たい場合でも、まったく課題ありません。これがソースコード(英語)です。

ステップ 1. Ktor プロジェクトを作成する

まず、Ktor アプリケーションを作成する必要があります。その方法については、(Kotlin) チャットボットの作成方法チュートリアルのステップ 1 に詳細な手順が記載されています。次の点に注意して、次の手順に従います。

  • クトルバージョン。このチュートリアルでは、Ktor 2.0.1 の使用を前提としています。他のバージョンを選択できますが、一部の Ktor 関連のインポートとメソッド呼び出しはそれに応じて更新する必要がある場合があります。

  • Space SDK。ここで説明されているように、必ず最新のバージョンを選択してください。

  • このアプリケーションのテストは作成しないため、build.gradle ファイルからすべての testImplementation 依存関係をクリーンアップし、プロジェクトルートの src/test フォルダーを削除します。

  • アプリケーションには Java 用 Slack SDK が必要なので、必ず次の依存関係を build.gradledependencies セクションに追加してください。: implementation "com.slack.api:slack-api-client:1.18.0"

結果の build.gradle ファイルは次のようになります。

plugins { id 'application' id 'org.jetbrains.kotlin.jvm' version '1.7.10' } group "com.linkpreviews" version "0.0.1" mainClassName = "com.linkpreviews.ApplicationKt" repositories { mavenCentral() maven { url "https://maven.pkg.jetbrains.space/public/p/space/maven" } } dependencies { implementation "io.ktor:ktor-server-core:$ktor_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0" // we use version 93759, but when you read this tutorial // a newer version might be already available implementation "org.jetbrains:space-sdk-jvm:93759-beta" implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-cio:$ktor_version" implementation "com.slack.api:slack-api-client:1.18.0" }

ステップ 2. トンネリングサービスを実行する

アプリケーションの開発中に、Space サーバーがアクセスできるようにアプリケーションを公開する必要があります。これを可能にする最も簡単な方法は、トンネリングサービスを使用することです。ngrok サービスを使用してトンネルを設定するプロセスについては、(Kotlin) チャットボットの作成方法チュートリアルのステップ 3 で詳しく説明されています。

ステップ 3. Space にアプリケーションを登録する

アプリケーションは Slack と Space の両方と通信するため、両方の側で登録する必要があります。まずは Space から始めましょう。まだ学習中なので、簡単な方法で単一組織アプリケーションを作成します。これは、アプリケーションを JetBrains マーケットプレイスを通じて他の Space 組織に配布できないことを意味します。はい、欠点のように聞こえますが、一方で、これにより、Space UI でアプリケーションを手動で構成できるようになります。複数組織アプリケーションの場合は、アプリケーションのコードに構成ロジックを実装する必要があります。

  1. Space インスタンスを開きます。

  2. メインメニューで、 API Playground 拡張、インストール済みの順にクリックします。

  3. 新しいアプリをクリックし、アプリケーションに一意の名前を付けます。Slack が展開作成をクリックし、次にアプリケーション設定に移動しますをクリックします。

  4. エンドポイントタブを開きます。

  5. エンドポイント URL で、Space からの呼び出しを受信するアプリケーションのエンドポイントを指定します。ngrok トンネルのパブリック URL に /api/space を追加して、この URL を作成します。また、このタブの認証方法として公開鍵が選択されていることを確認してください。保存をクリックします。

  6. 認証」タブを開きます。クライアント資格情報フローはデフォルトで有効になっていることに注意してください。私たちのアプリケーションは、このフローを使用して、それ自体に代わって Space で認証を行います。

  7. アプリケーションの資格情報セクションからクライアント ID 値とクライアントシークレット値をコピーします。

  8. アプリケーションのソースコードで、Credentials.kt ファイルを作成し、次のコードを追加します (URL、クライアント ID、シークレットの値を前の手順の値に置き換えます)。

    package com.linkpreviews import space.jetbrains.api.runtime.SpaceAppInstance import space.jetbrains.api.runtime.SpaceAuth import space.jetbrains.api.runtime.SpaceClient import space.jetbrains.api.runtime.ktorClientForSpace val spaceAppInstance = SpaceAppInstance( clientId = "<Space app client id>", clientSecret = "<Space app client secret>", spaceServerUrl = "https://<your-Space-org>.jetbrains.space" ) private val spaceHttpClient = ktorClientForSpace() val spaceClient by lazy { SpaceClient(spaceHttpClient, spaceAppInstance, SpaceAuth.ClientCredentials()) }

Space アプリケーションのセットアップは以上です。Slack 側に移りましょう。

ステップ 4. Slack でアプリケーションを構成する

  1. ブラウザーで https://api.slack.com/apps(英語) に移動します。ワークスペースにサインインします。

  2. ログインしたら、新しいアプリを作成するをクリックします。

  3. ダイアログで、アプリマニフェストからオプションを選択します。

  4. 必要な Slack ワークスペースを選択します。

  5. YAML 形式を選択し、次のマニフェストを指定します。your-ngrok-url を ngrok トンネルのパブリックアドレスに置き換えます。

    display_information: name: Slack unfurls in Space oauth_config: redirect_urls: - https://your-ngrok-url/slack/oauth/callback settings: token_rotation_enabled: true
  6. アプリケーション設定を確認し、作成をクリックします。

  7. 設定ページで、クライアント ID とクライアントシークレットを含むアプリの認証情報セクションまで下にスクロールします。アプリケーションが Slack API と通信するためにこれらの値が必要となるため、これらの値をコピーします。

  8. アプリケーションのソースコードで、次のコードを Credentials.kt に追加します (URL、クライアント ID、シークレットの値を前の手順の値に置き換えます)。

    // ... other imports import com.slack.api.Slack // ... Space credentials and client related code object SlackWorkspace { val clientId = "<Slack app client id>" val clientSecret = "<Slack app client secret>" val domain = "<your Slack domain>.slack.com" } // list of Slack permissions needed to fetch link preview contents val slackPermissionScopes = listOf( "channels:history", "groups:history", "channels:read", "groups:read", "team:read", "users:read", "usergroups:read" ) val slackApiClient = Slack.getInstance()

ステップ 5. Space で権限をリクエストする

Space 組織に追加されたアプリケーションが最初に行うべきことは、アプリケーション自体を構成し、必要なすべてのアクセス許可を要求することです。ステップ 3 では、単一組織アプリを作成することについて説明しました。これは、Space のアプリケーション設定で必要な権限を直接リクエストできることを意味します。それでも、複数組織アプリケーションがどのように作成されるのかを覗いてみて、アプリケーションコードから直接アクセス許可をリクエストすることを提案します。

通常、アプリケーションは、Space から InitPayload を受信した後の初期化フェーズ中にアクセス許可を必要とする必要があります。ただし、私たちのアプリは 100% マルチ組織アプリケーションではないため、init コマンドをアプリケーションチャットチャネルに送信して初期化します。

  1. 次のコードを含む CommandInit.kt ファイルを作成します。

    package com.linkpreviews import space.jetbrains.api.runtime.resources.applications import space.jetbrains.api.runtime.resources.chats import space.jetbrains.api.runtime.types.* suspend fun commandInit(spaceUserId: String) { // request global permission to provide link previews spaceClient.applications.authorizations.authorizedRights.requestRights( ApplicationIdentifier.Me, GlobalPermissionContextIdentifier, listOf("Unfurl.App.ProvideAttachment") ) // allow providing previews for the specific domain (our Slack workspace) // you can specify just slack.com here - Space will expand it to subdomains spaceClient.applications.unfurls.domains.updateUnfurledDomains(listOf(SlackWorkspace.domain)) // send “OK” to inform the user that init was successful spaceClient.chats.messages.sendMessage( channel = ChannelIdentifier.Profile(ProfileIdentifier.Id(spaceUserId)), content = message { section { text("ok") } } ) }

    上記のコードは、添付ファイルのリンクのプレビュー (チャットメッセージの横またはツールヒントに表示されるコンテンツ) の許可を要求します。プレビューについて詳しくはこちら

  2. ここで、commandInit 関数を Space コマンドを処理する Ktor HTTP エンドポイントに接続し、すぐに使用できる Space クライアントをそれに渡す必要があります。次のコードを含む Routes.kt ファイルを作成しましょう。

    package com.linkpreviews import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.util.* import space.jetbrains.api.runtime.helpers.readPayload import space.jetbrains.api.runtime.helpers.verifyWithPublicKey import space.jetbrains.api.runtime.types.ChatMessage import space.jetbrains.api.runtime.types.MessagePayload fun Routing.api() { // register route listening to POST requests at api/space post("api/space") { // read request body val body = call.receiveText() // verify that the request comes from Space val signature = call.request.header("X-Space-Public-Key-Signature") val timestamp = call.request.header("X-Space-Timestamp")?.toLongOrNull() if (signature.isNullOrBlank() || timestamp == null || !spaceClient.verifyWithPublicKey( body, timestamp, signature ) ) { call.respond(HttpStatusCode.Unauthorized) return@post } // read and process the payload when (val payload = readPayload(body)) { // determine message payload type is MessagePayload -> { // determine the init command if ((payload.message.body as? ChatMessage.Text)?.text == "init") { commandInit(payload.userId) } call.respond(HttpStatusCode.OK, "") } } } }
  3. 最後に、Routing.api() 機能を使用するようにルーティング機能を設定しましょう。plugins ディレクトリ内の Routing.kt ファイルを開き、以下のように編集します。

    package com.linkpreviews.plugins import io.ktor.server.application.* import io.ktor.server.routing.* fun Application.configureRouting() { routing { api() } }

新しい Slack リンクがチャットチャネル、ドキュメント、課題の説明に表示されたときに、アプリケーションに通知する必要があります。このようなリンクが投稿されるたびに、Space はそれを特別な展開キューに保持し、通知を送信します。Space API を使用してキューをポーリングする方法を見てみましょう。

Space からの通知は、Space がアプリケーションの同じ /api/space エンドポイントに送信できる別の種類のペイロードにすぎません。この新しいペイロードタイプを処理するには、Routes.kt ファイル内の when ステートメントを展開しましょう。

  1. まず、最後の etag 値を追跡するための新しいインポートとグローバル変数を追加します。

    import space.jetbrains.api.runtime.resources.applications import space.jetbrains.api.runtime.types.NewUnfurlQueueItemsPayload import space.jetbrains.api.runtime.types.ProfileIdentifier private var lastEtag: Long? = null

    イータグって何? Etag は、展開キュー内のアイテムの位置を定義する番号です。その数は確実に増加していることが保証されています。実際のアプリケーションでは、永続ストレージに保存しますが、サンプルアプリケーションでは、メモリ内変数として保存しても課題ありません。

  2. NewUnfurlQueueItemsPayload タイプの処理を when ステートメントに追加します。

    when (val payload = readPayload(body)) { // ... // handle notifications about new items in the unfurling queue is NewUnfurlQueueItemsPayload -> { val queueApi = spaceClient.applications.unfurls.queue // get queue items in batches of 100 items at a time var queueItems = queueApi.getUnfurlQueueItems(lastEtag, batchSize = 100) while (queueItems.isNotEmpty()) { queueItems.forEach { item -> // 'authorUserId' - is an ID of the user who posted a link (instance of 'ProfileIdentifier.Id') // Checking that a link points to a Slack message and link author is a Space user. // The reason is that we want to query Slack API on behalf of a user, not an application. // Thus, the app has to map each Space user to their personal token in Slack. if (item.authorUserId != null && item.target.startsWith("https://${SlackWorkspace.domain}/archives")) { val spaceUserId = (item.authorUserId as? ProfileIdentifier.Id)?.id ?: error("ProfileIdentifier.Id") provideUnfurlContent(item, spaceUserId) } } // save our current position in the queue lastEtag = queueItems.last().etag queueItems = queueApi.getUnfurlQueueItems(lastEtag, batchSize = 100) } } }

    provideUnfurlContent 関数を作成する必要があるため、このコードはまだコンパイルされないことに注意してください。これは次のステップで行います。

ステップ 7. Space ユーザーに Slack での認証を要求する

次に、アプリケーションが Space ユーザーに Slack での認証を要求し、ユーザーに代わってリンクプレビューを提供できるようにする方法を見てみましょう。

  1. unfurls.kt ファイルを作成し、次のコードをそれに追加します。NavigateUrlActionyour-ngrok-hostname を ngrok トンネルのホスト名に置き換えます。

    package com.linkpreviews import io.ktor.http.* import space.jetbrains.api.runtime.helpers.unfurl import space.jetbrains.api.runtime.resources.applications import space.jetbrains.api.runtime.types.* import java.util.concurrent.ConcurrentHashMap // Data class that stores a pair of Slack API tokens. // We use [[[Slack token rotation|https://api.slack.com/authentication/rotation]]]. // The first token is used to fetch data from Slack. // The second token is used to refresh the first token. data class SlackUserTokens(val accessToken: String, val refreshToken: String) // Each Space user (link author) has an associated pair of Slack tokens. // In real applications, store this map in a persistent storage. val slackUserTokens = ConcurrentHashMap<String, SlackUserTokens>() suspend fun provideUnfurlContent(item: ApplicationUnfurlQueueItem, spaceUserId: String) { // Check whether the link that we have conforms to the format of the Slack message link val url = Url(item.target) val parts = url.encodedPath.split('/').dropWhile { it != "archives" }.drop(1) val channelId = parts.firstOrNull() val messageId = parts.drop(1).firstOrNull() if (channelId == null || messageId == null) return // If the Slack tokens pair is missing for a given Space user (link author), // request this user to authenticate in Slack. var tokens = slackUserTokens[spaceUserId] ?: run { requestAuthentication(item, spaceUserId) return } // TODO - fetch data from Slack and provide the link preview } // The authentication request is a pseudo link preview that appears in the Space UI // in the same position as a regular link preview but is visible only to the message author. suspend fun requestAuthentication(item: ApplicationUnfurlQueueItem, spaceUserId: String) { spaceClient.applications.unfurls.queue.requestExternalSystemAuthentication( item.id, // use the message constructor to create the message unfurl { section { text("Authenticate in Slack to get link previews in Space") controls { button( "Authenticate", NavigateUrlAction( // replace it with your ngrok tunnel public address "https://<your-ngrok-hostname>/slack/oauth?user=$spaceUserId", withBackUrl = true, openInNewTab = false ) ) } } } ) }

    現在、外部システム認証リクエストは Space チャットでのみサポートされていることに注意してください。詳細

ステップ 8. Slack での認証フローの処理

ユーザーがリクエスト内の認証するボタンをクリックすると、ブラウザーはアプリケーションのエンドポイントにリダイレクトされます。これはボタンに取り付けられた NavigateUrlAction によって実現されます。次に、アプリケーションがこのリダイレクトをどのように処理するかを見てみましょう。

  1. 次の行を Routes.kt ファイルの末尾、Routing.api() 呼び出しの外側に追加します。

    // key - generated nonce, value - Space user id val oAuthSessions = ConcurrentHashMap<String, OAuthSession>() data class OAuthSession(val spaceUserId: String, val backUrl: String)
  2. Routes.kt ファイルで、api 関数に渡されるラムダにルートを 1 つ追加します。このコードは、Slack のリダイレクト URL を提供します。

    // the endpoint expects two query string parameters: 'user' and 'backUrl' get("slack/oauth") { // 'user' is a Space user ID specified in 'NavigateUrlAction' val spaceUserId = call.parameters.get("user") ?: run { call.respond(HttpStatusCode.BadRequest, "user parameter expected") return@get } // 'backUrl' points to a page in Space where the user was before being redirected. // The app will return the user back to this page after authentication. // Space sends 'backUrl' as we set 'withBackUrl=true' in 'NavigateUrlAction'. val backUrl = call.parameters.get("backUrl") ?: run { call.respond(HttpStatusCode.BadRequest, "backUrl parameter expected") return@get } val flowId = generateNonce() oAuthSessions[flowId] = OAuthSession(spaceUserId, backUrl) val authUrl = with(URLBuilder("https://${SlackWorkspace.domain}/oauth/v2/authorize")) { parameters.apply { append("client_id", SlackWorkspace.clientId) append("user_scope", slackPermissionScopes.joinToString(",")) append("state", flowId) // replace it with your ngrok tunnel public address append("redirect_uri", "https://<your-ngrok-hostname>/slack/oauth/callback") } build() } call.respondRedirect(authUrl.toString()) }

    このエンドポイントの基本的なフローは単純です。ノンスを生成し、Space ユーザー ID およびバック URL とともにメモリ内マップに記憶し、OAuth フローの実行を担当する Slack エンドポイントにユーザーをさらにリダイレクトします。リダイレクト中に、Slack のアプリケーションのクライアント ID、生成された nonce (後で Slack からのコールバックを受信したときに検証します)、およびアプリケーションがリンクプレビュー用のデータをフェッチするために必要な Slack 権限のリストを提供します。その後、指定したリダイレクト URL を使用して、Slack がアプリケーションにコールバックするのを待つだけです。

    ここでは、 Slack の OAuth フローの詳細を確認できます。ユーザーに代わって Slack API を呼び出すため、ユーザートークンを取得し、権限スコープのリストを渡すために user_scope パラメーターを使用します。

    ここで state パラメーターは 2 つの目的を果たします。

    • 認証リクエスト (Space ユーザー ID とバック URL) に関する追加のコンテキストを永続化し、フローの補完後にそれを使用できるようにします。

    • アプリケーションのコールバック URL へのユーザー要求が、悪意のある当事者からのものではなく、正当な Slack 認証フローからのものであることを検証することで、CSRF 攻撃から保護します。state パラメーターのセキュリティ面の詳細については、こことここを参照してください。

  3. OAuth フローを完了するには、認証コードを使用して Slack からのコールバックを処理する必要があります。api 関数に渡されるラムダにこのルートを追加しましょう。次のコードを Routes.kt ファイルに追加します。

    get("slack/oauth/callback") { val flowId = call.parameters.get("state") ?: run { call.respond(HttpStatusCode.BadRequest, "state parameter expected") return@get } val session = oAuthSessions.get(flowId) ?: run { call.respond(HttpStatusCode.Unauthorized, "invalid auth session") return@get } val code = call.parameters.get("code") ?: run { call.respond(HttpStatusCode.BadRequest, "code parameter expected") return@get } val slackUserToken = requestOAuthToken(code) if (slackUserToken == null) { call.respond(HttpStatusCode.Unauthorized, "could not fetch OAuth token from Slack") return@get } slackUserTokens[session.spaceUserId] = slackUserToken spaceClient.applications.unfurls.queue.clearExternalSystemAuthenticationRequests( ProfileIdentifier.Id(session.spaceUserId) ) call.respondRedirect(session.backUrl) }
  4. このハンドラーで使用される requestOAuthToken 関数も実装しましょう。これを Routes.kt ファイルの末尾に追加します。

    fun requestOAuthToken(code: String): SlackUserTokens? { val response = slackApiClient.methods() .oauthV2Access { it.clientId(SlackWorkspace.clientId).clientSecret(SlackWorkspace.clientSecret).code(code) } return response?.takeIf { it.isOk } ?.authedUser?.takeIf { it.accessToken != null && it.refreshToken != null } ?.let { SlackUserTokens(it.accessToken, it.refreshToken) } }

    Slack からコールバックを受信すると、リクエストパラメーターからセッション ID と認証コードを抽出します。次に、requestOAuthToken 関数は Slack API を使用して、Slack トークンのペアの認証コードを交換します。ステップ 4 の Slack アプリケーション設定で token_rotation_enabled: true フラグを指定しているため、有効期間の短いアクセストークンと有効期間の長いアクセストークンの両方を取得することが期待されます。

ステップ 9. ユーザーに代わってプレビューコンテンツを提供する

Slack トークンを使用して、プレビューコンテンツを取得してみましょう。ステップ 7 では、provideUnfurlContent 関数内に TODO コメントを作成しました。このギャップを埋める時期が来ています。

  1. まず、Slack からデータをフェッチするためのいくつかの単純なヘルパーを実装しましょう。Unfurls.kt ファイルを開き、次のインポートを追加します。

    import com.slack.api.methods.SlackApiException import com.slack.api.model.Message
  2. 以下のコードをファイルに追加します。これらのメソッドはすべて、Slack API を使用してデータをフェッチし、応答モデルを変換します。これらのメソッドを使用すると、実際にリンクプレビューを提供する provideUnfurlContent 関数を作成できます。

    fun fetchMessage(channelId: String, messageId: String, threadTs: String?, accessToken: String): Message? { return if (threadTs != null) { // https://api.slack.com/methods/conversations.replies slackApiClient.methods(accessToken) .conversationsReplies { it.channel(channelId).latest(threadTs).ts(messageIdToTs(messageId)).inclusive(true).limit(1) } ?.messages?.singleOrNull() } else { // https://api.slack.com/methods/conversations.history slackApiClient.methods(accessToken) .conversationsHistory { it.channel(channelId).latest(messageIdToTs(messageId)).inclusive(true).limit(1) } ?.messages?.singleOrNull() } } // https://api.slack.com/methods/users.info fun fetchAuthorName(accessToken: String, slackUserId: String): String { return slackApiClient.methods(accessToken).usersInfo { it.user(slackUserId) }?.user?.profile?.let { it.displayName?.takeUnless { it.isBlank() } ?: it.realName?.takeUnless { it.isBlank() } } ?: slackUserId } // https://api.slack.com/methods/conversations.info fun fetchChannelName(accessToken: String, channelId: String): String { return slackApiClient.methods(accessToken) .conversationsInfo { it.channel(channelId) }?.channel?.name?.let { "[#$it](https://${SlackWorkspace.domain}/archives/$channelId)" } ?: channelId } // Converts Slack message id as it's present in the message url to // the timestamp value for usage in Slack API requests private fun messageIdToTs(messageId: String) = messageId.removePrefix("p").let { it.dropLast(6) + "." + it.drop(it.length - 6) }
  3. 以下のコードを provideUnfurlContent 関数に追加します。

    suspend fun provideUnfurlContent(item: ApplicationUnfurlQueueItem, spaceUserId: String) { // Extract a message, channel, and optional thread IDs from the URL. val url = Url(item.target) val parts = url.encodedPath.split('/').dropWhile { it != "archives" }.drop(1) val channelId = parts.firstOrNull() val messageId = parts.drop(1).firstOrNull() if (channelId == null || messageId == null) return var tokens = slackUserTokens[spaceUserId] ?: run { requestAuthentication(item, spaceUserId) return } val threadTs = url.parameters["thread_ts"] // Fetch the message itself, author and channel names from Slack val message = try { fetchMessage(channelId, messageId, threadTs, tokens.accessToken) // Catch Slack API exceptions, specifically token expiration. } catch (ex: SlackApiException) { // If token is expired, refresh the short-lived token // by performing a call to https://api.slack.com/methods/oauth.v2.access if (ex.error.error == "token_expired") { val response = slackApiClient.methods().oauthV2Access { it .clientId(SlackWorkspace.clientId) .clientSecret(SlackWorkspace.clientSecret) .grantType("refresh_token") .refreshToken(tokens.refreshToken) } val accessToken = response.accessToken ?: response.authedUser?.accessToken val newRefreshToken = response.refreshToken ?: tokens.refreshToken if (accessToken != null) { tokens = SlackUserTokens(accessToken, newRefreshToken) // Save new tokens to our in-memory storage. slackUserTokens[spaceUserId] = SlackUserTokens(accessToken, newRefreshToken) // Fetch the message one more time, but this time with updated tokens. fetchMessage(channelId, messageId, threadTs, tokens.accessToken) } else null } else null } if (message == null) return val channelLink = if (threadTs != null) { // Converting message timestamp value to id for the message link // (an operation opposite to `messageIdToTs`) val parentMessageId = "p" + threadTs.filterNot { it == '.' } "https://${SlackWorkspace.domain}/archives/$channelId/$parentMessageId" } else { fetchChannelName(tokens.accessToken, channelId) } val authorName = fetchAuthorName(tokens.accessToken, message.user) // Build link preview with message constructor DSL val content: ApplicationUnfurlContent.Message = unfurl { outline( MessageOutline( ApiIcon("slack"), "*$authorName* in $channelLink" ) ) section { text(message.text) text("[View message](${item.target})") } } // Post the preview to Slack including queue item ID spaceClient.applications.unfurls.queue.postUnfurlsContent( listOf(ApplicationUnfurl(item.id, content)) ) }

ステップ 10. アプリケーションの実行

アプリケーションを実行してリンクプレビューの動作を確認する準備がほぼ整いました。ただし、アプリケーションを初めて実行するには、init コマンドによる初期化が必要であることを忘れないでください。初期化中に、アプリケーションは Space 組織に必要なすべての許可を要求します。組織管理者はリクエストを承認する必要があります。

  1. Gradle run タスクを実行してアプリケーションを開始します。コマンドプロンプトで ./gradlew run を実行するか、ガターのアイコンを使用して Application.ktmain 関数を実行します。

  2. Space を開き、左側のサイドバーにある検索アイコンをクリックし、アプリケーション名を入力します。Space がアプリケーションを見つけたら、それをクリックしてアプリケーションのチャットチャネルに移動します。

  3. チャットチャネルで「init 」と入力し、このメッセージをアプリケーションに送信します。アプリケーションは ok で応答する必要があります。

  4. メインメニューで、 API Playground 拡張、インストール済みの順にクリックします。アプリケーションをクリックして設定ページに移動します。

  5. 許可タブには、保留中の外部展開を添付ファイルとして提供する権限が 1 つあります。承認してください。

  6. 展開」タブに切り替えます。ここで、アプリケーションは Slack リンクをリッスンする許可を要求しています。それも承認してください。

ステップ 11. アプリケーションを実際に動かしてみてください。

  1. Slack のパブリックチャネルまたはプライベートチャネルに移動し、メッセージを見つけます。このメッセージへのリンクをコピーします。

  2. Space に移動し、任意のチャットチャネルを開き、コピーしたリンクを貼り付けてメッセージを送信します。

  3. しばらくすると、チャットメッセージに認証リクエストが表示されます。認証するをクリックして Slack に移動します。

  4. Slack ワークスペースにサインインします。Slack は、Space の最後のページにリダイレクトします。

  5. Space チャットへの Slack メッセージリンクを含むメッセージをもう 1 つ送信します。しばらくすると、元の Slack メッセージの内容を表示するプレビューが表示されます。

チャットメッセージからリンクプレビューを削除する場合は、プレビューの上にマウスを置き、十字アイコンをクリックします。

取り除く

アプリケーションが提供する展開を Space で実装するために何が必要かを概観してみましょう。

  • Space アプリケーションは Unfurl.App.ProvideAttachment 権限スコープを要求します。また、処理する展開ドメインとパターンのリストも要求します。ここでは展開パターンについては説明しませんでしたが、基本的には外部リンクと同じです。権限のスコープ、ドメイン、パターンは、Space 組織管理者によって承認される必要があります。

  • Space は、アプリケーションの展開ドメインを指すリンク、または展開パターンに一致する単語を検出すると、展開キューにアイテムを作成し、アプリケーションに通知を送信します。

  • アプリケーションは、Space API を使用してこのキューをポーリングできます。通知を受信すると、アプリケーションはキューから項目をクエリすることが期待されます。アイテムがこのキューに存在するのは 30 分だけです。

  • 各キュー項目には etag (符号なし 64 ビット単調増加整数) が割り当てられています。アプリケーションは、永続ストレージに保存することで最後に処理された etag を追跡し、キューから次のアイテムバッチをクエリするときにそれを Space API メソッドに提供することが期待されます。

  • アプリケーションは、リンクプレビューをすぐに提供するか、ユーザーに外部システムでの認証を要求するという 2 つの可能な方法のいずれかでキューアイテムを処理できます。ヒント: アプリケーションに新しい項目用の独自のキューを用意し、バックグラウンドで処理することをお勧めします。

  • アプリケーション自体に代わって外部システムで認証することは簡単ですが、リンクを投稿したユーザーに代わって認証する場合と比べて安全性は低くなります。

  • アプリケーションが外部システムのユーザートークンを使用する場合、アプリケーションは認証フローを処理し、取得したユーザートークンを安全に (これは重要です ! ) 保存する必要があります。リフレッシュトークンを含むフローを使用することをお勧めします。

  • リンクプレビューコンテンツを提供するには、アプリケーションは、アイテムのリストを取得し、元のアイテム ID と展開されたコンテンツをペアにする Space API メソッドを呼び出す必要があります (これによりバッチ処理が可能になります)。展開されたコンテンツを提供するには、いくつかの形式が考えられます。

関連ページ:

リンク展開

チュートリアル (Kotlin) リンクを展開する方法、サンプル Slack リンクを展開するサンプルアプリケーション、デフォルトでは、ユーザーがチャットチャネルに外部リンクを投稿すると、Space はリンクを展開します。つまり、リンクの背後にページのプレビューが表示されます。Space は、チャットメッセージだけでなく、ドキュメント、課題の説明、コミットとコードレビューのタイトルでもリンク展開をサポートします。Space がリンクプレビューを構築できるようにするには、リンクの背後にあるページが次の...

(Kotlin) チャットボットの作成方法

チャットボットとは何ですか ? これは、独自のチャットチャネルで Space ユーザーと通信する Space アプリケーションです。最低限実行可能なボットは次のことを行う必要があります。ユーザーがチャネルに (スラッシュ) を入力すると、使用可能なコマンドのリストが表示されます。少なくとも 1 つのコマンドを指定します。ユーザーがこのコマンドをチャネルに送信した後、ボットは何かを実行してからメッセージで応答する必要があります。何をしたらいいでしょう:もちろん、初めてのチャットボットでも ! 早速...

アプリケーションの開発

アプリケーションは、Space の機能を拡張する主な方法です。アプリケーションは、以下を通じて Space と対話できる外部サーバー側サービスまたはクライアント側プログラム (JavaScript、デスクトップ、モバイル) です。Space SDK、Space HTTP API、Space とアプリケーション間のインタラクション:空間とアプリケーションは双方向で通信できます。アプリケーションはデータを Space に送信できます。例: チャットチャネルにメッセージを送信し、プロジェクトやチーム...

(Kotlin) インタラクティブ UI をメッセージに追加する方法

前提条件:次のように仮定します。「リマインダー」ボットのソースコードがあります。チャットボットの作成方法チュートリアルを完了しました。具体的には、環境をセットアップする手順を完了しました。ステップ 3. トンネリングサービスを実行する、ステップ 4. Space にチャットボットを登録する、何をしたらいいでしょう:このチュートリアルでは、(Kotlin) チャットボットの作成方法チュートリアルで作成した「リマインダー」ボットの機能を拡張します。具体的には、ボットメッセージの 1 つに UI...

(.NET) チャットボットの作成方法

チャットボットとは何ですか ? これは、独自のチャットチャネルで Space ユーザーと通信する Space アプリケーションです。最低限実行可能なボットは次のことを行う必要があります。ユーザーがチャネルに (スラッシュ) を入力すると、使用可能なコマンドのリストが表示されます。少なくとも 1 つのコマンドを指定します。ユーザーがこのコマンドをチャネルに送信した後、ボットは何かを実行してからメッセージで応答する必要があります。何をしたらいいでしょう:まずはチャットボットを作ってみましょう ! よ...