Story Extension
24時間で消える一時的なコンテンツ(ストーリー)をActivityPubで配信するための拡張仕様。 Instagram StoriesやSnapchat Storiesのような機能を分散SNSで実現します。
概要
Story拡張は、時間制限付きのコンテンツを配信するためのActivityPub拡張です。
AS2標準の
Note を拡張し、単一のメディア(画像または動画)と
インタラクティブなオーバーレイ(投票など)を定義します。
設計方針
- 1投稿 = 1メディア - 各Storyは独立した画像または動画を持つ
- テキスト/スタンプは焼き込み - メディアに合成済みの状態で配信
-
インタラクティブ要素は分離 - 投票などは
overlaysで定義 - 連続再生はUIの責務 - 同一ユーザーの複数Storyをクライアントが連続表示
- AS2相互運用 - Story非対応の実装でも通常のNoteとして表示可能
名前空間
Story拡張は以下の名前空間を使用します:
https://yurucommu.com/ns/story#
JSON-LD Context
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"story": "https://yurucommu.com/ns/story#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"Story": "story:Story",
"displayDuration": {
"@id": "story:displayDuration",
"@type": "xsd:duration"
},
"overlays": {
"@id": "story:overlays",
"@container": "@list"
},
"position": "story:position"
}
]
}
型定義
Story
ストーリーオブジェクト。AS2の Note を拡張します。
1つのStoryは1つのメディア(画像または動画)を持ちます。
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
type |
Array | Yes | ["Story", "Note"] |
attachment |
Document | Video | Yes | メディア(画像または動画) |
displayDuration |
xsd:duration | Yes |
表示時間(例: PT5S = 5秒)
|
endTime |
DateTime | Yes | ストーリーの有効期限(AS2標準) |
overlays |
Array<Object> | No | インタラクティブなオーバーレイ要素 |
OverlayPosition
オーバーレイ要素の位置を指定します。座標は相対値(0.0〜1.0)で指定します。
| プロパティ | 型 | 説明 |
|---|---|---|
x |
Number (0.0-1.0) | 要素中心のX座標(左端=0, 右端=1) |
y |
Number (0.0-1.0) | 要素中心のY座標(上端=0, 下端=1) |
width |
Number (0.0-1.0) | 要素の幅(キャンバス幅に対する比率) |
height |
Number (0.0-1.0) | 要素の高さ(キャンバス高さに対する比率) |
座標系
┌─────────────────────────────┐
│ (0,0) │
│ │
│ ┌───────┐ │
│ │ x=0.5 │ ← 中央配置 │
│ │ y=0.75│ │
│ └───────┘ │
│ (1,1) │
└─────────────────────────────┘
x, y: 要素の中心点の位置
width, height: 要素のサイズ(相対値)
Overlays
overlays 配列には任意のActivityStreams
2.0オブジェクトを含めることができます。 各オーバーレイは
position プロパティを持つ必要があります。
想定されるオーバーレイ型:
Question- 投票(AS2標準)Note- テキストリンクLink- 外部リンク- 任意の拡張型
例
基本的なストーリー(画像のみ)
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"story": "https://yurucommu.com/ns/story#",
"Story": "story:Story",
"displayDuration": "story:displayDuration"
}
],
"id": "https://example.com/users/alice/stories/2026-01-15/1",
"type": ["Story", "Note"],
"attributedTo": "https://example.com/users/alice",
"published": "2026-01-15T10:00:00Z",
"endTime": "2026-01-16T10:00:00Z",
"to": ["https://example.com/users/alice/followers"],
"attachment": {
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://example.com/media/story/abc123.jpg",
"width": 1080,
"height": 1920
},
"displayDuration": "PT5S"
}
動画ストーリー
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"story": "https://yurucommu.com/ns/story#",
"Story": "story:Story",
"displayDuration": "story:displayDuration"
}
],
"id": "https://example.com/users/alice/stories/2026-01-15/2",
"type": ["Story", "Note"],
"attributedTo": "https://example.com/users/alice",
"published": "2026-01-15T12:00:00Z",
"endTime": "2026-01-16T12:00:00Z",
"to": ["https://example.com/users/alice/followers"],
"attachment": {
"type": "Video",
"mediaType": "video/mp4",
"url": "https://example.com/media/story/video456.mp4",
"width": 1080,
"height": 1920,
"duration": "PT15S"
},
"displayDuration": "PT15S"
}
投票付きストーリー
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"story": "https://yurucommu.com/ns/story#",
"Story": "story:Story",
"displayDuration": "story:displayDuration",
"overlays": { "@id": "story:overlays", "@container": "@list" },
"position": "story:position"
}
],
"id": "https://example.com/users/alice/stories/2026-01-15/3",
"type": ["Story", "Note"],
"attributedTo": "https://example.com/users/alice",
"published": "2026-01-15T14:00:00Z",
"endTime": "2026-01-16T14:00:00Z",
"to": ["https://example.com/users/alice/followers"],
"attachment": {
"type": "Document",
"mediaType": "image/jpeg",
"url": "https://example.com/media/story/poll-bg.jpg",
"width": 1080,
"height": 1920
},
"displayDuration": "PT7S",
"overlays": [
{
"type": "Question",
"name": "今日どっちがいい?",
"oneOf": [
{ "type": "Note", "name": "A" },
{ "type": "Note", "name": "B" }
],
"closed": "2026-01-15T20:00:00Z",
"position": {
"x": 0.5,
"y": 0.75,
"width": 0.8,
"height": 0.15
}
}
]
}
配信
StoryはActivityPubの
Create アクティビティで配信されます。
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"actor": "https://example.com/users/alice",
"to": ["https://example.com/users/alice/followers"],
"object": {
"type": ["Story", "Note"],
"id": "https://example.com/users/alice/stories/2026-01-15/1",
...
}
}
連続再生
同一ユーザーの複数Storyは、クライアントUIが連続再生として表示します。 これはサーバー側ではなくクライアント側の責務です。
// クライアントの表示ロジック
const storiesByUser = groupBy(stories, s => s.attributedTo);
// 各ユーザーのストーリーを published 順にソート
for (const [userId, userStories] of storiesByUser) {
userStories.sort((a, b) =>
new Date(a.published) - new Date(b.published)
);
}
削除
ストーリーの期限切れ時、または手動削除時は
Delete アクティビティを送信します。
endTime
を過ぎたストーリーは、受信側で自動的に非表示にすることが推奨されます。
実装上の注意
-
標準実装での扱い -
type: ["Story", "Note"]とすることで、Story非対応の実装でもNoteとして表示可能 -
期限管理 -
endTimeを必ず設定し、期限切れコンテンツは表示しない -
displayDuration形式 - ISO 8601 duration形式(
PT5S= 5秒、PT1M30S= 1分30秒) -
動画の尺 - 動画の実際の長さはAS2標準の
durationプロパティを使用 - アスペクト比 - 推奨は縦型9:16(1080x1920px)。モバイルファーストで全画面表示に最適化
- テキスト/スタンプ - メディアに焼き込み済みの状態で配信。別レイヤーとしては配信しない
推奨メディアサイズ
| 形式 | 推奨サイズ | 最大尺 |
|---|---|---|
| 画像 | 1080x1920px (9:16) | - |
| 動画 | 1080x1920px (9:16) | 60秒 |
変更履歴
| バージョン | 変更内容 |
|---|---|
| v2 (2026-01) |
frames 配列を廃止、attachment
をStory直下に移動、overlays を追加
|
| v1 (2025-12) |
初期仕様(frames 配列ベース)
|