El Reglamento General de Protección de Datos (RGDP) es un reglamento europeo sobre protección de datos y privacidad. Otorga a los individuos el derecho a solicitar la eliminación de sus datos personales, conocido como el derecho a la eliminación. Si almacena cualquier Información Personalmente Identificable (Información de Identificación Personal (IIP)) de sus usuarios, como sus ID de usuario, debe cum
En lugar de manejar solicitudes manualmente, puede configurar un webhook y usar un bot dentro de una aplicación de mensajería de terceros para automatizar el proceso. Como datos se almacenan siendo la forma más común de almacenar datos PII, este tutorial proporciona un ejemplo de cómo crear un bot dentro de Guilded o Discord que usa la API de Open Cloud para almacenar
Flujo de trabajo
Al completar este tutorial, debería poder crear un programa personalizado que se ejecuta localmente para automatizar el manejo de solicitudes de eliminación de derechos de los usuarios. El flujo de trabajo para este proceso es el siguiente:
- El soporte de Roblox recibe una solicitud de eliminación de un usuario.
- El webhook de Roblox se activa, que contiene el ID de usuario y una lista de ID de lugar de inicio para las experiencias que se han unido en el payload.
- Tu bot escucha estas notificaciones de webhook, verifica su autenticidad y utiliza la API de Open Cloud para almacenar datos para eliminar los datos almacenados en los almacenes de datos.
- El bot responde a la solicitud de webhook en Discord o Guilded con el estado de eliminación.
Configurando un Webhook con integración de terceros
Antes de crear un bot, configura un servidor con integración de webhook en la aplicación de mensajería de terceros. Luego usa el servidor para configurar un webhook en el Panel de Creator.
Configurando un servidor
Las siguientes etapas muestran cómo configurar el servidor utilizando Guilded o Discord.
- Crea un nuevo servidor Guilded. Si no estás familiarizado con el proceso, see Soporte Guilded.
- Bajo las configuraciones de privacidad , establece el servidor como servidor privado. El servidor crea automáticamente un canal privado de # general como canal predeterminado.
- Cree una integración de webhook con el nuevo servidor y le dé un nombre que pueda entender fácilmente, como GDPR Hook. Si no estás familiarizado con el proceso, see Soporte de Gremio .
- Copia la URL del webhook y almacénala en un lugar seguro. Solo permite que los miembros de tu equipo de confianza accedan a ella, ya que filtrar la URL puede permitir a los actores maliciosos enviar mensajes falsos y potencialmente eliminar tus datos de usuario.
Configurando un Webhook en Roblox
Después de obtener la URL del tercer servidor, úsela para configurar un webhook en el Panel de Creator. asegúrese de realizar las siguientes configuraciones:
- Agregue la URL del servidor de Guilded o Discord como el URL del Webhook .
- Incluya un secreto personalizado. Aunque un secreto es opcional para completar la configuración, debe incluir uno para evitar que los actores maliciosos se hagan pasar por Roblox y eliminen sus datos. Para obtener más información sobre el uso de un secreto, consulte Verificando la seguridad del webhook.
- Seleccione Solicitud de borrado derecho debajo de Activadores .
Puedes probar el webhook usando el botón Respuesta de prueba para ver si recibes una notificación en tu canal # general de Roblox. Si no recibes la notificaciones, intenta de nuevo o revisa tus configuraciones del servidor para solucionar el error.
Configurando un Bot
Después de agregar el webhook, úselo para configurar el bot con los siguientes pasos:
Abre la lista de Todos los servidores haciendo clic en su icono o usando el atajo:
- CtrlS en Windows.
- ⌘S en Mac.
Seleccione su servidor para recibir notificaciones de derecho a eliminar.
Expand the list under Inicio del servidor and select Administrar Bots .
El servidor crea automáticamente un canal privado de # general como tu canal por defecto.
Haga clic en el botón Crear un bot y agregue un nombre de bot. Guilded lo redirige a la página de configuración del bot.
Seleccione la sección API en la página de configuración del bot.
En la sección Fichas, haz clic en el botón Generar ficha.
Guarde y almacene el token generado en un lugar seguro.
Crear una llave de Clave APIOpen Cloud
Para permitir que su tercer bot de terceros acceda a sus almacenes de datos para almacenar datos Información de Identificación Personal (IIP)de los usuarios, crea una llave de API de Open Cloud que puede acceder a sus experiencias y agregar el permiso de almacenamiento de datos de Eliminar entrada para la eliminación de datos. Si usa almacenes de datos de terceros para almacenar
Obtener identificadores de experiencias y lugares
Para que el bot localice los datos PII solicitados por los usuarios para eliminar, obtenga los siguientes identificadores de todas las experiencias que tiene la intención de usar el bot:
- El ID del Universo , el identificador único de tu experiencia.
- El ID del Lugar de Inicio , el identificador único del lugar de inicio de una experiencia.
Para obtener estos identificadores, abra la página Creaciones en Panel del Creador . Luego seleccione una experiencia y copie el ID del Universo y 2> Lugar de inicio de la ID2> .
Agregar Scripts
Después de configurar el webhook, el bot y la llave de API para almacenar datos, agrégelos a los scripts que implementan la lógica de automatización del bot. El siguiente ejemplo usa Python 3:
Instala bibliotecas de Python usando los siguientes comandos:
Instala bibliotecaspip3 install discordpip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6Copia y guarda los siguientes scripts que correspondan a diferentes partes de la lógica del bot en el mismo directorio:
bot_config.pyDISCORD_BOT_TOKEN = ""GUILDED_BOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Diccionario del ID del Lugar de Salida para# (ID del universo, lista de (nombres de almacenes de datos, rango y clave de entrada)) para# Almacenes de datos estándar# Los datos del usuario almacenados debajo de estas entradas se eliminaránSTANDARD_DATA_STORE_ENTRIES = {# ID de lugar de inicio111111111: (# ID del Universo222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# Diccionario del ID del Lugar de Salida para# (ID del universo, lista de (nombres de almacenes de datos, rango y clave de entrada)) para# Almacenamiento de datos ordenado# Los datos del usuario almacenados debajo de estas entradas se eliminaránORDERED_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# Evita que se repita el ataque dentro de la ventana de 300 segundosrequest_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# Valida la firmatimestamp_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# Firma válidareturn 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):# Parsley recibe un mensaje por el ID del usuario y el ID del juegoif 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):# Parsa y valida el mensajeuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# Elimina los almacenes de datos estándar que almacenan datos del usuario[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)}")# Elimina los datos almacenados de los usuarios[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()En el archivo bot_config.py para la configuración principal del bot:
- Establece DISCORD_BOT_TOKEN o GUILDED_BOT_TOKEN en el token generado por tu bot.
- Establece OPEN_CLOUD_API_KEY como la llave API que creaste.
- Establece ROBLOX_WEBHOOK_SECRET como el secreto que estableces al configurar el webhook en el Panel de control del creador.
- En STANDARD_DATA_STORE_ENTRIES y ORDERED_DATA_STORE_ENTRIES diccionarios para localizar el almacén de datos de cada registro para eliminar:
- Agregue sus ID de lugar de inicio copiados como claves.
- Añadir los ID del Universo como el primer elemento del valor de tu lista.
- Reemplace el segundo elemento del tutorial con el nombre, rango, nombre de la llave de entrada y el ID de usuario asociado de sus almacenes de datos. Si usa un diferente esquema de datos, modifique para que coincida con su propio esquema de datos de acuerdo.
Ejecuta el siguiente comando para ejecutar el bot:
Ejecutar Guilded Botpython3 guilded_bot.pyEl botón entonces comienza a escuchar y verificar las llamadas de API de Roblox para tener derecho a las solicitudes de eliminación y llama al punto de interconexión abierto para eliminar el tiendade datos correspondiente.
Testeo
Puede crear y ejecutar un mensaje de prueba para verificar que su programa personalizado puede manejar correctamente las solicitudes de eliminación y eliminar datos PII:
Envía una solicitud HTTP POST a tu servidor web de Guilded o Discord con el cuerpo de solicitud siguiente:
Solicitud de Ejemplocurl -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}"}}]}'Si tienes un secreto de webhook:
- Genera un Roblox-Signature al aplicar la codificación HMAC-SHA256 a tu clave de secreto webhook.
- Establezca el tiempo actual utilizando UTC timestamp en segundos como Timestamp .
Pon juntos el description en el siguiente formato:
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}`.Por ejemplo:
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
Su programa debería ser capaz de identificar que su mensaje es de la fuente oficial de Roblox ya que ha codificado el mensaje con su secreto. Luego debería eliminar los datos PII asociados con su solicitud.
Cuerpo de Ejemplo
{
"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://crear.roblox.com/dash/artefactos/webhook/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}