全球数据保护和隐私法规赋予个人控制其数据的权利,包括请求删除数据的权利(通常称为“删除权”或“删除权利”)。如果您存储任何个人数据或个人可识别信息(PII),例如用户 ID,您有责任遵守适用的隐私框架,在收到用户请求后永久删除此信息。更多信息请参见被遗忘权与创作者。
考虑替代方案
对于数据存储中的个人数据,最简单的自动化选项是配置 RTBF 删除模板。当 Roblox 处理 RTBF 请求时,它会自动删除匹配的标准或有序数据存储条目,而无需您托管自定义代码。要配置模板并查看支持的模式,请参见数据存储被遗忘权(RTBF)。
本页面上的网络钩子工作流程在以下情况下有用:
- 您在数据存储之外存储个人数据。
- 您的数据存储架构或用例不受 RTBF 删除模板的支持。
- 您需要自定义处理或删除逻辑。
创作者网络钩子工作流程
本教程演示如何设置网络钩子并创建一个本地运行的 Discord 机器人,该机器人使用数据存储的开放云 API来自动处理删除权请求。该过程的工作流程如下:
- Roblox 支持团队收到用户的删除权请求。
- Roblox 网络钩子被触发,包含用户 ID 和他们在有效负载中加入的游戏的起始地点 ID 列表。
- 您的机器人监听这些网络钩子通知,验证其真实性,并利用数据存储的开放云 API删除存储在数据存储中的 PII 数据。
- 机器人在 Discord 中回复网络钩子消息,提供删除状态。

使用第三方集成配置网络钩子
在创建机器人之前,先在第三方消息应用程序上设置带有网络钩子集成的服务器。然后使用该服务器在创作者仪表板上配置网络钩子。
设置服务器
以下步骤展示如何使用 Discord 设置服务器。
创建一个新的 Discord 服务器。如果您不熟悉该过程,请参见Discord 支持。
服务器会自动创建一个**#general频道作为默认频道。单击#general频道的编辑频道**图标。
在权限下,将频道设置为私有。
创建与新服务器的网络钩子集成,将其命名为RTBF Hook。如果您不熟悉该过程,请参见Discord 支持。
复制网络钩子 URL 并将其存储在安全的地方。仅允许受信任的团队成员访问,因为泄露 URL 可能会使恶意行为者发送虚假消息并可能删除您的用户数据。
在 Roblox 上配置网络钩子
在获取第三方服务器 URL 后,使用它在创作者仪表板上配置网络钩子。确保执行以下设置:
- 将 Discord 服务器 URL 添加为网络钩子 URL。
- 包含自定义密钥。尽管密钥在完成配置时是可选的,但您应该包含一个以防止恶意行为者冒充 Roblox 并删除您的数据。有关密钥使用的更多信息,请参见验证网络钩子安全性。
- 在触发器下选择删除权请求。
您可以使用测试响应按钮测试网络钩子,以查看是否收到来自 Roblox 的通知,通知在您服务器的**#general**频道中。如果未收到通知,请重试或检查您的服务器设置以排除错误。
配置机器人
在添加网络钩子后,使用它配置机器人,步骤如下。有关更多信息,请参见Discord 文档。
- 导航到应用程序页面。
- 创建一个新应用程序并将其命名为RTBF Bot。
- 系统会将您重定向到机器人的常规信息设置。复制并安全保存其应用程序 ID。
- 在设置菜单中,选择OAuth2。
- 导航到OAuth2设置
- 启用机器人范围,显示额外的机器人权限列表。
- 添加管理员权限。保存更改。
- 保存生成的 URL。
- 复制生成的 URL。保持应用程序设置页面未关闭。
- 导航到生成的 URL。选择目标服务器。单击继续按钮,然后单击授权按钮。
- 返回应用程序设置页面并导航到机器人设置。
- 在特权网关意图部分,启用消息内容意图。
- 在机器人设置 > 构建机器人部分,安全保存机器人令牌以备后续步骤。如果您未看到令牌,请单击重置令牌按钮以生成新的令牌。
创建开放云 API 密钥
为了允许您的第三方机器人访问您的数据存储以存储用户的 PII 数据,创建一个开放云 API 密钥,该密钥可以访问您的游戏并添加数据存储的删除条目权限以进行数据删除。如果您使用有序数据存储来存储 PII,您还需要添加有序数据存储的写入权限。完成后,复制并安全保存 API 密钥以便在后续步骤中使用。
获取游戏和地点的标识符
为了让机器人找到用户请求删除的 PII 数据,请获取您打算为其使用机器人的所有游戏的以下标识符:
- 宇宙 ID,您游戏的唯一标识符。
- 起始地点 ID,游戏起始地点的唯一标识符。
要获取这些标识符:
导航到创作者仪表板。
将鼠标悬停在游戏缩略图上,单击**⋯按钮,分别选择复制宇宙 ID和复制起始地点 ID**。

添加脚本
在您完成设置网络钩子、机器人和数据存储的 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"""调用数据存储开放云 API 删除配置在STANDARD_DATA_STORE_ENTRIES 中的所有条目。返回成功删除和删除失败的列表。"""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"""调用有序数据存储开放云 API 删除配置在ORDERED_DATA_STORE_ENTRIES 中的所有条目。返回成功删除和删除失败的列表。"""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 签名和时间戳,页脚仅在您配置了网络钩子密钥时设置"""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 上接收到的网络钩子消息。提取用户 ID,基于接收到的时间戳防止重放攻击,并使用配置的密钥验证 Roblox 签名以检查有效性。"""def parse_message(message):# 解析接收到的消息以获取用户 ID 和游戏 IDif 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 的网络钩子消息"""@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"已删除用户 ID: {user_id} 的标准数据存储数据,数据: {dict(successes)}")if failures:await message.reply(f"未能删除用户 ID: {user_id} 的标准数据存储数据,数据: {dict(failures)}")# 删除有序数据存储用户数据[successes, failures] = data_stores_api.delete_ordered_data_stores(user_id, start_place_ids)if successes:await message.reply(f"已删除用户 ID: {user_id} 的有序数据存储数据,数据: {dict(successes)}")if failures:await message.reply(f"未能删除用户 ID: {user_id} 的有序数据存储数据,数据: {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 作为键添加。
- 将宇宙 ID 作为元组值的第一个元素添加。
- 将元组的第二个元素替换为数据存储的名称、范围、条目键名称和相关的用户 ID。如果您使用不同的数据架构,请相应地修改以匹配您自己的数据架构。
执行以下命令以运行机器人:
运行 Discord 机器人python3 discord_bot.py机器人将开始监听并验证 Roblox 网络钩子以处理删除权请求,并调用开放云端点以删除相应的数据存储。
测试
您可以创建并运行测试消息,以验证您的自定义程序是否能够正确处理删除权请求并删除 PII 数据:
向您的 Discord 网络钩子服务器发送 HTTP POST 请求,请求体如下:
示例请求curl -X POST {serverUrl}-H 'Content-Type: application/json'-d '{"embeds":[{"title":"RightToErasureRequest","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}"}}]}'如果您有网络钩子密钥:
- 通过对您的网络钩子密钥应用 HMAC-SHA256 编码生成 Roblox-Signature。
- 使用 UTC 时间戳(以秒为单位)设置当前时间作为 Timestamp。
将 description 组合成以下格式:
描述字段格式{Timestamp}. 您已收到关于用户 ID: {userId} 在游戏 ID: {gameIds} 中的删除权的新通知。例如:
示例描述字段1683927229. 您已收到关于用户 ID: 2425654247 在游戏 ID: 10539205763, 13260950955 中的删除权的新通知。
您的程序应该能够识别您的消息来自官方 Roblox 源,因为您使用密钥对消息进行了编码。然后,它应该删除与您的请求相关的 PII 数据。
{
"embeds": [
{
"title": "RightToErasureRequest",
"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"
}
}
]
}