一般データ保護規則 (GDPR) は、データ保護およびプライバシーに関する欧州の規則です。これは、個人が自分の個人データの削除を要求する権利を与えるものであり、これを削除の権利と呼びます。ユーザーのユーザーIDなどの個人を特定できる情報 (PII) を保存している場合、ユーザーからの要求に応じてこの情報を削除することでGDPRの要件に従わなければなりません。
要求を手動で処理するのではなく、Webhookを設定し、サードパーティのメッセージングアプリケーション内でボットを使用してプロセスを自動化できます。PIIデータを保存するための最も一般的な方法であるデータストアを利用して、このチュートリアルでは、データストア用のオープンクラウドAPIを使用してPIIデータを削除するボットをDiscord内で作成する方法の例を提供します。
ワークフロー
このチュートリアルを完了すると、ユーザーからの削除要求を自動的に処理するローカルで実行されるカスタムプログラムを作成できるようになります。このプロセスのワークフローは次のとおりです。
- Robloxサポートがユーザーからの削除要求を受け取ります。
- RobloxのWebhookがトリガーされ、ペイロードにユーザーIDと、ユーザーが参加したゲームのスタートプレイスIDのリストが含まれます。
- あなたのボットはこれらのWebhook通知を受信し、その信頼性を検証し、オープンクラウドAPI for data storesを利用してデータストアに保存されたPIIデータを削除します。
- ボットはDiscord内のWebhookメッセージに削除状況を応答します。

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

スクリプトの追加
Webhook、ボット、データストアの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のすべてのエントリーを削除するためにデータストアオープンクラウド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のすべてのエントリーを削除するために順序付けられたデータストアオープンクラウド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署名とタイムスタンプを解析します。フッターはWebhookシークレットを設定した場合のみ設定されます。"""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上で受信したWebhookメッセージを解析します。ユーザー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からのWebhookメッセージのハンドラー"""@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"{user_id}の標準データストアデータを削除しました:" +f"データ: {dict(successes)}")if failures:await message.reply(f"{user_id}の標準データストアデータの削除に失敗しました:" +f"データ: {dict(failures)}")# 順序付けられたデータストアのユーザーデータを削除します[successes, failures] = data_stores_api.delete_ordered_data_stores(user_id, start_place_ids)if successes:await message.reply(f"{user_id}の順序付けられたデータストアデータを削除しました:" +f"データ: {dict(successes)}")if failures:await message.reply(f"{user_id}の順序付けられたデータストアデータの削除に失敗しました:" +f"データ: {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にクリエイターダッシュボードでWebhookを設定したときに設定したシークレットを設定します。
- STANDARD_DATA_STORE_ENTRIESおよびORDERED_DATA_STORE_ENTRIESの辞書の中で削除する各レコードのデータストアを特定するために:
- コピーしたスタートプレイスIDをキーとして追加します。
- タプル値の最初の要素としてユニバースIDを追加します。
- タプルの二番目の要素をデータストアの名前、スコープ、エントリーキー名、関連するユーザーIDに置き換えます。異なるデータスキーマを使用している場合は、自分のデータスキーマに合わせて変更してください。
ボットを実行するために、次のコマンドを実行します:
Discordボットを実行python3 discord_bot.pyその後、ボットは削除要求のためにRobloxのWebhookを監視し、対応するデータストアを削除するためにオープンクラウドエンドポイントを呼び出します。
テスト
削除要求を適切に処理し、PIIデータを削除できることを確認するために、テストメッセージを作成して実行できます:
Discord WebhookサーバーにHTTP POSTリクエストを送信し、以下のリクエストボディを使用します:
リクエストの例curl -X POST {serverUrl}-H 'Content-Type: application/json'-d '{"embeds":[{"title":"削除要求","description":"ユーザーID: {userIds}の新しい削除要求通知を受信しました。ゲームID: {gameIds}のゲームで。","footer":{"icon_url":"https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png","text":"Roblox-Signature: {robloxSignature}, Timestamp: {timestamp}"}}]}'Webhookシークレットがある場合:
- WebhookシークレットキーにHMAC-SHA256エンコーディングを適用してRoblox-Signatureを生成します。
- 現在の時刻をUTCタイムスタンプ(秒単位)としてTimestampに設定します。
次の形式でdescriptionをまとめます:
説明フィールドの形式{Timestamp}. ユーザーID: {userId}の新しい削除要求通知を受信しました。ゲームID: {gameIds}でのゲーム。たとえば:
説明フィールドの例1683927229. ユーザーID: 2425654247の新しい削除要求通知を受信しました。ゲームID: 10539205763, 13260950955でのゲーム
プログラムは、メッセージが公式のRobloxソースからのものであることを識別できるはずです。これは、メッセージをシークレットでエンコードしたためです。それにより、リクエストに関連するPIIデータを削除できるようになります。
ボディの例
{
"embeds": [
{
"title": "削除要求",
"description": "ユーザーID: 2425654247の新しい削除要求通知を受信しました。ゲームID: 10539205763, 13260950955でのゲーム。",
"footer": {
"icon_url": "https://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}