一般データ保護規則 (GDPR) は、データ保護とプライバシーに関する欧州規制です。それは個人に、知られる個人データの削除をリクエストする権利、「消去権」と呼ばれるものを付与します。ユーザーのユーザーIDなどの個人識別情報を保存する場合、ユーザーのリクエストを受け取ったときにこの情報を削除して、GDPR要件に従わなければなりません。
リクエストを手動で処理するのではなく、ウェブホックを設定し、サードパーティのメッセージアプリ内のボットを使用してプロセスを自動化できます。As データストア が PII データを保存する最も一般的な方法であるため、このチュートリアルでは、オープンクラウド API for データストア を使用して、自動化ソリューションとして PII データを削除するボットを Guilded または Discord 内で作成する方法の例を提供します。
ワークフロー
このチュートリアルを完了すると、ユーザーの消去リクエストの処理を自動化するローカル実行のカスタムプログラムを作成できるはずです。このプロセスのワークフローは次のとおりです:
- Roblox サポートは、ユーザから消去リクエストを受け取ります。
- Roblox ウェブホックがトリガーされ、ユーザー ID と、ペイロードに参加したエクスペリエンスのスタートプレース ID のリストが含まれています。
- ボットはこれらの Webhook 通知を聞き、その正確性を確認し、データストアに保存された PII データを削除するために オープンクラウドAPI for data stores を使用します。
- ボットは、Discord または Guilded の Webhook メッセージに削除ステータスで応答します。

サードパーティ統合で Webhook を構成する
ボットを作成する前に、サードパーティのメッセージアプリケーションで Webhook 統合を持つサーバーを設定します。次に、サーバーを使用してクリエイターダッシュボード上の Webhook を構成します。
サーバーを設定する
次のステップでは、Guilded または Discord を使用してサーバーを設定する方法を示します。
- 新しい Guilded サーバーを作成します。プロセスに精通していない場合は、Guilded サポート を参照してください。
- プライバシー 設定の下で、サーバーをプライベートに設定します。サーバーはデフォルトのチャネルとして、プライベートの #一般 チャンネルを自動的に作成します。
- 新しいサーバーと Webhook 統合を作成し、GDPR Hook など、簡単に理解できる名前を付けます。プロセスに精通していない場合は、ギルデッドサポート を参照してください。
- Webhook URLをコピーして、安全な場プレースに保存するURLを漏洩すると、悪意のあるアクターが偽のメッセージを送信し、ユーザーデータを削除する可能性があるため、信頼できるチームメンバーのみがアクセスできるようにすること。
Roblox で Roblox(ロブロックス)ebhook を構成する
サードパーティのサーバー URL を取得した後、クリエイターダッシュボードで ウェブホックを構成する を使用します。次の設クリエーターダッシュボードを実行したことを確認してください:
- Guilded または Discord サーバーの URL を Webhook URL として追加します。
- カスタムの 秘密 を含めます。設定を完了するためには秘密はオプションですが、悪意のあるアクターが Roblox を偽装してデータを削除するのを防ぐためには、1つ含める必要があります。秘密の使用に関する詳細情報は、ウェブホックのセキュリティを確認する を参照してください。
- Select 消去リクエストの右を選択 under トリガー .
Webhook を使用して テスト応答 ボタンを使用して、Roblox からサーバーの #一般 チャネルで通知を受け取るかどうかをテストできます。通知を受け取らない場合は、再試行したり、サーバー設定をチェックしてエラーを解決します。

ボットを構成する
Webhook を追加した後、次の手順でボットを構成します:
アイコンをクリックして すべてのサーバー リストを開き、またはショートカットを使用します:
- Ctrl S に Windows 上。
- ⌘ S に Mac 上で。
消去通知を受信するサーバーを選択します。
Expand the list under サーバーホーム and select ボットの管理 .
サーバーはデフォルトのチャネルとして、プライベートの #一般 チャンネルを自動的に作成します。
クリックする ボットを作成する ボタンとボット名を追加し、ギルデッドはボット構成ページにリダイレクトします。
ボット構成ページの API セクションを選択します。
トークン セクションで、 トークンを生成 ボタンをクリックします。
生成されたトークンを安全な場プレースに保存して保存する。
オープンクラウド API キーを作成する
ユーザーの PII データを保存するために、サードパーティのボットがデータストアにアクセスできるようにするには、オープンクラウド API キーを作成して、エクスペリエンスにアクセスし、データ削除のためのデータストアの 削除権限 を追加します。IIを保存するには、注文されたデータストアを使用する場合、注文されたデータストアの 書き込み 権限も追加する必要があります。完了後、API キーを安全な場所にコピーして保存して、後のステップで使用します。
エクスペリエンスと場所の識別子を取得する
ボットがユーザーが削除をリクエストした PII データを見つけるために、ボットを使用する予定のすべてのエクスペリエンスの次の識別子を取得します:
- ユニバースID 、あなたの経験の唯一の識別子。
- スタートプレースID 、エクスペリエンスのスタートプレースの唯一の識別子。
これらの識別子を取得するには:
ナビゲート to the クリエイターダッシュボード.
エクスペリエンスのサムネイルの上にマウスポインタを置き、 ⋯ ボタンをクリックし、それぞれ ユニバースIDをコピーする と スタート場所IDをコピーする を選択します。
スクリプトを追加
Webhook、ボット、およびデータストアの設定を完了した後、ボットの自動化ロジックを実装するスクリプトに追加します。次の例では、Python 3 を使用します:
次のコマンドを使用して Python ライブラリをインストールします:
ライブラリをインストールpip3 install guilded.py==1.8.0pip3 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}")])}データ_ストア_api.pyimport requestsimport bot_configfrom collections import defaultdict"""Calls Data Stores Open Cloud API to delete all entries for a user_id configured inSTANDARD_DATA_STORE_ENTRIES. Returns a list of successful deletions and failures to delete."""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"""Calls Ordered Data Stores Open Cloud API to delete all entries for a user_id configured inORDERED_DATA_STORE_ENTRIES. Returns a list of successful deletions and failures to delete."""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"""Parses received message for Roblox signature and timestamp, the footer is only set if youconfigured webhook secret"""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"""Verifies Roblox signature with configured secret to check for validity"""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"""Parses a received webhook messaged on Discord or Guilded. Extracts user ID, prevents replay attackbased on timestamp received, and verifies Roblox signature with configured secret to check forvalidity."""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 "", []ギルデッド_ボット.pyimport guildedimport jsonimport bot_configimport data_stores_apiimport message_parserdef run():client = guilded.Client()@client.eventasync def on_ready():print(f"{client.user} is listening to Right to Erasure messages")"""Handler for webhook messages from 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"Deleted standard data stores data for " +f"user ID: {user_id}, data: {dict(successes)}")if failures:await message.reply(f"Failed to delete standard data stores data for " +f"user ID: {user_id}, data: {dict(failures)}")# 注文されたデータストアユーザーデータを削除する[successes, failures] = data_stores_api.delete_ordered_data_stores(user_id, start_place_ids)if successes:await message.reply(f"Deleted ordered data stores data for " +f"user ID: {user_id}, data: {dict(successes)}")if failures:await message.reply(f"Failed to delete ordered data stores data for " +f"user ID: {user_id}, data: {dict(failures)}")client.run(bot_config.BOT_TOKEN)if __name__ == "__main__":run()ボットのメイン構成のファイル bot_config.py において:
- BOT_TOKEN をボットによって生成されたトークンに設定します。
- Set OPEN_CLOUD_API_KEY を作成した API キーとして設定します。
- クリエイターダッシュボードで Webhook を設定するときに設定したクリエーターダッシュボード密として ROBLOX_WEBHOOK_SECRET を設定します。
- 削除する各レコードのデータストアを見つけるための STANDARD_DATA_STORE_ENTRIES および ORDERED_DATA_STORE_ENTRIES 辞書で:
- コピーしたスタート地点IDをキーとして追加します。
- ユニバースIDをタプル値の最初の要素として追加する。
- トークンの 2番目の要素をデータストアの名前、スコープ、エントリキー名、および関連するユーザーIDに置き換えます。異なるデータスキームを使用する場合は、自分のデータスキームに合わせて変更します。
次のコマンドを実行してボットを実行します:
ギルデッドボットを実行python3 guilded_bot.pyボットは次に、消去リクエストと呼び出しのための Roblox ウェブホックを聞き始め、検証し、対応するデータストアを削除するためのオープンクラウドエンドポイン保管を呼び出します。
テスト
カスタムプログラムが消去リクエストと PII データを適切に処理できるかどうかを確認するためのテストメッセージを作成して実行できます:
次のリクエストボディで Guilded または Discord の Webhook サーバーに HTTP リクエストを送信します:
例のリクエスト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}"}}]}'Webhook の秘密がある場合:
- Webhook の秘密キーに HMAC-SHA256 エンコードを適用して Roblox-Signature を生成します。
- 秒単位の UTC タイムスタンプを使用して、現在の時間を Timestamp と設定します。
次の形式で description を組み合わせてください:
Description Field Format{Timestamp}. You have received a new notification for Right to Erasure for the User Id: {userId} in the game(s) with Ids: {gameIds}`.例えば:
Example Description Field1683927229. 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/dashboards/assets/webhooks/roblox_logo_metal.png"、
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}