Le règlement général sur la protection des données (GDPR) est un règlement européen sur la protection des données et de la confidentialité.Il accorde aux individus le droit de demander la suppression de leurs données personnelles, appelée le droit à l'oubli.Si vous stockez toute information personnellement identifiable (IPI) de vos utilisateurs, telle que leurs identifiants d'utilisateur, vous devez respecter les exigences du RGPD en supprimant cette information lorsque vous recevez la demande d'un utilisateur.
Au lieu de gérer les demandes manuellement, vous pouvez configurer un webhook et utiliser un bot dans une application de messagerie tiers pour automatiser le processus.Comme les magasins de données sont les plus courants pour stocker les données PII, ce tutoriel fournit un exemple sur la façon de créer un bot dans Guilded ou Discord qui utilise la Open Cloud API pour les magasins de données pour supprimer les données PII en tant que solution d'automatisation.
Flux de travail
Une fois ce tutoriel terminé, vous devriez être en mesure de créer un programme personnalisé localement exécutable qui automatise la gestion des demandes de suppression des utilisateurs.Le flux de travail pour ce processus est comme suit :
- Le support de Roblox reçoit une demande de suppression d'un utilisateur.
- Le webhook Roblox est déclenché, contenant l'ID utilisateur et une liste d'ID de lieu de départ pour les expériences auxquelles ils ont rejoint dans le payload.
- Votre bot écoute ces notifications de webhook, vérifie leur authenticité et utilise la Open Cloud API pour les magasins de données pour supprimer les données PII stockées dans les magasins de données.
- Le bot répond au message Webhook dans Discord ou Guilded avec le statut de suppression.

Configurer un webhook avec une intégration tiers
Avant de créer un bot, configurez un serveur avec une intégration de webhook sur l'application de messagerie tiers.Ensuite, utilisez le serveur pour configurer un webhook sur le tableau de bord du créateur.
Configurer un serveur
Les étapes suivantes montrent comment configurer le serveur en utilisant Guilded ou Discord.
- Créez un nouveau serveur Guilded. Si vous ne connaissez pas le processus, voir soutien Guilded.
- Dans les paramètres de confidentialité , définissez le serveur en privé.Le serveur crée automatiquement un canal privé # général comme chat (chat privé)par défaut.
- Créez une intégration de webhook avec le nouveau serveur et donnez-lui un nom que vous pouvez facilement comprendre, comme GDPR Hook .Si vous n'êtes pas familier avec le processus, voir soutien guidé.
- Copiez l'URL du webhook et stockez-la dans un emplacementsécurisé.Ne permettez d'y accéder qu'aux membres de l'équipe de confiance, car fuiter l'URL peut permettre aux acteurs malveillants d'envoyer des messages faux et de potentiellement supprimer les données de votre utilisateur.
Configurer un webhook sur Roblox
Après avoir obtenu l'URL du serveur tiers, utilisez-la pour configurer un webhook sur le tableau de bord du créateur.assurez-vous d'effectuer les paramètres suivants :
- Ajoutez l'URL du serveur Guilded ou Discord comme URL de Webhook .
- Incluez un secret personnalisé secret .Bien que le secret soit facultatif pour terminer la configuration, vous devez en inclure un pour empêcher les acteurs malveillants de se faire passer pour Roblox et de supprimer vos données.Pour plus d'informations sur l'utilisation d'un secret, voir Vérifier la sécurité du webhook.
- Sélectionnez Droit à la demande d'effacement sous déclencheurs .
Vous pouvez tester le webhook en utilisant le bouton Réponse de test pour voir si vous recevez une notification dans le canal #général de votre serveur de Roblox.Si vous ne recevez pas la notification, réessayez ou vérifiez les paramètres de votre serveur pour résoudre l'erreur.

Configurer un bot
Après avoir ajouté le webhook, utilisez-le pour configurer le bot avec les étapes suivantes :
Ouvrez la liste de tous les serveurs en cliquant sur son icône ou utilisez le raccourci :
- CtrlS sur Windows.
- ⌘S sur Mac.
Sélectionnez votre serveur pour recevoir les notifications de droit à l'effacement.
Étendez la liste sous Accueil du serveur et sélectionnez Gérer les bots .
Le serveur crée automatiquement un canal privé # général comme chat (chat privé)par défaut.
Cliquez sur le bouton Créer un bot et ajoutez un nom de bot. Guilded vous redirige vers la page de configuration du bot.
Sélectionnez la section API sur la page de configuration du bot.
Dans la section jetons , cliquez sur le bouton générer un jeton .
Enregistrez et stockez le jeton généré dans un emplacementsûr.
Créer une clé Open Cloud API
Pour permettre à votre bot tiers d'accéder à vos magasins de données pour stocker les données PII des utilisateurs, créez une clé Open Cloud API qui peut accéder à vos expériences et ajoutez la permission Supprimer l'entrée de magasins de données pour la suppression des données.Si vous utilisez des magasins de données ordonnés pour stocker des IPI, vous devez également ajouter l'autorisation Écrire des magasins de données ordonnés.Une fois la finition terminée, copiez et sauvegardez la clé API dans un emplacement sécurisé pour l'utiliser dans des étapes ultérieures.
Obtenir les identifiants des expériences et des lieux
Pour que le bot localise les données PII demandées par les utilisateurs pour suppression, obtenez les identifiants suivants de toutes les expériences auxquelles vous prévoyez d'utiliser le bot :
- L'ID Univers , l'identifiant unique de votre expérience.
- L'ID de lieu de départ Start Place ID , l'identifiant unique du lieu de départ d'une expérience.
Pour obtenir ces identifiants :
Accédez à la tableau de bord du créateur.
Passez la souris sur une vignette d'expérience, cliquez sur le bouton ⋯ et sélectionnez Copier l'ID de l'univers et Copier l'ID du lieu de départ , respectivement.
Ajouter des scripts
Après avoir configuré le webhook, le bot et la clé API pour les magasins de données, ajoutez-les aux scripts qui implémentent la logique d'automatisation du bot.L'exemple suivant utilise Python 3 :
Installez les bibliothèques Python en utilisant les commandes suivantes :
Installer les bibliothèquespip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6Copiez et enregistrez les scripts suivants correspondant à différentes parties de la logique du bot dans le même dossier :
bot_config.pyBOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Dictiction de l'ID de lieu de départ à# (ID de l'univers, liste des (nom des magasins de données, de la portée et de la clé d'entrée)) pour# Stockage de données standard# Les données utilisateur stockées sous ces entrées seront suppriméesSTANDARD_DATA_STORE_ENTRIES = {# ID de lieu de départ111111111: (# ID de l'univers222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# Dictiction de l'ID de lieu de départ à# (ID de l'univers, liste des (nom des magasins de données, de la portée et de la clé d'entrée)) pour# Stockage de données ordonné# Les données utilisateur stockées sous ces entrées seront suppriméesORDERED_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# Empêche l'attaque de relecture dans la fenêtre de 300 secondesrequest_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# Vérifie la signaturetimestamp_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# Signature validereturn 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):# Parses reçoit le message pour l'ID de l'utilisateur et l'ID du jeuif 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):# Parse et valide le messageuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# Supprime les données des magasins de données standard[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)}")# Supprime les données des magasins de données commandées de l'utilisateur[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()Sur le fichier bot_config.py pour la configuration principale du bot :
- Définissez BOT_TOKEN à la valeur générée par votre bot.
- Définissez OPEN_CLOUD_API_KEY comme clé API que vous avez créée.
- Définissez ROBLOX_WEBHOOK_SECRET comme le secret que vous avez défini lors de la configuration du webhook sur le tableau de bord de Creator.
- Dans STANDARD_DATA_STORE_ENTRIES et ORDERED_DATA_STORE_ENTRIES dictionnaires pour localiser le magasin de données de chaque enregistrement à supprimer :
- Ajoutez vos ID de lieu de départ copiés en tant que clés.
- Ajoutez les ID d'univers comme premier élément de la valeur de tuple.
- Remplacez le deuxième élément de la tuple par le nom, la portée, le nom de la clé d'entrée et l'ID d'utilisateur associé de vos magasins de données.Si vous utilisez un schéma de données différent, modifiez-le pour correspondre à votre propre schéma de données en conséquence.
Exécutez la commande suivante pour exécuter le bot :
Exécuter le bot de guildepython3 guilded_bot.pyLe bot commence ensuite à écouter et à vérifier les webhooks Roblox pour les demandes de suppression et appelle l'extrémité du cloud ouverte pour supprimer le boutiquede données correspondant.
Testez
Vous pouvez créer et exécuter un message de test pour vérifier que votre programme personnalisé peut gérer correctement les demandes de suppression et supprimer les données PII :
Envoyez une demande HTTP POST à votre serveur webhook Guilded ou Discord avec le corps de requête suivant :
Exemple de demandecurl -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 vous avez un secret webhook :
- Générez un Roblox-Signature en appliquant l'encodage HMAC-SHA256 à la clé secrète de votre webhook.
- Définissez le temps actuel en utilisant l'horodatage UTC en secondes comme Timestamp .
Assemblez le description dans le format suivant :
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}`.Par exemple :
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
Votre programme devrait être en mesure d'identifier que votre message provient de la source Roblox officielle puisque vous avez codé le message avec votre secret.Il devrait ensuite supprimer les données PII associées à votre demande.
Exemple de corps
{
"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://créer.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}