一般資料保護條例 (GDPR) 是一個歐洲的資料保護和隱私權、隱私法規。它賦予個人權利要求個人資料的刪除,也就是「刪除權」。如果您存儲任何個人식別資訊 (例如用戶的 User ID ),您必須遵守 GDPR
而不是手動處理請求,您可以 設置 webhook 並使用第三方傳送應用程式內的機器人來自動化過程。作為資料存儲的最常見方式,資料存儲 在 Guilded 或 Discord 中使用 Open Cloud API 為資料存儲 來刪除資料作為自動化解決方案。這個教學提
工作流程
完成此教學後,您應該能創建一個本地執行的自訂程式,自動化對用戶的右鍵清除請求的處理。 此過程的工作流程如下:
- Roblox 支援受到用戶的消去請求。
- Roblox 網路連接已啟動,包含使用者 ID 和一組 Start Place ID 對應的體驗,在載入時傳送到指定位置。
- 您的機器人聽到這些網路通知,並確認其真實性,並使用 開啟雲端 API 存取資料存放庫 來刪除存放在資料存放庫中的 PII 資料。
- 機器人會在 Discord 或 Guilded 回應刪除狀態的 Webhook 訊息。
與第三方統合網路連接器設定
在創建機器人之前,設置一個 webhook 集成在第三方訊息應用程序上的伺服器。然後使用服務器在創作者儀表板上設置 webhook。
設置伺服器
下列步驟顯示如何使用 Guilded 或 Discord 設置伺服器。
- 創建一個新的 Guilded 伺服器。如果您對此過程不熟悉,請參閱 Guilded Support。
- 在 隱私 設定下,將伺服器設為私人。伺服器會自動建立一個私人 #general 頻道作為您的預設頻道。
- 與新服務器建立 webhook 集成,並且設定一個簡單於理解的名稱,例如 GDPR Hook。如果您對此過程不熟悉,請參閱 團隊支持。
- 複製 webhook URL 並將其存放在安全位空間。 只有信任的團隊成員才能存取,因為泄露 URL 可能會啟用惡意的發件來偷走使用者資料。
在 Roblox 上設定 Webhook
獲得第三方伺服器網址後,使用它來 在創作者控制板上配置網路通知。請確認您執行以下設定:
- 將聯盟或 Discord 伺服器網址加入 Webhook 網址 。
- 包含自訂 秘密 。雖然秘密是可選項完成設置,但您應該包含一個來防止惡意者偽裝 Roblox 並刪除您的資料。有關使用秘密的更多資訊,請參閱 驗證網路釘子安全性。
- 選擇 右向擊殺請求 在 觸發 下。
您可以使用 測試回應 按鈕來測試是否在 Roblox 的 # général 頻道從服務伺服器收到通知。如果您沒有收到通知,請嘗試再次或檢查您的服務器設定來排查錯誤。
設定機器人
在添加網路連接器後,使用它來設定機器人:
按一下其圖示或使用快捷方式開啟 所有服務器 列表:
- CtrlS 在Windows上。
- ⌘S 在 Mac 上。
選擇您的服務器以收到正確的擊殺通知。
在 服務器主頁 下拉欄表,並選擇 管理機器人 。
服務器會自動創建私人 # general 頻道為您的預設頻道。
點擊 創建機器人 按鈕,並輸入機器人名稱。您的公會會帶您到機器人配置頁面。
選擇機器人設定頁面上的 API 區域。
在 代幣 區域,按一下 生成代幣 按鈕。
保存並存放生成的代幣在安全的位空間。
創建開放雲 API 鑰匙
要允許您的第三方機器人存取您的資料儲存以儲存使用者的PII資料,請創建一個名為Open Cloud API Key的API鑰匙,可以存取您的體驗並且添加資料儲存的刪除權限。如果您使用訂購的資料儲存來
獲取體驗和地點的標識
要讓機器人找到用戶要求刪除的PII資料,請取得所有體驗的識別器,並將其與機器人對話:
- 宇宙 ID,您體驗的獨一無二的識別器。
- 開始地點 ID,體驗的開始地點的獨一標識。
若要取得此類標識,請在 創作者面板 上的 創作頁面 開啟。然後選擇體驗並複製 宇宙 ID 和 2>開始位置 ID2>。
添加指令碼
完成設定 webhook、bot 和 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}")])}數據儲存_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, 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 檔案中 for主配置的機器人:
- 將 DISCORD_BOT_TOKEN 或 GUILDED_BOT_TOKEN 設為你的機器人生成的代幣。
- 將 OPEN_CLOUD_API_KEY 設定為您創建的 API 鑰匙。
- 將 ROBLOX_WEBHOOK_SECRET 設定為您在創作者後台設定網路連接時設定的秘密。
- 在 STANDARD_DATA_STORE_ENTRIES 和 ORDERED_DATA_STORE_ENTRIES 典型詞典中查找每個記錄的數據存取庫以刪除:
- 將您複製的開始地點 ID 加入鑰匙。
- 將宇宙 ID 作為套件值的第一個元素。
- 將第二個元素的 tuple 變更為您的資料儲存的名稱、範圍、入口鑰匙名稱和協議名稱。 如果您使用不同的資料模型,請修改以反映您自己的資料模型。
執行以下指令來執行機器人:
執行團體機器人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}"}}]}'如果您有 webhook 秘密:
- 使用 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://建立、創作reate.roblox.com/ashboard/資產/webhook/roblox_logo_metal.png"
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}