O Regulamento Geral de Proteção de Dados (Regulamento Geral sobre a Proteção de Dados) é um regulamento europeu sobre proteção de dados e privacidade. Ele concede aos indivíduos o direito de solicitar a exclusão de seus dados pessoais, conhecido como o direito de exclusão. Se você armazenar qualquer Informação Pessoalmente IDável (IIP) de seus usuários, como
Em vez de lidar com solicitações manualmente, você pode configurar um webhook e usar um bot dentro de uma aplicação de mensageria de terceiros para automatizar o processo. Como Armazenamento de dados é a maneira mais comum de armazenar dados PII, este tutorial fornece uma exemplificação sobre como criar um bot dentro do Guilded ou Discord que usa o Open Cloud API for Data Sto
Fluxo de Trabalho
Ao concluir este Tutorial, você deve estar habilitado a criar um programa personalizado que é executado localmente para automatizar o gerenciamento de solicitações de exclusão de direitos dos usuários. O fluxo de trabalho para este processo é como segue:
- O Roblox Support recebe um pedido de exclusão de um usuário.
- O webhook do Roblox é acionado, contendo o ID do Usuário e uma lista de IDs de Start Place para as experiências que eles se juntaram nopayload.
- Seu bot ouve essas notificações de webhook, verifica sua autenticidade e utiliza a Open Cloud API for Data Stores para excluir os dados armazenados em armazenamentos de dados.
- O bot responde à mensagem de webhook no Discord ou Guilded com o status de exclusão.
Configurando um Webhook com integração de terceiros
Antes de criar um bot, configure um servidor com integração de webhook na aplicação de mensageria de terceiros. Em seguida, use o servidor para configurar um webhook no Painel do Criador.
Configurando um Servidor
As seguintes etapas mostram como configurar o servidor usando Guilded ou Discord.
- Crie um novo servidor Guilded. Se você não estiver familiarizado com o processo, veja Suporte Guilded.
- Sob as configurações de Privacidade ', o servidor é definido como privado. O servidor cria automaticamente um canal privado # general como seu canal padrão.
- Crie uma integração de webhook com o novo servidor e dê-lhe um nome que você pode facilmente entender, como GDPR Hook. Se você estiver des familiarizado com o processo, consulte Suporte Guildado.
- Copie a URL do webhook e armazená-la em um localseguro. Somente permita que membros de equipe confiáveis acesso, pois a divulgação da URL pode permitir que atores maliciosos enviem mensagens falsas e possam potencialmente excluir seus dados do usuário.
Configurando um Webhook no Roblox
Depois de obter o URL do terceiro servidor, use-o para ajustar um webhook no Painel do Criador. certifique-se de executar as seguintes configurações:
- Adicione o URL do servidor da Guilded ou Discord como o Webhook URL .
- Inclua um segredo personalizado . Embora um segredo seja opcional para completar a configuração, você deve incluir um para impedir que os atores maliciosos imitem o Roblox e excluam seus dados. Para mais informações sobre o uso de um segredo, veja Verificando a segurança do webhook.
- Selecione Pedido de Exclusão Direito embaixo de Gatilhos .
Você pode testar o webhook usando o botão Testar Resposta para ver se você recebe uma notificação no canal # geral do seu servidor do Roblox. Se você não receber a notificações, tente novamente ou verifique suas configurações do servidor para solucionar o erro.
Configurando um Bot
Depois de adicionar o webhook, use-o para configurar o bot com as seguintes etapas:
Abra a lista Todos os servidores clicando em seu ícone ou use o atalho:
- CtrlS em Windows.
- ⌘S no Mac.
Selecione seu servidor para receber notificações de direito de exclusão.
Expanda a lista em Casa do Servidor e selecione Gerenciar Bots .
O servidor cria automaticamente um canal privado # general como seu canal padrão.
Clique no botão Criar um Bot e adicione um nome de bot. Redirecionamentos de guilda o leva à página de configuração de Bots.
Selecione a seção API na página de configuração do bot.
Sob a seção Tokens, clique no botão Gerar Tokens.
Salve e armazene o token gerado em um localseguro.
Criando uma Chave da API de Nuvem Aberta
Para permitir que seu bot de terceiros acesse seus armazenamentos de dados para armazenar dados IIPde usuários, crie uma chave de API de Nuvem Aberta que possa acessar suas experiências e adicionar a permissão Remover Entrada dos armazenamentos de dados para a exclusão de dados. Se você usar armazenamentos de d
Obtendo IDs de Experiências e Locais
Para o bot localizar os dados PII solicitados pelos usuários para exclusão, obtenha os seguintes identificadores de todas as experiências que você intenciona usar o bot para:
- O ID do Universo , o identificador exclusivo de sua experiência.
- O ID do Local de Início ID , o identificador exclusivo do local de início de uma experiência.
Para obter esses identificadores, abra a página Criações no Painel do Criador. Em seguida, selecione uma experiência e copie o ID do Universo e 2>Start Place ID2>.
Adicionando Scripts
Depois de configurar o webhook, o bot e a chave da API para armazenamentos de dados, adicione-os aos scripts que implementam a lógica de automação do bot. O exemplo a seguir usa o Python 3:
Instale bibliotecas Python usando os seguintes comandos:
Instale as Bibliotecaspip3 install discordpip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6Copie e salve os seguintes scripts correspondentes a diferentes partes da lógica do bot no mesmo diretório:
bot_config.pyDISCORD_BOT_TOKEN = ""GUILDED_BOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Dicionário do ID do local de partida para# (ID do universo, lista de (nome dos armazenamentos de dados, alcance e chave de entrada)) para# Armazenamento de Dados Padrão# Os dados do usuário armazenados sob essas entradas serão excluídosSTANDARD_DATA_STORE_ENTRIES = {# ID do Local de Início111111111: (# ID do Universo222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# Dicionário do ID do local de partida para# (ID do universo, lista de (nome dos armazenamentos de dados, alcance e chave de entrada)) para# Armazenamento de Dados Ordenado# Os dados do usuário armazenados sob essas entradas serão excluídosORDERED_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# Impede o ataque de replay dentro da janela 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 a assinaturatimestamp_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# Assinatura 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):# Parses recebem mensagem para ID do usuário e ID do jogoif 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 e valida a mensagemuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# Exclui dados padrão armazenados de usuários[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)}")# Exclui dados solicitados armazenados de usuários[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()No arquivo bot_config.py para a configuração principal do bot:
- Definir DISCORD_BOT_TOKEN ou GUILDED_BOT_TOKEN para o token gerado por seu bot.
- Defina OPEN_CLOUD_API_KEY como a chave de API que você criou.
- Definir ROBLOX_WEBHOOK_SECRET como o segredo que você configurou ao configurar o webhook no Painel do Criador.
- Em STANDARD_DATA_STORE_ENTRIES e ORDERED_DATA_STORE_ENTRIES dicionários para localizar o armazenamento de dados de cada registro para excluir:
- Adicione seus IDs de Local Inicial copiados como chaves.
- Adicione IDs de Universo como o primeiro elemento do valor da lista.
- Substitua o segundo elemento do tupleo com o nome, alcance, nome da chave de entrada e ID de usuário associado de seus armazenamentos de dados. Se você usar um diferente esquema de dados, modifique para ajustar o seu próprio esquema de dados.
Execute o seguinte comando para executar o bot:
Executar Bot Guildedpython3 guilded_bot.pyO bot então começa a ouvir e verificar os webhooks do Roblox para direito de exclusão de solicitações e chamadas o ponto de terminação da Nuvem Aberta para excluir o lojade dados correspondente.
Testando
Você pode criar e executar uma mensagem de teste para verificar que seu programa personalizado pode lidar com solicitações de exclusão com sucesso e excluir dados PII:
Envie uma solicitação HTTP para o seu servidor webhook Guilded ou Discord com o seguinte corpo da solicitação:
Exemplo de Pedidocurl -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}"}}]}'Se você tiver um webhook secreto:
- Gerar uma Roblox-Signature aplicando a encriptação HMAC-SHA256 à sua chave de webhook secreta.
- Defina a hora atual usando o UTC timestamp em segundos como Timestamp .
Coloque juntos o description no seguinte 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 exemplo:
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
Seu programa deve ser capaz de identificar que sua mensagem vem da fonte oficial do Roblox, pois você codificou a mensagem com seu segredo. Em seguida, deve excluir os dados PII associados à sua solicitar / pedir.
Corpo de Exemplo
{
"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://criar.roblox.com/dash/artefatos/webhook/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}