一般數據保護規則(GDPR)是歐洲數據保護和隱私權、隱私的法規。它賦予個人要求刪除個人資料的權利,也稱為刪除權。如果您存儲任何 個人識別資訊 (PII) 的用戶,例如他們的使用者ID,您必須在收到使用者的請邀請後刪除此資訊,以遵守GDPR要求。
您可以設置網絡呼叫並使用第三方即時通訊應用程式內的機器人來自動化過程,而不是手動處理請求。因為 資料儲存庫 是儲存個人資訊資料最常見的方式,本教學提供了如何在Guilded或Discord中創建一個使用開放雲端API資料儲存庫來刪除個人資訊資料的自動化解決方案的範例。
工作流程
完成此教學後,您應該能夠創建一個本地運行的自定義程序,自動處理來自使用者的刪除請求。此過程的工作流程如下:
- Roblox 支持收到來自使用者的刪除請求。
- Roblox 網絡呼叫被觸發,包含使用者 ID 和他們加入的體驗的起始地址列表。
- 您的機器人聆聽這些網絡通知,驗證其真實性,並使用 開啟雲端 API 對數據存儲進行刪除 來刪除存儲在數據存儲中的個人資訊數據。
- 機器人會回應 Discord 或 Guilded 的網絡通訊訊息以刪除狀態。

配置第三方整合的網絡鉤子
在創建機器人之前,在第三方傳訊應用程式上設定一個具有網路端點整合的伺服器。然後使用伺服器來在創作者面板上配置網路呼叫。
設定伺服器
以下步驟顯示如何使用 Guilded 或 Discord 設置伺服器。
- 創建一個新的 Guilded 伺服器。如果您對過程不熟悉,請參閱 Guilded 支援。
- 在 隱私 設定下,將伺服器設為私人。伺服器會自動創建一個私人 #一般 通道作為您的預設頻道。
- 創建一個新服務器的網絡通訊整合,並給予它易於理解的名稱,例如 GDPR Hook 。如果您對過程不熟悉,請參閱Guilded支持。
- 複製網絡端點URL,並將其存放在安全的空間方。只允許信任的團隊成員訪問它,因為洩露 URL 可能會啟用惡意行為者發送假消息並可能刪除您的使用者資料。
在 Roblox 上配置網絡呼叫點
獲得第三方服務器URL後,使用它來在創作者後台上配置Webhook。請確認您執行以下設定:
- 將 Guilded 或 Discord 伺服器 URL 添加為 Webhook URL 。
- 包含自訂的 秘密 。雖然秘密是完成配置的選擇,但你應該包含一個以防止惡意行為者冒充 Roblox 並刪除你的資料。要了解有關秘密使用的更多信息,請參閱驗證Webhook安全。
- 在 觸發 下選擇 刪除請求權 。
您可以使用 測試回應 按鈕測試網絡來看您是否在 Roblox 的 #一般 通道中收到通知。如果您未收到通知,請再試一次或檢查服務器設定以解決錯誤。

配置機器人
在你添加網絡後,使用它來配置機器人以下步驟:
單擊其圖示以開啟 所有伺服器 列表,或使用捷徑方式:
- Ctrl S 在 Windows 上。
- ⌘ S 在 Mac 上。
選擇您要接收刪除通知的伺服器。
在 伺服器主頁 下擴展列表,然後選擇 管理機器人 。
伺服器會自動創建一個私人 #一般 通道作為您的預設頻道。
點擊 創建機器人 按鈕,並添加機器人名稱。Guilded 將你重定向到機器人配置頁面。
在機器人配置頁面上選擇 API 部分。
在 代幣 部分下,單擊 生成代幣 按鈕。
保存並將生成的代幣存放在安全的空間方。
創建開放雲端 API 鑰鍵
要允許您的第三方機器人存取您的數據儲存以存儲用戶的PII數據,創建一個開放雲API鑰匙,可以存取您的體驗並添加數據儲存的 刪除入口 權限以刪除數據。如果您使用訂購數據存儲來存儲 PII,您還需要添加訂購數據存儲的 寫入 權限。完成後,將 API 鑰匙複製並儲存在安全位置,以在後續步驟中使用。
獲得體驗和地點的標識符
為了讓機器人找到用戶要求刪除的 PII 數據,獲得您打算使用機器人的所有體驗的以下標識符:
- 宇宙ID ,您體驗的唯一標識。
- 開始位置ID ,體驗開始位置的唯一標識。
要獲得這些標識符:
導航到 創作者面板。
將鼠標懸停在體驗縮略圖上,點擊 ⋯ 按鈕,然後選擇 複製宇宙ID 和 複製起始位置ID ,分別。
新增腳本
完成設定網絡後,請將網絡、機器人和 API 鑰匙加入實現機器人自動化邏輯的腳本中。下面的例子使用了 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}")])}data_stores_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 "", []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.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加為 tuple 值的第一個元素。
- 將 tuple 的第二元素替換為您數據儲存的名稱、範圍、入口鑰匙名稱和相關的使用者ID。如果您使用不同的數據模型,則適當修改以符合自己的數據模型。
執行以下指令以運行機器人:
執行公會機器人python3 guilded_bot.py機器人然後開始聆聽和驗證 Roblox 網絡呼叫的刪除請求,並呼叫開放雲端端點刪除相應的數據存商店 商家。
測試
您可以創建並執行測試訊息來驗證您的自訂程式可以正確處理刪除請求和刪除 PII 資料:
以下是派送 HTTP POST 請求到你的 Guilded 或 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 放在以下格式中:
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://建立、創作reate.roblox.com/ashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}