一般データ保護規範 (GD個人識別情報R) は、ヨーロッパのデータ保護とプライバシーに関する規制です。個人には、個人データの削除を求める権利 (「消去権」) が付与されます。ユーザーのユーザーIDなどの個人データを保存する場合、ユーザ
リクエストを手動で処理する代わりに、ウェブホークを設定し、サードパーティのメッセージアプリ内のボットを使用してプロセスを自動化できます。データストアが最も一般的な方法でデータを保存する場合、このチュートリアルでは、
ワークフロー
このチュートリアルを完了すると、ユーザーから右に消去リクエストを自動処理するローカル実行のカスタムプログラムを作成できるようになります。このプロセスのワークフローは次のとおりです:
- Roblox サポートは、ユーザーからエラーを修正する権利を受け取ります。
- Roblox Webhook がトリガーされ、ユーザー ID と、エクスペリエンスがプロジェクトに参加するためのスタート場所の ID のリストが含まれています。
- ボットは、これらのウェブホーク通知をリスニングし、その正当性を確認し、オープンクラウド API のデータストア を使用して、データストアに保存された PII データを削除します。
- ボットは、Discord または Guilded の Webhook メッセージに応答し、削除ステータ状況を持つ。
サードパーティ統合を使用したウェブクールを構成する
ボットを作成する前に、サーバーを Webhook インテグレーションを含むサードパーティメッセージアプリケーションで構成します。その後、サーバーを使用してクリエイターダッシュボードで Webhook を構成します。
サーバーのセットアップ
次のステップでは、Guilded または Discord を使用してサーバーを設定する方法を示しています。
- 新しいギルドサーバーを作成します。プロセスに熟悉していない場合は、ギルドサポートを参照してください。
- プライバシー設定の下で、サーバーをプライベートに設定します。サーバーは自動的にプライベートチャンネルを作成し、デフォルトチャンネルとして # general チャンネルを自動的に作成します。
- 新しいサーバーとウェブホークの統合を作成し、簡単に理解できる名前を付けます。例えば、GDPR Hook など。如果プロセスに熟悉でない場合は、ギルドサポート を参照してください。
- ウェブホーク URL をコピーし、安全な場プレースに保存します。信頼できるチームメンバーのみがアクセスできるようにし、URL を公開すると、ユーザーのデータを損傷する可能性があるため、これを許可しません。
Roblox(ロブロックス)oblox の Webhook の構成
サードパーティのサーバー URL を入手した後、クリエイターダッシュボードで ウェブホークを構成する を行うこと。次の設定を実行してください:
- ギルドされたまたは Discord サーバー URL を ウェブホーク URL として追加します。
- カスタムの 秘密 を含めること。配置を完了するオプションではありますが、悪意のあるアクターが Roblox を偽装し、データを削除することを防ぐためには、を含める必要があります。For more information on the usage of a secret, see Roblox の Webhook セキュリティを検証 .
- Select 右クリック トリガー の下で エラーを消去リクエスト を選択します。
テストレポート ボタンを使用して、ウェブホークをテストして、Roblox の # general チャンネルからサーバーの 通常 チャンネルから通知を受信するかどうかをテストできます。通知を受信できない場合は、1>テストレポート1> ボタンを再試行するか、サーバー設定をチェックしてエラーをトラブルシ
ボットの構成
ウェブコールを追加した後、次のステップを使用してボットを構成します:
アイコンをクリックするか、ショートカットを使用して すべてのサーバー リストを開きます:
- Ctrl S on Windows.
- ⌘S Mac の上で。
正しくエラーを受信するためにサーバーを選択します。
Expand the list under サーバーホーム and select ボットの管理 .
サーバーは、デフォルトのチャンネルとしてプライベートな # general チャンネルを自動的に作成します。
ボットの作成ボタンをクリックし、ボット名を追加します。ギルドは、ボット構成ページに移動します。
ボット構成ページの API セクションを選択します。
Under the トークン section, click the トークンを生成 button.
生成されたトークンを安全な場プレースに保存して保管する。
オープンクラウド API キーを作成する
ユーザーの PII データを保存するために、サードパーティボットがデータストアにアクセスするようにするには、オープンクラウド A個人識別情報 キーを作成 し、エクスペリエンスにアクセスし、データストアの読み込み権限を追加することです
エクスペリエンスと場所の識別子を取得する
ボットがユーザーの要求により削除する PII データを見つけるには、ボットを使用するすべてのエクスペリエンスの識別子を取得します:
- あなたのエクスペリエンスのユニークな識別子である 宇宙ID 。
- 場所の開始 ID、エクスペリエンスの開始場所のユニークな識別子。
これらの識別子を取得するには、クリエイターダッシュボード の クリエーション ページを開きます。その後、エクスペリエンスを選択し、宇宙 ID と2>開始場所 ID2> をコピーします。
スクリプトの追加
データストアのウェブホーク、ボット、API キーを設定した後、ボットの自動化ロジックを実装するスクリプトに追加します。次の例では、Python 3を使用しています:
次のコマンドを使用してPython ライブラリをインストールします:
ライブラリをインストールするpip3 install discordpip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6同じダイレクトリ内のボットロジックの異なる部分に対応するスクリプトをコピーして保存する:
bot_config.pyDISCORD_BOT_TOKEN = ""GUILDED_BOT_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}")])}データストア apiimport 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, failuresメッセージ_アナライザー.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 "", []guilded_bot.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.GUILDED_BOT_TOKEN)if __name__ == "__main__":run()ボットのメイン設定のファイル bot_config.py で:
- ボットのトークンを設定します DISCORD_BOT_TOKEN または GUILDED_BOT_TOKEN ボットのトークンを生成する。
- API キーとして OPEN_CLOUD_API_KEY を設定します。
- クリエイターダッシュボードのウェブホークを構成するときに、ROBLOX_WEBHOOK_SECRET をクリエーターダッシュボード密として設定します。
- In STANDARD_DATA_STORE_ENTRIES and ORDERED_DATA_STORE_ENTRIES dictionaries for locating the data store of each record to delet削除:
- コピーしたスタート場所の ID をキーとして追加します。
- ユニバースID をツプレットの値の最初の要素として追加します。
- 次のエレメントをツプリにあなたのデータストアのユーザーIDにマッチするように変更します。別のデータスキーマを使用する場合は、それに対応するユーザーIDを変更します。
ボットを実行するには、次のコマンドを実行してください:
ギルドボットを実行するpython3 guilded_bot.pyボットは、[削除リクエスト] と [呼び出し] のために Roblox ウェブコールをリスニングし、Open Cloud エンドポイントを呼び出して、対応するデータストアを削除します。
テスト
カスタムプログラムが正しくエラーを処理し、PII データを削除することができるかをテストするテストメッセージを作成して実行できます:
以下のリクエストボディで、ギルドedまたは Discord ウェブフークサーバーに HTTP POST リクエストを送信します:
リクエストの例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}"}}]}'ウェブホークの秘密がある場合:
- Roblox-Signature を生成するには、ウェブホークの秘密キーにHMAC-SHA256の暗号化を適用します。
- 現在の時刻を Timestamp 秒で UTC タイムスタンプ として設定します。
次の形式で 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/dash/アセット/ウェブコール/roblox_logo_metal.png"、
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}