グローバルなデータ保護およびプライバシー規制は、個人に自分のデータを制御する権利を付与しており、データの削除を要求する権利(一般に「消去権」または「削除権」と呼ばれます)を含みます。ユーザーIDなどの個人データや**個人を特定できる情報(PII)**を保存している場合、ユーザーからのリクエストを受け取った際にこの情報を永久に削除することによって、適用されるプライバシーフレームワークに従う責任があります。詳細については、RTBFとクリエイターを参照してください。
代替案を検討する
データストア内の個人データに対して、最も簡単な自動化オプションは、RTBF削除テンプレートを構成することです。RobloxがRTBFリクエストを処理すると、カスタムコードをホストすることなく、一致する標準または順序付きデータストアエントリを自動的に削除します。テンプレートを構成し、サポートされているパターンを確認するには、データストアの忘れられる権利(RTBF)を参照してください。
このページのウェブフックワークフローは、次の場合に役立ちます:
- データストアの外に個人データを保存している。
- データストアのスキーマまたはユースケースがRTBF削除テンプレートでサポートされていない。
- カスタム処理または削除ロジックが必要である。
クリエイターウェブフックワークフロー
このチュートリアルでは、ウェブフックを設定する方法と、Open Cloud API for data storesを使用して消去権リクエストの処理を自動化するローカルで実行されるDiscordボットを作成する方法を示します。このプロセスのワークフローは次のとおりです:
- Robloxサポートがユーザーからの消去権リクエストを受け取ります。
- Robloxウェブフックがトリガーされ、ペイロードにユーザーIDと、ユーザーが参加したゲームのスタートプレイスIDのリストが含まれます。
- ボットがこれらのウェブフック通知をリッスンし、その真正性を確認し、Open Cloud API for data storesを利用してデータストアに保存されたPIIデータを削除します。
- ボットは、削除ステータスをDiscordのウェブフックメッセージに応答します。

サードパーティ統合でウェブフックを構成する
ボットを作成する前に、サードパーティのメッセージングアプリケーションでウェブフック統合を持つサーバーを設定します。その後、サーバーを使用してクリエイターダッシュボードでウェブフックを構成します。
サーバーを設定する
以下の手順は、Discordを使用してサーバーを設定する方法を示しています。
新しいDiscordサーバーを作成します。プロセスに不慣れな場合は、Discordサポートを参照してください。
サーバーは自動的にデフォルトチャネルとして**#generalチャネルを作成します。#generalチャネルのチャネルを編集**アイコンをクリックします。
権限の下で、チャネルをプライベートに設定します。
新しいサーバーでウェブフック統合を作成し、名前をRTBF Hookにします。プロセスに不慣れな場合は、Discordサポートを参照してください。
ウェブフックURLをコピーして、安全な場所に保存します。信頼できるチームメンバーのみがアクセスできるようにし、URLが漏洩すると悪意のある行為者が偽のメッセージを送信し、ユーザーデータを削除する可能性があります。
Robloxでウェブフックを構成する
サードパーティサーバーのURLを取得したら、それを使用してクリエイターダッシュボードでウェブフックを構成することができます。次の設定を行うことを確認してください:
- DiscordサーバーのURLをウェブフックURLとして追加します。
- カスタムシークレットを含めます。シークレットは構成を完了するためにオプションですが、悪意のある行為者がRobloxを偽装してデータを削除するのを防ぐために含めるべきです。シークレットの使用に関する詳細は、ウェブフックセキュリティの確認を参照してください。
- トリガーの下で消去権リクエストを選択します。
テスト応答ボタンを使用してウェブフックをテストし、Robloxからサーバーの**#general**チャネルに通知が届くかどうかを確認できます。通知が届かない場合は、再試行するか、サーバー設定を確認してエラーをトラブルシューティングしてください。
ボットを構成する
ウェブフックを追加した後、次の手順でボットを構成します。詳細については、Discordのドキュメントを参照してください。
- アプリケーションページに移動します。
- 新しいアプリケーションを作成し、名前をRTBF Botにします。
- システムはボットの一般情報設定にリダイレクトします。アプリケーションIDをコピーして、安全な場所に保存します。
- 設定メニューの下でOAuth2を選択します。
- OAuth2設定に移動します。
- ボットスコープを有効にすると、追加のボット権限のリストが表示されます。
- 管理者権限を追加します。変更を保存します。
- 生成されたURLを保存します。
- 生成されたURLをコピーします。アプリケーション設定ページを閉じないでください。
- 生成されたURLに移動します。ターゲットサーバーを選択します。続行ボタンをクリックし、その後承認ボタンをクリックします。
- アプリケーション設定ページに戻り、ボット設定に移動します。
- 特権ゲートウェイインテントセクションで、メッセージコンテンツインテントを有効にします。
- ボット設定 > ボットを作成セクションで、後の手順のためにボットトークンを安全な場所に保存します。トークンが表示されない場合は、トークンをリセットボタンをクリックして新しいトークンを生成します。
Open Cloud APIキーを作成する
サードパーティボットがユーザーのPIIデータを保存するためにデータストアにアクセスできるようにするために、Open Cloud APIキーを作成し、データ削除のためにデータストアのエントリ削除権限を追加します。PIIを保存するために順序付きデータストアを使用する場合は、順序付きデータストアの書き込み権限も追加する必要があります。完了したら、APIキーを安全な場所にコピーして保存し、後の手順で使用します。
ゲームとプレイスの識別子を取得する
ボットがユーザーによって削除を要求されたPIIデータを見つけるために、ボットを使用する予定のすべてのゲームの次の識別子を取得します:
- ゲームの一意の識別子であるユニバースID。
- ゲームのスタートプレイスの一意の識別子であるスタートプレイスID。
これらの識別子を取得するには:
クリエイターダッシュボードに移動します。
ゲームのサムネイルにカーソルを合わせ、⋯ボタンをクリックし、それぞれユニバースIDをコピーとスタートプレイスIDをコピーを選択します。

スクリプトを追加する
ウェブフック、ボット、およびデータストア用のAPIキーの設定が完了したら、ボットの自動化ロジックを実装するスクリプトにそれらを追加します。以下の例はPython 3を使用しています。
次のコマンドを使用してPythonライブラリをインストールします:
ライブラリのインストールpip3 install discordpip3 install requestspip3 install urllib3==1.26.6ボットロジックの異なる部分に対応する次のスクリプトを同じディレクトリにコピーして保存します:
bot_config.pyBOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# スタートプレイスIDから# (ユニバースID、(データストア名、スコープ、エントリーキー)のリスト)への辞書# 標準データストアに保存されたユーザーデータはこれらのエントリーの下で削除されますSTANDARD_DATA_STORE_ENTRIES = {# スタートプレイスID111111111: (# ユニバースID222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# スタートプレイスIDから# (ユニバースID、(データストア名、スコープ、エントリーキー)のリスト)への辞書# 順序付きデータストアに保存されたユーザーデータはこれらのエントリーの下で削除されますORDERED_DATA_STORE_ENTRIES = {111111111: (222222222,[("OrderedDataStore1", "Scope2", "Key4_{user_id}")])}data_stores_api.pyimport requestsimport bot_configfrom collections import defaultdict"""ユーザーIDに対してSTANDARD_DATA_STORE_ENTRIESで構成されたすべてのエントリーを削除するためにデータストアOpen Cloud APIを呼び出します。成功した削除と削除失敗のリストを返します。"""def delete_standard_data_stores(user_id, start_place_ids):successes = defaultdict(list)failures = defaultdict(list)for owned_start_place_id in bot_config.STANDARD_DATA_STORE_ENTRIES:if owned_start_place_id not in start_place_ids:continueuniverse_id, universe_entries = bot_config.STANDARD_DATA_STORE_ENTRIES[owned_start_place_id]for (data_store_name, scope, entry_key) in universe_entries:entry_key = entry_key.replace("{user_id}", user_id)response = requests.delete(f"https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores/datastore/entries/entry",headers={"x-api-key": bot_config.OPEN_CLOUD_API_KEY},params={"datastoreName": data_store_name,"scope": scope,"entryKey": entry_key})if response.status_code in [200, 204]:successes[owned_start_place_id].append((data_store_name, scope, entry_key))else:failures[owned_start_place_id].append((data_store_name, scope, entry_key))return successes, failures"""ユーザーIDに対してORDERED_DATA_STORE_ENTRIESで構成されたすべてのエントリーを削除するために順序付きデータストアOpen Cloud APIを呼び出します。成功した削除と削除失敗のリストを返します。"""def delete_ordered_data_stores(user_id, start_place_ids):successes = defaultdict(list)failures = defaultdict(list)for owned_start_place_id in bot_config.ORDERED_DATA_STORE_ENTRIES:if owned_start_place_id not in start_place_ids:continueuniverse_id, universe_entries = bot_config.ORDERED_DATA_STORE_ENTRIES[owned_start_place_id]for (data_store_name, scope, entry_key) in universe_entries:entry_key = entry_key.replace("{user_id}", user_id)response = requests.delete(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{universe_id}/orderedDatastores/{data_store_name}/scopes/{scope}/entries/{entry_key}",headers={"x-api-key": bot_config.OPEN_CLOUD_API_KEY})if response.status_code in [200, 204, 404]:successes[owned_start_place_id].append((data_store_name, scope, entry_key))else:failures[owned_start_place_id].append((data_store_name, scope, entry_key))return successes, failuresmessage_parser.pyimport timeimport hmacimport hashlibimport reimport base64import bot_config"""受信したメッセージのRoblox署名とタイムスタンプを解析します。フッターはウェブフックシークレットを設定した場合のみ設定されます。"""def parse_footer(message):if not message.embeds[0].footer or \not message.embeds[0].footer.text:return "", 0footer_match = re.match(r"Roblox-Signature: (.*), Timestamp: (.*)",message.embeds[0].footer.text)if not footer_match:return "", 0else:signature = footer_match.group(1)timestamp = int(footer_match.group(2))return signature, timestamp"""構成されたシークレットでRoblox署名を検証して有効性を確認します。"""def validate_signature(message, signature, timestamp):if not message or not signature or not timestamp:return False# 300秒のウィンドウ内でのリプレイ攻撃を防ぎますrequest_timestamp_ms = timestamp * 1000window_time_ms = 300 * 1000oldest_timestamp_allowed = round(time.time() * 1000) - window_time_msif request_timestamp_ms < oldest_timestamp_allowed:return False# 署名を検証しますtimestamp_message = "{}.{}".format(timestamp, message.embeds[0].description)digest = hmac.new(bot_config.ROBLOX_WEBHOOK_SECRET.encode(),msg=timestamp_message.encode(),digestmod=hashlib.sha256).digest()validated_signature = base64.b64encode(digest).decode()if signature != validated_signature:return False# 有効な署名return True"""Discordで受信したウェブフックメッセージを解析します。ユーザーIDを抽出し、受信したタイムスタンプに基づいてリプレイ攻撃を防ぎ、構成されたシークレットでRoblox署名を検証して有効性を確認します。"""def parse_message(message):# 受信したメッセージからユーザーIDとゲームIDを解析しますif len(message.embeds) != 1 or \not message.embeds[0].description:return "", []description_match = re.match(r"You have received a new notification for Right to Erasure for the User Id: (.*) in " +r"the game\(s\) with Ids: (.*)",message.embeds[0].description)if not description_match:return "", []user_id = description_match.group(1)start_place_ids = set(int(item.strip()) for item in description_match.group(2).split(","))signature, timestamp = parse_footer(message)if validate_signature(message, signature, timestamp):return user_id, start_place_idselse:return "", []discord_bot.pyimport discordimport bot_configimport data_stores_apiimport message_parserdef run():intents = discord.Intents.default()intents.message_content = Trueclient = discord.Client(intents=intents)@client.eventasync def on_ready():print(f"{client.user}は消去権メッセージをリッスンしています")"""Robloxからのウェブフックメッセージのハンドラー"""@client.eventasync def on_message(message):# メッセージを解析して検証しますuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# 標準データストアのユーザーデータを削除します[successes, failures] = data_stores_api.delete_standard_data_stores(user_id, start_place_ids)if successes:await message.reply(f"ユーザーID: {user_id}の標準データストアデータを削除しました。データ: {dict(successes)}")if failures:await message.reply(f"ユーザーID: {user_id}の標準データストアデータの削除に失敗しました。データ: {dict(failures)}")# 順序付きデータストアのユーザーデータを削除します[successes, failures] = data_stores_api.delete_ordered_data_stores(user_id, start_place_ids)if successes:await message.reply(f"ユーザーID: {user_id}の順序付きデータストアデータを削除しました。データ: {dict(successes)}")if failures:await message.reply(f"ユーザーID: {user_id}の順序付きデータストアデータの削除に失敗しました。データ: {dict(failures)}")client.run(bot_config.BOT_TOKEN)if __name__ == "__main__":run()bot_config.pyファイルのボットのメイン設定について:
- BOT_TOKENをボットによって生成されたトークンに設定します。
- OPEN_CLOUD_API_KEYを作成したAPIキーに設定します。
- ROBLOX_WEBHOOK_SECRETをクリエイターダッシュボードでウェブフックを構成したときに設定したシークレットに設定します。
- 削除する各レコードのデータストアを見つけるためのSTANDARD_DATA_STORE_ENTRIESおよびORDERED_DATA_STORE_ENTRIES辞書に:
- コピーしたスタートプレイスIDをキーとして追加します。
- ユニバースIDをタプル値の最初の要素として追加します。
- タプルの2番目の要素をデータストアの名前、スコープ、エントリーキー名、および関連するユーザーIDに置き換えます。異なるデータスキーマを使用している場合は、独自のデータスキーマに一致するように修正します。
ボットを実行するために次のコマンドを実行します:
Discordボットを実行python3 discord_bot.pyボットは消去権リクエストのためにRobloxウェブフックをリッスンし、対応するデータストアを削除するためにOpen Cloudエンドポイントを呼び出します。
テスト
カスタムプログラムが消去権リクエストを適切に処理し、PIIデータを削除できることを確認するために、テストメッセージを作成して実行できます:
次のリクエストボディを持つHTTP POSTリクエストをDiscordウェブフックサーバーに送信します:
リクエストの例curl -X POST {serverUrl}-H 'Content-Type: application/json'-d '{"embeds":[{"title":"RightToErasureRequest","description":"You have received a new notification for Right to Erasure for the User Id: {userIds} in the game(s) with Ids: {gameIds}","footer":{"icon_url":"https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png","text":"Roblox-Signature: {robloxSignature}, Timestamp: {timestamp}"}}]}'ウェブフックシークレットがある場合:
- ウェブフックシークレットキーにHMAC-SHA256エンコーディングを適用してRoblox-Signatureを生成します。
- 現在の時間をUTCタイムスタンプ(秒単位)としてTimestampに設定します。
次の形式でdescriptionをまとめます:
説明フィールド形式{Timestamp}. You have received a new notification for Right to Erasure for the User Id: {userId} in the game(s) with Ids: {gameIds}`.例えば:
説明フィールドの例1683927229. You have received a new notification for Right to Erasure for the User Id: 2425654247 in the game(s) with Ids: 10539205763, 13260950955
あなたのプログラムは、メッセージが公式のRobloxソースからのものであることを識別できるはずです。なぜなら、メッセージをシークレットでエンコードしたからです。その後、リクエストに関連するPIIデータを削除する必要があります。
{
"embeds": [
{
"title": "RightToErasureRequest",
"description": "You have received a new notification for Right to Erasure for the User Id: 2425654247 in the game(s) with Ids: 10539205763, 13260950955",
"footer": {
"icon_url": "https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}