| コンポーネント | 役割 |
|---|---|
| Chat XDK | 暗号化、復号、署名、および秘密鍵の保存(安全な鍵バックアップまたは鍵 blob) |
| X API | 公開鍵、会話鍵、メッセージ、イベント — Python または TypeScript XDK 経由、または HTTPS とユーザーアクセストークン経由 |
前提条件
- 開発者アカウント と OAuth 2.0 用に構成されたアプリ
dm.read、dm.write、tweet.read、users.readを持つユーザーアクセストークン
1. 依存関係をインストールする
- Python
- TypeScript
- Rust
- Go
- C#
- Java
pip install chatxdk xdk
chatxdk です。chat_xdk としてインポートします。Python 3.10+ が必要です。npm install @xdevplatform/chat-xdk @xdevplatform/xdk
npm install juicebox-sdk # optional peer dependency — required for setup()/unlock() secure key backup
@xdevplatform/chat-xdk に同梱されています。ビルド手順は不要です。Node.js 18+ が必要です。[dependencies]
# chat-xdk-core is not yet on crates.io — use the git dependency
chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.4.0" }
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde_json = "1"
base64 = "0.22"
# Required until thrift 0.24 is released on crates.io
[patch.crates-io]
thrift = { git = "https://github.com/apache/thrift.git", rev = "deb36fa409849de45973b04ffc3ce49d277ca90a" }
go get github.com/xdevplatform/chat-xdk/go/chatxdk
dotnet add package XDevPlatform.ChatXdk
<dependency>
<groupId>com.x</groupId>
<artifactId>chatxdk</artifactId>
<version>0.4.0</version>
</dependency>
jna.library.path の設定は不要です。com.x.chatxdk からインポートします。JDK 17+ が必要です。- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk import Client
client = Client(access_token="YOUR_OAUTH2_USER_TOKEN")
import { Client } from '@xdevplatform/xdk';
const client = new Client({ accessToken: 'YOUR_OAUTH2_USER_TOKEN' });
let access_token = std::env::var("X_ACCESS_TOKEN")?;
let http = reqwest::blocking::Client::new();
let auth = format!("Bearer {access_token}");
accessToken := os.Getenv("X_ACCESS_TOKEN")
httpClient := &http.Client{Timeout: 30 * time.Second}
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("X_ACCESS_TOKEN"));
String accessToken = System.getenv("X_ACCESS_TOKEN");
HttpClient http = HttpClient.newHttpClient();
2. 既存の鍵で Chat XDK を初期化する
このステップでは、すでに持っている鍵をロードします — この identity が過去に初回セットアップを完了している場合に使用してください:- 安全な鍵バックアップ: 公開鍵レコードの
juicebox_configで SDK を構築し、パスコードでunlockして秘密鍵を復元します(たとえば新しいデバイスで)。 - 鍵 blob: 以前に
export_keysでエクスポートした blob をimport_keysに渡し、登録済みの鍵バージョンをあわせて指定します(Rust と Go ではこのバリアントをimport_keys_with_version/ImportKeysWithVersionと呼びます)。
set_identity(user_id, signing_key_version) を、あなたのユーザー ID とレコードの public_key_version とともに一度だけ呼び出します。これはセッション ID を保存します: 以降のすべての encrypt および prepare 呼び出しはこの ID として署名するため、呼び出しごとに送信者 ID や署名鍵バージョンを渡す必要はありません。
初めてセットアップする場合は? 同じ方法で SDK を構築しますが、unlock / import_keys はスキップし、ステップ 3 に進んで鍵の作成、バックアップ、登録を行ってください。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
import json
from chat_xdk import Chat
resp = client.chat.get_user_public_keys(
"YOUR_USER_ID",
public_key_fields=[
"public_key_version", "public_key", "signing_public_key",
"identity_public_key_signature", "juicebox_config",
],
)
record = resp.data[0]
signing_key_version = str(record["public_key_version"])
chat = Chat(json.dumps(record["juicebox_config"]))
chat.unlock("YOUR_PASSCODE") # recovers keys stored by setup() during first-time setup (step 3)
# Or load a key blob instead of secure key backup:
# chat.import_keys(blob, version=signing_key_version)
chat.set_identity("YOUR_USER_ID", signing_key_version)
import { createChat } from '@xdevplatform/chat-xdk';
const resp = await client.chat.getUserPublicKeys('YOUR_USER_ID', {
publicKeyFields: [
'public_key_version', 'public_key', 'signing_public_key',
'identity_public_key_signature', 'juicebox_config',
],
});
const record = resp.data[0];
const signingKeyVersion = String(record.public_key_version);
const chat = await createChat({
juiceboxConfig: JSON.stringify(record.juicebox_config),
getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId),
});
await chat.unlock('YOUR_PASSCODE');
chat.setIdentity('YOUR_USER_ID', signingKeyVersion);
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use chat_xdk_core::ChatCore;
let chat = ChatCore::new();
let blob = B64.decode(std::env::var("PRIVATE_KEYS_B64")?)?;
let signing_key_version = std::env::var("SIGNING_KEY_VERSION").unwrap_or_else(|_| "1".into());
chat.import_keys_with_version(&blob, &signing_key_version)?;
chat.set_identity("YOUR_USER_ID", &signing_key_version);
import "github.com/xdevplatform/chat-xdk/go/chatxdk"
chat := chatxdk.New()
defer chat.Close()
blob, err := chatxdk.Base64ToBytes(os.Getenv("PRIVATE_KEYS_B64"))
if err != nil {
log.Fatal(err)
}
signingKeyVersion := os.Getenv("SIGNING_KEY_VERSION")
if signingKeyVersion == "" {
signingKeyVersion = "1"
}
if err := chat.ImportKeysWithVersion(blob, signingKeyVersion); err != nil {
log.Fatal(err)
}
if err := chat.SetIdentity(myUserID, signingKeyVersion); err != nil {
log.Fatal(err)
}
using ChatXdk;
using var chat = new Chat();
var signingKeyVersion = Environment.GetEnvironmentVariable("SIGNING_KEY_VERSION") ?? "1";
chat.ImportKeys(Convert.FromBase64String(
Environment.GetEnvironmentVariable("PRIVATE_KEYS_B64")!), signingKeyVersion);
chat.SetIdentity(myUserId, signingKeyVersion);
import com.x.chatxdk.Chat;
String signingKeyVersion = Optional.ofNullable(System.getenv("SIGNING_KEY_VERSION")).orElse("1");
try (Chat chat = new Chat()) {
chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64")), signingKeyVersion);
chat.setIdentity(myUserId, signingKeyVersion);
}
export_keys / import_keys)を使用します。クライアントアプリでは通常安全な鍵バックアップ(パスコードを使った setup / unlock)を使用します。両方のパスについては Chat XDK リファレンスを参照してください。
独自の鍵を持ち込みますか?
import_keys は、Chat XDK の export_keys が生成する不透明な blob のみを受け付けます — これは鍵の完全な状態をバージョン付きで非公開にシリアライズしたものであり、未加工または PEM エンコードされた P-256 鍵ではありません。この blob を自分で構築することはできません: generate_keypairs(ステップ 3)で鍵を生成し、blob を一度エクスポートして、base64 エンコードで保存してください。手作りまたは改変された blob はインポートに失敗します。3. 鍵を作成して登録する(初回セットアップ)
ステップ 2 で既存の鍵をロードした場合は、このステップをスキップしてください。それ以外の場合、新しい identity の 1 回限りのセットアップでは 3 つのことを行います:- キーペアを作成する —
generate_keypairsが identity と署名のキーペアを生成します。 - 秘密鍵を保存する — パスコードで
setupすると安全な鍵バックアップに書き込まれます(クライアント)。またはexport_keysが返す鍵 blob を安全に保存します(サーバーとボット)。 - 公開鍵を登録する — 他のユーザーがあなた宛てに暗号化し、あなたの署名を検証できるよう、登録ペイロードを add-public-key エンドポイントに POST します。
set_identity を呼び出して、このセッションが新しい identity として署名するようにします。
すべてのバインディング(Python、TypeScript、Go、Rust、C#、Java)向けの、すぐに実行できるワンタイム登録スクリプトが
chat-xdk/examples にあります。新しい identity をオンボードするだけの場合は、以下のフローを手作りするのではなくそれらを利用してください。- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk.chat.models import AddUserPublicKeyRequest
registration = chat.generate_keypairs()
pk = registration.public_key
client.chat.add_user_public_key(
"YOUR_USER_ID",
AddUserPublicKeyRequest(
public_key={
"identity_public_key_signature": pk.identity_public_key_signature,
"public_key": pk.public_key,
"public_key_fingerprint": pk.public_key_fingerprint,
"registration_method": pk.registration_method,
"signing_public_key": pk.signing_public_key,
"signing_public_key_signature": pk.signing_public_key_signature,
},
version=registration.version,
generate_version=registration.generate_version,
),
)
chat.setup("YOUR_PASSCODE")
chat.set_identity("YOUR_USER_ID", str(registration.version or "1"))
const registration = chat.generateKeypairs();
const pk = registration.publicKey;
await client.chat.addUserPublicKey('YOUR_USER_ID', {
public_key: {
identity_public_key_signature: pk.identityPublicKeySignature,
public_key: pk.publicKey,
public_key_fingerprint: pk.publicKeyFingerprint,
registration_method: pk.registrationMethod,
signing_public_key: pk.signingPublicKey,
signing_public_key_signature: pk.signingPublicKeySignature,
},
version: registration.version,
generate_version: registration.generateVersion,
});
await chat.setup('YOUR_PASSCODE');
chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1'));
let registration = chat.generate_keypairs()?;
let body = serde_json::to_value(®istration)?;
let resp = http
.post(format!("https://api.x.com/2/users/{user_id}/public_keys"))
.header("Authorization", &auth)
.json(&body)
.send()?;
if !resp.status().is_success() {
anyhow::bail!("register keys: {}", resp.text()?);
}
let _blob = chat.export_keys()?; // store securely
let key_version = registration.version.clone().unwrap_or_else(|| "1".into());
chat.set_identity(&user_id, &key_version);
registration, err := chat.GenerateKeypairs()
if err != nil {
log.Fatal(err)
}
regJSON, _ := json.Marshal(registration)
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/users/"+userID+"/public_keys",
bytes.NewReader(regJSON))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
privateKeys, _ := chat.ExportKeys() // store securely
_ = privateKeys
keyVersion := "1"
if registration.Version != nil {
keyVersion = *registration.Version
}
if err := chat.SetIdentity(userID, keyVersion); err != nil {
log.Fatal(err)
}
var registration = chat.GenerateKeypairs();
var regJson = System.Text.Json.JsonSerializer.Serialize(registration);
using var content = new StringContent(regJson, Encoding.UTF8, "application/json");
using var regResp = await http.PostAsync(
$"https://api.x.com/2/users/{Uri.EscapeDataString(userId)}/public_keys", content);
regResp.EnsureSuccessStatusCode();
var blob = chat.ExportKeys(); // store securely
chat.SetIdentity(userId, registration.Version ?? "1");
var registration = chat.generateKeypairs();
String regJson = new ObjectMapper().writeValueAsString(registration);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/users/" + userId + "/public_keys"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(regJson))
.build();
HttpResponse<String> regResp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (regResp.statusCode() >= 300) {
throw new RuntimeException("register keys: " + regResp.body());
}
byte[] blob = chat.exportKeys(); // store securely
chat.setIdentity(myUserId, registration.version != null ? registration.version : "1");
安全な鍵バックアップには強力なパスコードを使用してください。パスコードを紛失したり、保護されていない鍵 blob を失うと、過去のメッセージの復号ができなくなる可能性があります。
4. 会話鍵をセットアップする
prepare_conversation_key_change を、すべての参加者の identity 公開鍵とともに呼び出します。送信者 ID はステップ 2 で設定したセッションから取得されます。1 回の呼び出しで新しい会話鍵を生成し、各参加者向けに暗号化し、変更に署名します。結果を add conversation keys エンドポイント(POST /2/chat/conversations/{id}/keys)に POST します — ボディには conversation_key_version、conversation_participant_keys(SDK の encrypted_key → API の encrypted_conversation_key)、および action_signatures が必要です(必須;API は署名がないと呼び出しを拒否します)。送信用に未加工の会話鍵を保持します。
レスポンスは、正規の会話 ID(data.conversation_id — 1:1 の場合はハイフンで結合されたペア、グループの場合は g プレフィックス付き ID)と、鍵変更の data.sequence_id を返します。以降のリクエストでは、クライアント側で再構築するのではなく、返された ID を使用してください。同じ呼び出しは後で鍵をローテーションするためにも使えます: 既存の会話 ID を prepare_conversation_key_change に渡し、新しい鍵バージョンで POST します。会話鍵が漏洩したと疑われる場合はローテーションしてください — ローテーションは将来のメッセージのみを保護します;以前の鍵バージョンで暗号化されたメッセージは、そのバージョンを保持している人にとっては引き続き読み取り可能です。
ラップする前に取得した鍵を検証してください。
prepare_conversation_key_change は、渡された任意の公開鍵に対して新しい会話鍵を暗号化します。各取得済みレコードに対してまず verify_key_binding(identity, signing, signature) でチェックし(public-keys API のレコードの public_key、signing_public_key、identity_public_key_signature フィールドを渡します)、置き換えられた identity 鍵が会話鍵を受け取れないようにしてください。- Python
- TypeScript
- Rust
- Go
- C#
- Java
def public_key_input(user_id: str) -> dict:
r = client.chat.get_user_public_keys(
user_id, public_key_fields=["public_key_version", "public_key"]
).data[0]
return {"user_id": user_id, "public_key": r["public_key"], "key_version": r["public_key_version"]}
prepared = chat.prepare_conversation_key_change(
[public_key_input("YOUR_USER_ID"), public_key_input("RECIPIENT_USER_ID")],
# conversation_id=None for a new 1:1; pass the id to rotate later
)
resp = client.chat.add_conversation_keys(
"RECIPIENT_USER_ID",
{
"conversation_key_version": prepared["conversation_key_version"],
"conversation_participant_keys": [
{
"user_id": pk["user_id"],
"encrypted_conversation_key": pk["encrypted_key"],
"public_key_version": pk["public_key_version"],
}
for pk in prepared["participant_keys"]
],
"action_signatures": [
{
"message_id": sig["message_id"],
"encoded_message_event_detail": sig["encoded_message_event_detail"],
"message_event_signature": {
"signature": sig["signature"],
"public_key_version": sig["public_key_version"],
"signature_version": sig["signature_version"],
},
}
for sig in prepared["action_signatures"]
],
},
)
conversation_id = resp.data["conversation_id"] # canonical id for later requests
sequence_id = resp.data["sequence_id"]
conv_key = prepared["conversation_key"]
conv_key_version = prepared["conversation_key_version"]
async function publicKeyInput(userId: string) {
const r = (await client.chat.getUserPublicKeys(userId, {
publicKeyFields: ['public_key_version', 'public_key'],
})).data[0];
return { userId, publicKey: r.public_key, keyVersion: r.public_key_version };
}
// Omit conversationId for a new 1:1; pass the id to rotate later
const prepared = chat.prepareConversationKeyChange({
publicKeys: [
await publicKeyInput('YOUR_USER_ID'),
await publicKeyInput('RECIPIENT_USER_ID'),
],
});
const resp = await client.chat.addConversationKeys('RECIPIENT_USER_ID', {
conversation_key_version: prepared.conversationKeyVersion,
conversation_participant_keys: prepared.participantKeys.map((pk) => ({
user_id: pk.userId,
encrypted_conversation_key: pk.encryptedKey,
public_key_version: pk.publicKeyVersion,
})),
action_signatures: prepared.actionSignatures.map((sig) => ({
message_id: sig.messageId,
encoded_message_event_detail: sig.encodedMessageEventDetail,
message_event_signature: {
signature: sig.signature,
public_key_version: sig.publicKeyVersion,
signature_version: sig.signatureVersion,
},
})),
});
const conversationId = resp.data.conversation_id; // canonical id for later requests
const sequenceId = resp.data.sequence_id;
const convKey = prepared.conversationKey;
const convKeyVersion = prepared.conversationKeyVersion;
// public_key_inputs: Vec<PublicKeyInput> from GET public keys
// (user_id, public_key, key_version ← public_key_version)
// New 1:1; set params.conversation_id = Some(id) to rotate later
let prepared = chat.prepare_conversation_key_change(
ConversationKeyChangeParams::new(public_key_inputs),
)?;
let participant_keys: Vec<_> = prepared
.participant_keys
.iter()
.map(|pk| {
serde_json::json!({
"user_id": pk.user_id,
"encrypted_conversation_key": pk.encrypted_key,
"public_key_version": pk.public_key_version,
})
})
.collect();
let action_signatures: Vec<_> = prepared
.action_signatures
.iter()
.map(|sig| {
serde_json::json!({
"message_id": sig.message_id,
"encoded_message_event_detail": sig.encoded_message_event_detail,
"message_event_signature": {
"signature": sig.signature,
"public_key_version": sig.public_key_version,
"signature_version": sig.signature_version,
},
})
})
.collect();
let body = serde_json::json!({
"conversation_key_version": prepared.conversation_key_version,
"conversation_participant_keys": participant_keys,
"action_signatures": action_signatures,
});
let resp: serde_json::Value = http
.post(format!("https://api.x.com/2/chat/conversations/{recipient_id}/keys"))
.header("Authorization", &auth)
.json(&body)
.send()?
.json()?;
// Canonical id for later requests
let conversation_id = resp["data"]["conversation_id"].as_str().unwrap().to_string();
// conversation_key is Option<XChatConversationKey>; encrypt_message wants owned bytes
let conv_key = prepared.conversation_key.expect("key present").to_bytes();
let conv_key_version = prepared.conversation_key_version;
// KeyVersion comes from the public_key_version field on each record
prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
PublicKeys: []chatxdk.PublicKeyInput{
{UserID: myUserID, PublicKey: myIdentityPubB64, KeyVersion: myKeyVersion},
{UserID: recipientID, PublicKey: theirIdentityPubB64, KeyVersion: theirKeyVersion},
},
// ConversationID empty for a new 1:1; pass the id to rotate later
})
var parts []map[string]string
for _, pk := range prepared.ParticipantKeys {
parts = append(parts, map[string]string{
"user_id": pk.UserID,
"encrypted_conversation_key": pk.EncryptedKey,
"public_key_version": pk.PublicKeyVersion,
})
}
var sigs []map[string]any
for _, sig := range prepared.ActionSignatures {
sigs = append(sigs, map[string]any{
"message_id": sig.MessageID,
"encoded_message_event_detail": sig.EncodedMessageEventDetail,
"message_event_signature": map[string]string{
"signature": sig.Signature,
"public_key_version": sig.PublicKeyVersion,
"signature_version": sig.SignatureVersion,
},
})
}
body, _ := json.Marshal(map[string]any{
"conversation_key_version": prepared.ConversationKeyVersion,
"conversation_participant_keys": parts,
"action_signatures": sigs,
})
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/chat/conversations/"+recipientID+"/keys",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
// Response data.conversation_id is the canonical id for later requests
_ = resp
convKey := prepared.ConversationKey
convKeyVersion := prepared.ConversationKeyVersion
// KeyVersion comes from the public_key_version field on each record
var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams(new[] {
new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer },
new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer },
})); // ConversationId null for a new 1:1; set it to rotate later
var keysBody = new {
conversation_key_version = prepared.ConversationKeyVersion,
conversation_participant_keys = prepared.ParticipantKeys.Select(pk => new {
user_id = pk.UserId,
encrypted_conversation_key = pk.EncryptedKey,
public_key_version = pk.PublicKeyVersion,
}),
action_signatures = prepared.ActionSignatures.Select(sig => new {
message_id = sig.MessageId,
encoded_message_event_detail = sig.EncodedMessageEventDetail,
message_event_signature = new {
signature = sig.Signature,
public_key_version = sig.PublicKeyVersion,
signature_version = sig.SignatureVersion,
},
}),
};
var json = System.Text.Json.JsonSerializer.Serialize(keysBody);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var resp = await http.PostAsync(
$"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(recipientId)}/keys",
content);
resp.EnsureSuccessStatusCode();
var data = System.Text.Json.JsonDocument.Parse(await resp.Content.ReadAsStringAsync())
.RootElement.GetProperty("data");
string conversationId = data.GetProperty("conversation_id").GetString()!; // canonical id
byte[] convKey = prepared.ConversationKey!;
string convKeyVersion = prepared.ConversationKeyVersion;
// keyVersion comes from the public_key_version field on each record
PublicKeyInput mine = new PublicKeyInput();
mine.userId = myUserId; mine.publicKey = myIdentityPubB64; mine.keyVersion = myKeyVersion;
PublicKeyInput theirs = new PublicKeyInput();
theirs.userId = recipientId; theirs.publicKey = theirIdentityPubB64; theirs.keyVersion = theirKeyVersion;
// conversationId stays null for a new 1:1; set it to rotate later
PreparedConversationChange prepared =
chat.prepareConversationKeyChange(new ConversationKeyChangeParams(List.of(mine, theirs)));
List<Map<String, String>> parts = new ArrayList<>();
for (var pk : prepared.participantKeys) {
parts.add(Map.of(
"user_id", pk.userId,
"encrypted_conversation_key", pk.encryptedKey,
"public_key_version", pk.publicKeyVersion));
}
List<Map<String, Object>> sigs = new ArrayList<>();
for (var sig : prepared.actionSignatures) {
sigs.add(Map.of(
"message_id", sig.messageId,
"encoded_message_event_detail", sig.encodedMessageEventDetail,
"message_event_signature", Map.of(
"signature", sig.signature,
"public_key_version", sig.publicKeyVersion,
"signature_version", sig.signatureVersion)));
}
ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(Map.of(
"conversation_key_version", prepared.conversationKeyVersion,
"conversation_participant_keys", parts,
"action_signatures", sigs));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/chat/conversations/" + recipientId + "/keys"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
JsonNode data = mapper.readTree(resp.body()).path("data");
String conversationId = data.path("conversation_id").asText(); // canonical id
byte[] convKey = prepared.conversationKey;
String convKeyVersion = prepared.conversationKeyVersion;
5. メッセージを送信する
ステップ 4 の未加工の会話鍵で暗号化します。SDK はメッセージ ID(UUID)を生成し、署名付きイベントに埋め込み、ペイロード上で返します — 自分で生成することはありません。送信リクエストでは、次のようにマップします:| Chat XDK フィールド | リクエストボディフィールド |
|---|---|
encrypted_content / encryptedContent / EncryptedContent | encoded_message_create_event |
encoded_event_signature / encodedEventSignature / EncodedEventSignature | encoded_message_event_signature |
ペイロードの message_id / messageId / MessageId | message_id |
: → -)。SDK 自体は柔軟です: encrypt_message と encrypt_reply は、保持している任意の形式で ID を受け付けます — イベントからの A:B、リストや URL パスからの A-B(どちらの順序でも可)、あるいは受信者のユーザー ID だけでも構いません — そして署名する前に正規化します。グループ ID(g プレフィックス付き)はそのまま渡されます。
- Python
- TypeScript
- Rust
- Go
- C#
- Java
from xdk.chat.models import SendMessageRequest
# Sender identity resolves from set_identity (step 2)
payload = chat.encrypt_message(
"CONVERSATION_ID",
"Hello!",
conversation_key=conv_key,
conversation_key_version=conv_key_version,
)
client.chat.send_message(
"RECIPIENT_USER_ID",
SendMessageRequest(
message_id=payload.message_id, # SDK-generated, embedded in the signed event
encoded_message_create_event=payload.encrypted_content,
encoded_message_event_signature=payload.encoded_event_signature,
),
)
// Sender identity resolves from setIdentity (step 2)
const payload = chat.encryptMessage({
conversationId: 'CONVERSATION_ID',
text: 'Hello!',
conversationKey: convKey,
conversationKeyVersion: convKeyVersion,
});
await client.chat.sendMessage('RECIPIENT_USER_ID', {
message_id: payload.messageId, // SDK-generated, embedded in the signed event
encoded_message_create_event: payload.encryptedContent,
encoded_message_event_signature: payload.encodedEventSignature,
});
use chat_xdk_core::EncryptMessageParams;
// Sender identity resolves from set_identity (step 2)
let payload = chat.encrypt_message(
EncryptMessageParams::new(&conversation_id, "Hello!")
.with_conversation_key(conv_key, &conv_key_version),
)?;
let body = serde_json::json!({
// SDK-generated, embedded in the signed event
"message_id": payload.message_id,
"encoded_message_create_event": payload.encrypted_content,
"encoded_message_event_signature": payload.encoded_event_signature,
});
let path_id = conversation_id.replace(':', "-");
http.post(format!("https://api.x.com/2/chat/conversations/{path_id}/messages"))
.header("Authorization", &auth)
.json(&body)
.send()?;
// Sender identity resolves from SetIdentity (step 2)
payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
ConversationID: conversationID,
Text: "Hello!",
ConversationKey: convKey,
ConversationKeyVersion: convKeyVersion,
})
if err != nil {
log.Fatal(err)
}
body, _ := json.Marshal(map[string]string{
// SDK-generated, embedded in the signed event
"message_id": payload.MessageID,
"encoded_message_create_event": payload.EncryptedContent,
"encoded_message_event_signature": payload.EncodedEventSignature,
})
pathID := strings.ReplaceAll(conversationID, ":", "-")
req, _ := http.NewRequest(http.MethodPost,
"https://api.x.com/2/chat/conversations/"+pathID+"/messages",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
_ = resp
// Sender identity resolves from SetIdentity (step 2)
var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") {
ConversationKey = convKey,
ConversationKeyVersion = convKeyVersion,
});
var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string> {
// SDK-generated, embedded in the signed event
["message_id"] = payload.MessageId,
["encoded_message_create_event"] = payload.EncryptedContent,
["encoded_message_event_signature"] = payload.EncodedEventSignature,
});
using var content = new StringContent(sendJson, Encoding.UTF8, "application/json");
var pathId = conversationId.Replace(':', '-');
using var resp = await http.PostAsync(
$"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(pathId)}/messages",
content);
resp.EnsureSuccessStatusCode();
// Sender identity resolves from setIdentity (step 2)
EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!");
params.conversationKey = convKey;
params.conversationKeyVersion = convKeyVersion;
SendPayload payload = chat.encryptMessage(params);
String pathId = conversationId.replace(':', '-');
String sendJson = new ObjectMapper().writeValueAsString(Map.of(
// SDK-generated, embedded in the signed event
"message_id", payload.messageId,
"encoded_message_create_event", payload.encryptedContent,
"encoded_message_event_signature", payload.encodedEventSignature));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.x.com/2/chat/conversations/" + pathId + "/messages"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(sendJson))
.build();
http.send(req, HttpResponse.BodyHandlers.ofString());
このフローでは、ステップ 4 で作成したばかりなので、スニペットは会話鍵を明示的に渡しています。鍵キャッシュがオンで、
decrypt_events パスが会話の鍵を検証済み(ステップ 6)であれば、encrypt_message(conversation_id, text) だけで十分です — SDK が最新の検証済み鍵を埋めます。再試行では同じ暗号化されたペイロードを再送信し、ID が二度と生成されないようにしてください。6. 受信して復号する
ライブトラフィックには Webhook またはアクティビティストリーム を使用します。履歴には会話イベントをページングします。- ライブペイロードのフィールド:
encoded_event、オプションのconversation_key_change_event - 履歴:
GET /2/chat/conversations/{id}/events— 全イベントに対するdecrypt_eventsとmeta.conversation_key_eventsを優先 - 復号には、SDK が各メッセージの作者を検証できるように、送信者の署名鍵が必要です。これらは他の参加者の公開鍵です — ステップ 4 で使用したのと同じ public-keys エンドポイントから取得し、フィールドを
SigningKeyEntryにマップします(以下のスニペットにはマッピングが含まれています) - 署名鍵(および
decrypt_eventについては会話鍵)を毎回渡すか、2 つのオプションのセッションストアを一度設定して短い呼び出し形式を使用できます。以下のスニペットはストアを使用します:set_signing_keys(entries)は参加者の鍵を保持し、set_cache_keys(true)(デフォルトではオフ)は各会話の最新の署名検証済み鍵を保持するため、後続の呼び出しでは鍵引数を省略できます。両方のスタイルは同じように検証します - JavaScript は camelCase のイベントタイプ(
message)を使用します;他の言語は JSON で"Message"と snake_case フィールドを使用します
- Python
- TypeScript
- Rust
- Go
- C#
- Java
# Once per process: fill the signing-key store and enable the key cache
def signing_keys_for(user_id: str) -> list[dict]:
resp = client.chat.get_user_public_keys(
user_id,
public_key_fields=[
"public_key_version", "public_key", "signing_public_key", "identity_public_key_signature",
],
)
return [
{
"user_id": user_id,
"public_key_version": r["public_key_version"],
"public_key": r["signing_public_key"],
"identity_public_key": r["public_key"],
"identity_public_key_signature": r["identity_public_key_signature"],
}
for r in resp.data
]
chat.set_signing_keys(
signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID")
)
chat.set_cache_keys(True)
# Initial load or pagination: batch decrypt. Conversation keys are
# extracted from the KeyChange events in the batch; per-event failures
# are collected in result["errors"], never raised.
result = chat.decrypt_events(all_events_b64)
for dm in result["messages"]:
event = dm["event"]
if event["type"] == "Message" and event["content"]["content_type"] == "Text":
print(event["sender_id"], event["content"]["text"], event["verified"])
# Live traffic: one event at a time
def handle_payload(payload: dict):
if payload.get("conversation_key_change_event"):
# A rotation enters the key cache only after its signature
# verifies, which is what decrypt_events does
chat.decrypt_events([payload["conversation_key_change_event"]])
event = chat.decrypt_event(payload["encoded_event"]) # raises on failure
if event["type"] == "Message" and event["content"]["content_type"] == "Text":
print(event["sender_id"], event["content"]["text"], event["verified"])
// Once per process: fill the signing-key store and enable the key cache
async function signingKeysFor(userId: string) {
const resp = await client.chat.getUserPublicKeys(userId, {
publicKeyFields: [
'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature',
],
});
return resp.data.map((r: {
public_key_version: string;
public_key: string;
signing_public_key: string;
identity_public_key_signature: string;
}) => ({
userId,
publicKeyVersion: r.public_key_version,
publicKey: r.signing_public_key,
identityPublicKey: r.public_key,
identityPublicKeySignature: r.identity_public_key_signature,
}));
}
chat.setSigningKeys([
...(await signingKeysFor('YOUR_USER_ID')),
...(await signingKeysFor('RECIPIENT_USER_ID')),
]);
chat.setCacheKeys(true);
// Initial load or pagination: batch decrypt. Conversation keys are
// extracted from the KeyChange events in the batch; per-event failures
// are collected in result.errors, never thrown.
const result = chat.decryptEvents(allEventsB64);
for (const dm of result.messages) {
if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') {
console.log(dm.event.senderId, dm.event.content.text, dm.event.verified);
}
}
// Live traffic: one event at a time
function handlePayload(payload: {
encoded_event: string;
conversation_key_change_event?: string;
}) {
if (payload.conversation_key_change_event) {
// A rotation enters the key cache only after its signature
// verifies, which is what decryptEvents does
chat.decryptEvents([payload.conversation_key_change_event]);
}
const event = chat.decryptEvent(payload.encoded_event); // throws on failure
if (event.type === 'message' && event.content?.contentType === 'text') {
console.log(event.senderId, event.content.text, event.verified);
}
}
// Once per instance: fill the signing-key store (Vec<SigningKeyEntry>
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.set_signing_keys(participant_signing_keys);
chat.set_cache_keys(true);
// Initial load: batch decrypt — per-event failures land in result.errors
let result = chat.decrypt_events(&all_events_b64, &[]);
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what decrypt_events does
if let Some(kc) = key_change_b64.as_deref() {
chat.decrypt_events(&[kc], &[]);
}
let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?;
// Once per instance: fill the signing-key store ([]SigningKeyEntry
// from GET /2/users/{id}/public_keys) and enable the key cache
if err := chat.SetSigningKeys(participantSigningKeys); err != nil {
log.Fatal(err)
}
chat.SetCacheKeys(true)
// Initial load: batch decrypt — per-event failures land in result.Errors
result, err := chat.DecryptEvents(allEventsB64, nil)
if err != nil {
log.Fatal(err)
}
for _, dm := range result.Messages {
if dm.Event.Type == "Message" {
fmt.Println(dm.Event.AsMessage().Text())
}
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what DecryptEvents does
if keyChange != "" {
chat.DecryptEvents([]string{keyChange}, nil)
}
event, err := chat.DecryptEvent(encodedEvent, nil, nil)
if err == nil && event.Type == "Message" {
fmt.Println(event.AsMessage().Text())
}
// Once per instance: fill the signing-key store (SigningKeyEntry list
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.SetSigningKeys(participantSigningKeys);
chat.SetCacheKeys(true);
// Initial load: batch decrypt — per-event failures land in result.Errors
var result = chat.DecryptEvents(allEventsB64);
foreach (var dm in result.Messages)
{
if (dm.Event.GetProperty("type").GetString() == "Message")
Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what DecryptEvents does
if (!string.IsNullOrEmpty(keyChangeB64))
chat.DecryptEvents(new[] { keyChangeB64 });
var evt = chat.DecryptEvent(encodedEvent); // throws on failure
if (evt.GetProperty("type").GetString() == "Message")
Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
// Once per instance: fill the signing-key store (SigningKeyEntry list
// from GET /2/users/{id}/public_keys) and enable the key cache
chat.setSigningKeys(participantSigningKeys);
chat.setCacheKeys(true);
// Initial load: batch decrypt — per-event failures land in result.errors
DecryptEventsResult result = chat.decryptEvents(allEventsB64, null);
for (DecryptedMessage dm : result.messages) {
if ("Message".equals(dm.event.path("type").asText())) {
System.out.println(dm.event.path("content").path("text").asText());
}
}
// Live traffic: a rotation enters the key cache only after its
// signature verifies, which is what decryptEvents does
if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
chat.decryptEvents(List.of(keyChangeB64), null);
}
JsonNode evt = chat.decryptEvent(encodedEvent, (Map<String, byte[]>) null, null);
if ("Message".equals(evt.path("type").asText())) {
System.out.println(evt.path("content").path("text").asText());
}
サーバーレスまたはマルチインスタンス? 署名鍵ストアと鍵キャッシュは SDK インスタンスのメモリに存在します。それが合わない場合 — 1 つの呼び出しが復号し、別のものが送信する — 代わりに鍵を明示的に渡してください:
decrypt_events(events, signing_keys)、decrypt_event(event_b64, conversation_keys, signing_keys)、および暗号化メソッドの conversation_key/conversation_key_version 上書き。decrypt_events が返す conversation_keys を自分で永続化し、次回渡してください。ベストプラクティス
- 署名鍵ストアを最新に保つ: 送信者が新しい鍵バージョンを登録したときは、参加者全員を含めて
set_signing_keysを再呼び出しし、署名検証の失敗時は更新する event_uuidでライブ配信の重複を排除する