GitHub
Draft v2

Story Extension

24時間で消える一時的なコンテンツ(ストーリー)をActivityPubで配信するための拡張仕様。 Instagram StoriesやSnapchat Storiesのような機能を分散SNSで実現します。

概要

Story拡張は、時間制限付きのコンテンツを配信するためのActivityPub拡張です。 AS2標準の 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 プロパティを持つ必要があります。

想定されるオーバーレイ型:

基本的なストーリー(画像のみ)

{
  "@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 を過ぎたストーリーは、受信側で自動的に非表示にすることが推奨されます。

実装上の注意

推奨メディアサイズ

形式 推奨サイズ 最大尺
画像 1080x1920px (9:16) -
動画 1080x1920px (9:16) 60秒

変更履歴

バージョン 変更内容
v2 (2026-01) frames 配列を廃止、attachment をStory直下に移動、overlays を追加
v1 (2025-12) 初期仕様(frames 配列ベース)

参考