Le Règlement général sur la protection des données (RGPD) 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, connue sous le nom de droit à l'effacement. Si vous stockez n'importe quelle donnée personnalisable de vos utilisateurs, telle que leurs ID
Au lieu de gérer manuellement les demandes, vous pouvez configurer un webhook et utiliser un bot dans une application de messagerie tiers pour automatiser le processus. Comme les données des magasins de données sont le moyen le plus courant de 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 API ouverte pour les don
Flux de travail
Après avoir terminé ce tutoriel, vous devriez être en mesure de créer un programme personnalisé s'exécutant localement qui automatise le traitement des demandes d'annulation des droits des utilisateurs. Le workflow pour ce processus est comme suivant :
- Le support Roblox reçoit une demande d'effacement d'un utilisateur.
- Le webhook Roblox est déclenché, contenant l'ID de l'utilisateur et une liste d'identifiants de lieu de départ pour les expériences qu'ils ont rejointes dans le chargement.
- Votre bot écoute ces notifications de webhook, vérifie leur authenticité et utilise la API ouverte pour les données des magasins de données pour supprimer les données PII stockées dans les magasins de données.
- Le bot répond au message du webhook dans Discord ou Guilded avec le statut de suppression.
Configurer un Webhook avec l'intégration tiers
Avant de créer un bot, configurez un serveur avec l'intégration webhook sur l'application de messagerie tiers. Puis 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éer un nouveau serveur Guilded. Si vous n'êtes pas familier avec le processus, voir Support Guilded.
- Sous les paramètres de confidentialité », le serveur est configuré pour être privé. Le serveur crée automatiquement un chat (chat privé)privé # général par défaut.
- Créez une intégration 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 Support de guilde.
- Copiez l'URL du webhook et stockez-la dans un emplacementsûr. Ne permettez que aux membres de l'équipe de confiance d'y accéder, car le piratage de l'URL peut permettre aux acteurs malveillants d'envoyer des messages faux et potentiellement de supprimer vos données utilisateur.
Configurer un webhook sur Roblox
Après avoir obtenu l'URL du serveur tiers, utilisez-le pour configurer un webhook sur le tableau de bord du créateur. assurez-vous de effectuer les paramètres suivants :
- Ajoutez l'URL du serveur Guilded ou Discord comme Webhook URL .
- Incluez un secret personnalisé . Bien qu'un secret soit facultatif pour terminer la configuration, vous devriez l'inclure pour empêcher les mauvais acteurs 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 Demande d'effacement droit sous Trigger .
Vous pouvez tester le webhook en utilisant le bouton Répondre au test pour voir si vous recevez une notification dans votre canal # général de Roblox. Si vous ne recevez pas la notification, essayez à nouveau ou vérifiez les paramètres de votre serveur pour déboguer l'erreur.
Configurer un Robot
Après avoir ajouté le webhook, utilisez-le pour configurer le bot avec les étapes suivantes :
Ouvrez la liste Tous les serveurs en cliquant sur son icône ou en utilisant la raccourci :
- CtrlS sur Windows.
- ⌘S sur Mac.
Sélectionnez votre serveur pour recevoir des notifications d'effacement à droite.
Expand the list under Accueil du serveur and select Gérer les Robots .
Le serveur crée automatiquement un canal privé # général comme votre chat (chat privé)par défaut.
Cliquez sur le bouton Créer un bot et ajoutez un nom de bot. Les redirections de guilde vous redirigent vers la page de configuration du bot.
Sélectionnez la section API sur la page de configuration du bot.
Sous la section Jetons, cliquez sur le bouton Générer un jeton.
Sauvegardez et stockez le jeton généré dans un emplacementsûr.
Créer une Clé APIOpen Cloud
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é API Open Cloud qui peut accéder à vos expériences et ajouter la permission Supprimer entrée des magasins de données pour la suppression des données. Si vous utilisez des magasins de données commandés pour stocker les données IPI,
Obtention d'identifiants d'expériences et de lieux
Pour que le bot localise les données PII demandées par les utilisateurs à supprimer, obtenez les identifiants suivants de toutes les expériences que vous souhaitez utiliser le bot pour :
- Le identifiant de l'univers , l'identifiant unique de votre expérience.
- Le lieu de départ ID , l'identifiant unique du lieu de départ d'une expérience.
Pour obtenir ces identifiants, ouvrez la page Créations sur Tableau de bord du créateur . Ensuite, sélectionnez une expérience et copiez le ID Universe et 1> Start Place ID1>.
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 discordpip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6Copiez et enregistrez les scripts suivants qui correspondent à différentes parties de la logique du bot dans le même dossier :
bot_config.pyDISCORD_BOT_TOKEN = ""GUILDED_BOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Dictionnaire 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# Stockages de données standard# Les données de l'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}")])}# Dictionnaire 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# Stockages de données commandés# Les données de l'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 la tentative de rejouer 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# Valide 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):# Démarre la réception du 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):# Décode 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 magasins de données standard les données des utilisateurs[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 magasins de données commandés[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()Sur le fichier bot_config.py pour la configuration principale du bot :
- Définir DISCORD_BOT_TOKEN ou GUILDED_BOT_TOKEN sur le jeton généré par votre bot.
- Définir OPEN_CLOUD_API_KEY comme clé API que vous avez créée.
- Définir ROBLOX_WEBHOOK_SECRET comme le secret que vous avez défini lors de la configuration du webhook sur la dashboard du créateur.
- 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 Universe en tant que premier élément de la valeur Universe.
- Remplacez le deuxième élément de la tableau par le nom, le champ de champ d'entrée, la clé de nom de l'entrée et l'ID de l'utilisateur associé de vos magasins de données. Si vous utilisez un autre schéma de données, modifiez pour correspondre à votre propre schéma de données de manière correspondante.
Exécutez la commande suivante pour exécuter le bot :
Exécuter le Robot Guildedpython3 guilded_bot.pyLe bot commence alors à écouter et à vérifier les webhook Roblox pour le droit d'effacer les demandes et appelle l'Open Cloud pour supprimer la correspondante du boutiquede données.
Test
Vous pouvez créer et exécuter un message de test pour vérifier que votre programme personnalisé peut correctement gérer les demandes d'effacement et supprimer les données PII :
Envoyez une demande HTTP POST à votre serveur de webhook Guilded ou Discord avec le corps de demande suivant :
Demande d'exemplecurl -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 de webhook :
- Générez une Roblox-Signature en appliquant l'encodage HMAC-SHA256 à votre clé webhook secret.
- Définir l'heure actuelle en utilisant le tampon de l'UTC dans secondes comme Timestamp .
Mettez ensemble 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 est de la source Roblox officielle puisque vous avez encodé 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/dash/ressources/webhook/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}