ボットの会話内のメッセージ
会話内の各メッセージは、messageType: message
型のActivity
オブジェクトです。 ユーザーがメッセージを送信すると、Microsoft Teamsはメッセージ アクティビティをボットに投稿します。 Teams は JSON オブジェクトをボットのメッセージング エンドポイントに送信し、Teams ではメッセージング用のエンドポイントを 1 つだけ許可します。 ボットはメッセージを調べて種類を判別し、種類に応じて応答します。
基本的な会話は、1 つの REST API である Bot Framework コネクタを介して処理されます。 この API を使用すると、ボットは Teams やその他のチャネルと通信できます。 Bot Builder SDK には、次の機能があります。
- Bot Framework コネクタに簡単にアクセスできます。
- 会話フローと状態を管理する機能。
- 自然言語処理 (NLP) などのコグニティブ サービスを組み込む簡単な方法。
ボットは、 Text
プロパティを使用して Teams からメッセージを受信し、1 つまたは複数のメッセージ応答をユーザーに送信します。
詳細については、「 ボット メッセージのユーザー属性」を参照してください。
次の表に、ボットが受け取ってアクションを実行できるアクティビティの一覧を示します。
種類 | ペイロード オブジェクト | 範囲 |
---|---|---|
メッセージ アクティビティを受信する | メッセージ アクティビティ | すべて |
メッセージの編集アクティビティを受信する | メッセージ編集アクティビティ | すべて |
メッセージの削除を取り消すアクティビティを受信する | メッセージの削除の取り消しアクティビティ | すべて |
論理的な削除メッセージ アクティビティを受信する | メッセージの論理的な削除アクティビティ | すべて |
メッセージ アクティビティを受信する
テキスト メッセージを受信するには、Activity
オブジェクトの Text
プロパティを使用します。 ボットのアクティビティ ハンドラーで、ターン コンテキスト オブジェクトの Activity
を使用して、1 つのメッセージ要求を読み取ります。
次のコードは、メッセージ アクティビティを受信する例を示しています。
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Text($"Echo: {turnContext.Activity.Text}"), cancellationToken);
}
開封確認メッセージを受信する
Teams の [開封確認] 設定を使用すると、1 対 1 のチャットとグループ チャットで受信者がメッセージを読んだときに、チャット メッセージの送信者に通知を受け取ります。 受信者がメッセージを読み取った後、メッセージの横に [表示 が表示されます。 また、[開封確認] 設定を使用して読み取り受信イベントを受信するようにボット を構成することもできます 。 開封確認イベントは、次の方法でユーザー エクスペリエンスを向上させるのに役立ちます。
アプリ ユーザーが個人用チャットでメッセージを読み取っていない場合は、フォローアップ メッセージを送信するようにボットを構成できます。
読み取りレシートを使用してフィードバック ループを作成して、ボットのエクスペリエンスを調整できます。
注:
- 読み取りレシートは、ユーザーからボットへのチャット シナリオでのみサポートされます。
- ボットの開封確認は、チーム、チャネル、グループ チャットスコープをサポートしていません。
- テナント管理者またはユーザーが [開封確認 ] 設定を無効にした場合、ボットは開封確認イベントを受け取りません。
ボットの開封確認イベントを受信するには、次のことを確認します。
- 次のように、 RSC
ChatMessageReadReceipt.Read.Chat
アクセス許可を アプリ マニフェストに追加します。
"webApplicationInfo": {
"id": "38f0ca43-1c38-4c39-8097e-47f62c686500",
"resource": ""
},
"authorization": {
"permissions": {
"orgwide": [],
"resourceSpecific": [
{
"name": "ChatMessageReadReceipt.Read.Chat",
"type": "Application"
}
]
}
}
Graph API を使用して RSC アクセス許可を追加することもできます。 詳細については、consentedPermissionSet
を参照してください。
メソッド
OnTeamsReadReceiptAsync
をIsMessageRead
ハンドラーでオーバーライドします。IsMessageRead
ヘルパー メソッドは、メッセージが受信者によって読み取られたかどうかを判断するのに役立ちます。compareMessageId
がLastReadMessageId
以下の場合は、メッセージが読み取られます。IsMessageRead
ヘルパー メソッドを使用して読み取りレシートを受信するには、OnTeamsReadReceiptAsync
メソッドをオーバーライドします。protected override async Task OnTeamsReadReceiptAsync(ReadReceiptInfo readReceiptInfo, ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken) { var lastReadMessageId = readReceiptInfo.LastReadMessageId; if (IsMessageRead("{id of the message that you care}", LastReadMessageId)) { await turnContext.SendActivityAsync(MessageFactory.Text("User read the bot's message"), cancellationToken); } }
ボットが受け取る開封確認イベント要求の例を次に示します。
{ "name": "application/vnd.microsoft.readReceipt", "type": "event", "timestamp": "2023-08-16T17:23:11.1366686Z", "id": "f:b4783e72-9d7b-2ed9-ccef-ab446c873007", "channelId": "msteams", "serviceUrl": "https://smba.trafficmanager.net/amer/", "from": { "id": "29:1-8Iuh70W9pRqV8tQK8o2nVjxz33RRGDKLf4Bh7gKnrzN8s7e4vCyrFwjkPbTCX_Co8c4aXwWvq3RBLr-WkkVMw", "aadObjectId": "5b649834-7412-4cce-9e69-176e95a394f5" }, "conversation": { "conversationType": "personal", "tenantId": "6babcaad-604b-40ac-a9d7-9fd97c0b779f", "id": "a:1xlimp68NSUxEqK0ap2rXuwC9ITauHgV2M4RaDPkeRhV8qMaFn-RyilMZ62YiVdqs8pp43yQaRKvv_U2S2gOS5nM-y_pOxVe4BW1qMGPtqD0Bv3pw-nJXF0zhDlZHMZ1Z" }, "recipient": { "id": "28:9901a8b6-4fef-428b-80b1-ddb59361adeb", "name": "Test Bot" }, "channelData": { "tenant": { "id": "6babcaad-604b-40ac-a9d7-9fd97c0b779f" } }, "value": { "lastReadMessageId": "1692206589131" } }
ボットが開封確認イベントを受信するためのテナントに対して、開封確認 管理者の設定 または ユーザー設定 が有効になっています。 テナント管理者またはユーザーは、開封確認の設定を有効または無効にする必要があります。
ユーザーからボットへのチャット シナリオでボットが有効になると、ボットは、ユーザーがボットのメッセージを読み取ったときに、読み取り受信イベントをすぐに受信します。 イベントの数をカウントすることでユーザー エンゲージメントを追跡でき、コンテキスト対応メッセージを送信することもできます。
メッセージを送信する
テキスト メッセージを送信するには、アクティビティとして送信する文字列を指定します。 ボットのアクティビティ ハンドラーで、ターン コンテキスト オブジェクトの SendActivityAsync
メソッドを使用して、1 つのメッセージ応答を送信します。 オブジェクトの SendActivitiesAsync
メソッドを使用して、複数の応答を送信します。
次のコードは、ユーザーが会話に追加されたときにメッセージを送信する例を示しています。
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(MessageFactory.Text($"Hello and welcome!"), cancellationToken);
}
注:
- メッセージ分割は、テキスト メッセージと添付ファイルが同じアクティビティ ペイロードで送信されるときに発生します。 Teams では、このアクティビティを 2 つの別々のアクティビティに分割します。1 つはテキスト メッセージで、もう 1 つは添付ファイルを含みます。 アクティビティが分割されると、応答としてメッセージ ID は受信されません。これは、メッセージを事前に 更新または削除 するために使用されます。 メッセージ分割に応じてではなく、個別のアクティビティを送信することをお勧めします。
- 送信されたメッセージは、パーソナル化を提供するためにローカライズできます。 詳細については、「 アプリのローカライズ」を参照してください。
ユーザーとボットの間で送信されるメッセージには、メッセージ内の内部チャネル データが含まれます。 このデータを使用すると、ボットはそのチャネルで適切に通信できます。 Bot Builder SDK を使用すると、メッセージ構造を変更できます。
メッセージの編集アクティビティを取得する
メッセージを編集すると、ボットはメッセージの編集アクティビティの通知を受け取ります。
ボットでメッセージ アクティビティの編集通知を取得するには、ハンドラー OnTeamsMessageEditAsync
オーバーライドできます。
送信されたメッセージが編集されたときに OnTeamsMessageEditAsync
を使用したメッセージの編集アクティビティ通知の例を次に示します。
protected override async Task OnTeamsMessageEditAsync(ITurnContext<IMessageUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var replyActivity = MessageFactory.Text("message is updated");
await turnContext.SendActivityAsync(replyActivity, cancellationToken);
}
メッセージの削除を取り消すアクティビティを取得する
メッセージの削除を取り消すと、ボットは削除解除メッセージ アクティビティの通知を受け取ります。
ボットで削除を取り消すメッセージ アクティビティ通知を取得するには、ハンドラー OnTeamsMessageUndeleteAsync
オーバーライドできます。
削除されたメッセージが復元されたときに、 OnTeamsMessageUndeleteAsync
を使用してメッセージ アクティビティを元に戻す通知の例を次に示します。
protected override async Task OnTeamsMessageUndeleteAsync(ITurnContext<IMessageUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var replyActivity = MessageFactory.Text("message is undeleted");
await turnContext.SendActivityAsync(replyActivity, cancellationToken);
}
論理的な削除メッセージ アクティビティを取得する
メッセージを論理的に削除すると、ボットは論理的な削除メッセージ アクティビティの通知を受け取ります。
ボットで論理的な削除メッセージ アクティビティ通知を取得するには、ハンドラー OnTeamsMessageSoftDeleteAsync
オーバーライドできます。
メッセージが論理的に削除されたときに、 OnTeamsMessageSoftDeleteAsync
を使用した論理的な削除メッセージ アクティビティ通知の例を次に示します。
protected override async Task OnTeamsMessageSoftDeleteAsync(ITurnContext<IMessageDeleteActivity> turnContext, CancellationToken cancellationToken)
{
var replyActivity = MessageFactory.Text("message is soft deleted");
await turnContext.SendActivityAsync(replyActivity, cancellationToken);
}
推奨されるアクションを送信する
推奨されるアクションを使用すると、ボットは、ユーザーが入力を提供するために選択できるボタンを表示できます。 推奨されるアクションは、ユーザーがキーボードで応答を入力するのではなく、質問に回答したり、ボタンを選択したりできるようにすることで、ユーザー エクスペリエンスを向上させます。 ユーザーがボタンを選択すると、リッチ カードでは表示され、アクセス可能なままになりますが、推奨されるアクションにはアクセスできません。 これにより、ユーザーが会話内の古いボタンを選択できなくなります。
推奨されるアクションをメッセージに追加するには、アクティビティ オブジェクトの suggestedActions
プロパティを設定して、ユーザーに表示するボタンを表すカード アクション オブジェクトの一覧を指定します。 詳細については、sugestedActions
を参照してください。
推奨されるアクションの実装とエクスペリエンスの例を次に示します。
"suggestedActions": {
"actions": [
{
"type": "imBack",
"title": "Action 1",
"value": "Action 1"
},
{
"type": "imBack",
"title": "Action 2",
"value": "Action 2"
}
],
"to": [<list of recepientIds>]
}
次に、推奨されるアクションの例を示します。
注:
-
SuggestedActions
は、テキスト ベースのメッセージとアダプティブ カードの両方を持つ 1 対 1 のチャット ボットでのみサポートされます。 -
SuggestedActions
は、任意の会話の種類の添付ファイルを含むチャット ボットではサポートされていません。 -
imBack
は唯一サポートされているアクションの種類であり、Teams には最大 6 つの推奨アクションが表示されます。
Teamsチャネルデータ
channelData
オブジェクトには Teams 固有の情報が含まれており、チーム ID とチャネル ID の決定的なソースです。 必要に応じて、これらの ID をキャッシュし、ローカル ストレージのキーとして使用できます。 SDK の TeamsActivityHandler
は、 channelData
オブジェクトから重要な情報を取得してアクセスできるようにします。 ただし、 turnContext
オブジェクトから元のデータにいつでもアクセスできます。
channelData
オブジェクトは、チャネルの外部で行われるので、個人的な会話のメッセージには含まれません。
ボットに送信されるアクティビティの一般的な channelData
オブジェクトには、次の情報が含まれています。
-
eventType
: チャネル変更イベントの場合にのみ、Teams イベントの種類が渡されます。 -
tenant.id
: すべてのコンテキストで渡された Microsoft Entra テナント ID。 -
team
: 個人用チャットではなく、チャネル コンテキストでのみ渡されます。-
id
: チャネルの GUID。 -
name
: チーム名が イベントの名前変更の場合にのみ渡されたチームの名前。
-
-
channel
: ボットがメンションされたとき、またはボットが追加されるチーム内のチャネルのイベントに対してのみ、チャネル コンテキストで渡されます。-
id
: チャネルの GUID。 -
name
: チャネル 変更イベントの場合にのみ渡されるチャネル名。
-
-
channelData.teamsTeamId
:廃止。 このプロパティは、下位互換性のためにのみ含まれています。 -
channelData.teamsChannelId
:廃止。 このプロパティは、下位互換性のためにのみ含まれています。
channelData オブジェクトの例
次のコードは、channelData オブジェクト (channelCreated イベント) の例を示しています。
"channelData": {
"eventType": "channelCreated",
"tenant": {
"id": "72f988bf-86f1-41af-91ab-2d7cd011db47"
},
"channel": {
"id": "19:693ecdb923ac4458a5c23661b505fc84@thread.skype",
"name": "My New Channel"
},
"team": {
"id": "19:693ecdb923ac4458a5c23661b505fc84@thread.skype"
}
}
メッセージの内容
ボットから受信またはボットに送信されるメッセージには、さまざまな種類のメッセージ コンテンツを含めることができます。
フォーマット | ユーザーからボットへ | ボットからユーザーへ | メモ |
---|---|---|---|
リッチ テキスト | ✔️ | ✔️ | ボットは、リッチ テキスト、画像、カードを送信できます。 ユーザーは、リッチ テキストと画像をボットに送信できます。 |
ピクチャ | ✔️ | ✔️ | 最大 1024 × 1024 ピクセル、PNG、JPEG、GIF 形式で 1 MB。 アニメーション GIF はサポートされていません。 |
カード | ❌ | ✔️ | サポートされているカードについては、「 Teams カードリファレンス」を参照 してください。 |
絵文字 | ✔️ | ✔️ | Teams では、顔を笑う U+1F600 など、UTF-16 を介した絵文字がサポートされています。 |
画像メッセージ
メッセージを強化するために、そのメッセージの添付ファイルとして画像を含めることができます。 添付ファイルの詳細については、「メッセージに メディア添付ファイルを追加する」を参照してください。
画像は、最大 1024 × 1024 ピクセル、PNG、JPEG、GIF 形式で 1 MB にすることができます。 アニメーション GIF はサポートされていません。
XML を使用して、各イメージの高さと幅を指定します。 Markdown では、イメージ サイズの既定値は 256×256 です。 例:
- 使用:
<img src="http://aka.ms/Fo983c" alt="Duck on a rock" height="150" width="223"></img>
。 - 使用しないでください:
![Duck on a rock](http://aka.ms/Fo983c)
。
会話型ボットには、ビジネス ワークフローを簡略化するアダプティブ カードを含めることができます。 アダプティブ カードは、カスタマイズ可能な豊富なテキスト、音声、画像、ボタン、入力フィールドを提供します。
アダプティブ カード
アダプティブ カードはボットで作成し、Teams や Web サイトなどの複数のアプリに表示できます。 詳細については「アダプティブ カード」を参照してください。
次のコードは、単純なアダプティブ カードを送信する例を示しています。
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.5",
"body": [
{
"items": [
{
"size": "large",
"text": " Simple Adaptivecard Example with a Textbox",
"type": "TextBlock",
"weight": "bolder",
"wrap": true
},
],
"spacing": "extraLarge",
"type": "Container",
"verticalContentAlignment": "center"
}
]
}
メッセージに通知を追加する
アプリケーションから通知を送信するには、次の 2 つの方法があります。
- ボット メッセージに
Notification.Alert
プロパティを設定します。 - Graph API を使用してアクティビティ フィード通知を送信する。
Notification.Alert
プロパティを使用して、メッセージに通知を追加できます。 通知は、新しいタスク、メンション、コメントなど、アプリケーション内のイベントに対してユーザーに警告します。 これらのアラートは、ユーザーが作業している内容や、アクティビティ フィードに通知を挿入して確認する必要がある内容に関連しています。 ボット メッセージからトリガーする通知の場合は、 TeamsChannelData
オブジェクト Notification.Alert
プロパティを true に設定 します。 通知が発生する場合は、個々のユーザーの Teams 設定に依存し、これらの設定をオーバーライドすることはできません。
ユーザーにメッセージを送信せずに任意の通知を生成する場合は、Graph API を使用できます。 詳細については、 Graph API とベスト プラクティスを使用してアクティビティ フィード通知を送信する方法 に関するページを参照 してください。
注:
[ 概要 ] フィールドには、フィード内の通知メッセージとしてユーザーからのテキストが表示されます。
次のコードは、メッセージに通知を追加する例を示しています。
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Returns a simple text message.
var message = MessageFactory.Text("You'll get a notification, if you've turned them on.");
message.TeamsNotifyUser();
// Sends an activity to the sender of the incoming activity.
await turnContext.SendActivityAsync(message);
}
ボットの会話型 API からの状態コード
Teams アプリでこれらのエラーを適切に処理してください。 次の表に、エラー コードと、エラーが生成される説明を示します。
状態コード | エラー コードとメッセージ値 | 説明 | 再試行要求 | 開発者アクション |
---|---|---|---|---|
400 |
コード: Bad Argument メッセージ: *シナリオ固有 |
ボットによって提供される要求ペイロードが無効です。 詳細については、「エラー メッセージ」を参照してください。 | いいえ | エラーの要求ペイロードを再評価します。 詳細については、返されたエラー メッセージを確認してください。 |
401 |
コード: BotNotRegistered メッセージ: このボットの登録が見つかりません。 |
このボットの登録が見つかりませんでした。 | いいえ | ボット ID とパスワードを確認します。 ボット ID (Microsoft Entra ID) が Teams 開発者ポータルに登録されているか、Azure の Azure ボット チャネル登録を介して "Teams" チャネルが有効になっていることを確認します。 |
403 |
コード: BotDisabledByAdmin メッセージ: テナント管理者がこのボットを無効にしました |
テナント管理者は、ユーザーとボット アプリ間の相互作用をブロックしました。 テナント管理者は、アプリ ポリシー内のユーザーのアプリを許可する必要があります。 詳細については、「 アプリ ポリシー」を参照してください。 | いいえ | ボットとの対話が、ボットがブロックされなくなったことを示す会話内のユーザーによって明示的に開始されるまで、会話への投稿を停止します。 |
403 |
コード: BotNotInConversationRoster メッセージ: ボットは会話名簿の一部ではありません。 |
ボットは会話の一部ではありません。 会話でアプリを再インストールする必要があります。 | いいえ | 別の会話要求を送信する前に、ボットが再度追加されたことを示す installationUpdate イベントを待ちます。 |
403 |
コード: ConversationBlockedByUser メッセージ: ユーザーはボットとの会話をブロックしました。 |
ユーザーは、モデレート設定を使用して、個人用チャットまたはチャネルでボットをブロックしました。 | いいえ | キャッシュから会話を削除します。 ボットとの対話が会話内のユーザーによって明示的に開始され、ボットがブロックされなくなったことを示すまで、会話への投稿を停止します。 |
403 |
コード: ForbiddenOperationException メッセージ: ボットがユーザーの個人用スコープにインストールされていない |
プロアクティブ メッセージは、個人用スコープにインストールされていないボットによって送信されます。 | いいえ | 別の会話要求を送信する前に、個人用スコープでアプリをインストールします。 |
403 |
コード: InvalidBotApiHost メッセージ: ボット API ホストが無効です。 GCC テナントの場合は、 https://smba.infra.gcc.teams.microsoft.com を呼び出します。 |
GCC テナントに属する会話のパブリック API エンドポイントと呼ばれるボット。 | いいえ | 会話のサービス URL を更新して https://smba.infra.gcc.teams.microsoft.com し、要求を再試行します。 |
403 |
コード: NotEnoughPermissions メッセージ: *シナリオ固有 |
ボットには、要求されたアクションを実行するための必要なアクセス許可がありません。 | いいえ | エラー メッセージから必要なアクションを決定します。 |
404 |
コード: ActivityNotFoundInConversation メッセージ: 会話が見つかりません。 |
指定されたメッセージ ID が会話で見つかりませんでした。 メッセージが存在しないか、削除されます。 | いいえ | 送信されるメッセージ ID が予期される値であるかどうかを確認します。 キャッシュされた場合は、ID を削除します。 |
404 |
コード: ConversationNotFound メッセージ: 会話が見つかりません。 |
会話が見つからなかったのは、存在しないか削除されているためです。 | いいえ | 送信された会話 ID が予期される値であるかどうかを確認します。 キャッシュされた場合は、ID を削除します。 |
412 |
コード: PreconditionFailed メッセージ: 前提条件に失敗しました。もう一度お試しください。 |
同じ会話に対する複数の同時操作が原因で、いずれかの依存関係で前提条件が失敗しました。 | はい | 指数バックオフを使用して再試行します。 |
413 |
コード: MessageSizeTooBig メッセージ: メッセージ サイズが大きすぎます。 |
受信要求のサイズが大きすぎます。 詳細については、「 ボット メッセージの書式設定」を参照してください。 | いいえ | ペイロード サイズを小さくします。 |
429 |
コード: Throttled メッセージ: 要求が多すぎます。 また、後で再試行するタイミングも返します。 |
ボットから送信された要求が多すぎます。 詳細については、「 レート制限」を参照してください。 | はい |
Retry-After ヘッダーを使用して、バックオフ時間を確認してください。 |
500 |
コード: ServiceError メッセージ: *各種 |
内部サーバー エラー。 | いいえ | 開発者コミュニティで問題を報告します。 |
502 |
コード: ServiceError メッセージ: *各種 |
サービス依存関係の問題。 | はい | 指数バックオフを使用して再試行します。 問題が解決しない場合は、 開発者コミュニティで問題を報告してください。 |
503 | サービスは使用できません。 | はい | 指数バックオフを使用して再試行します。 問題が解決しない場合は、 開発者コミュニティで問題を報告してください。 | |
504 | ゲートウェイのタイムアウト。 | はい | 指数バックオフを使用して再試行します。 問題が解決しない場合は、 開発者コミュニティで問題を報告してください。 |
状態コードの再試行ガイダンス
各状態コードの一般的な再試行ガイダンスを次の表に示します。ボットは、指定されていない状態コードの再試行を避ける必要があります。
状態コード | 再試行戦略 |
---|---|
403 |
InvalidBotApiHost の GCC API https://smba.infra.gcc.teams.microsoft.com を呼び出して再試行します。 |
412 | 指数バックオフを使用して再試行します。 |
429 |
Retry-After ヘッダーを使用して再試行し、要求の間の待機時間 (使用可能な場合) を秒単位で判断します。 それ以外の場合は、可能であれば、スレッド ID で指数バックオフを使用して再試行してください。 |
502 | 指数バックオフを使用して再試行します。 |
503 | 指数バックオフを使用して再試行します。 |
504 | 指数バックオフを使用して再試行します。 |
コード サンプル
サンプルの名前 | 説明 | Node.js | .NETCore | Python | .NET | マニフェスト |
---|---|---|---|---|---|---|
Teams 会話ボット | このサンプル アプリでは、Bot Framework v4 で使用できるさまざまなボット会話イベントを使用する方法を示します。 | 表示 | 表示 | 表示 | 該当なし | 表示 |
Teams アプリのローカライズ | このサンプルでは、ボットとタブを使用した Teams アプリのローカライズを示します。 | 表示 | 該当なし | 該当なし | 表示 | 該当なし |
次の手順
関連項目
Platform Docs