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.Garante aos indivíduos o direito de solicitar a exclusão de seus dados pessoais, conhecido como o direito ao apagamento.Se você armazenar qualquer Informação Pessoalmente Identificável (IIP) de seus usuários, como seus IDs de Usuário, você deve cumprir os requisitos do GDPR excluindo essas informações após receber o solicitar / pedirde um usuário.
Em vez de lidar com solicitações manualmente, você pode configurar um webhook e usar um bot dentro de um aplicativo de mensageria de terceiros para automatizar o processo.Como lojas de dados sendo a maneira mais comum de armazenar dados PII, este tutorial fornece um exemplo de como criar um bot dentro do Guilded ou do Discord que use a API de nuvem aberta para armazenar dados para excluir dados PII como uma solução de automação.
Fluxo de trabalho
Após concluir este Tutorial, você deve ser capaz de criar um programa personalizado executado localmente que automatize o manuseio de solicitações de direito de exclusão de usuários.O fluxo de trabalho para este processo é o seguinte:
- O Suporte do Roblox recebe um direito de solicitação de exclusão de um usuário.
- O webhook do Roblox é acionado, contendo o ID do Usuário e uma lista de IDs de Local de Início para as experiências que eles se juntaram no payload.
- Seu bot ouve essas notificações de webhook, verifica sua autenticidade e utiliza a API de Nuvem Aberta para armazenamento de dados para excluir os dados PII armazenados em armazenamentos de dados.
- O bot responde à mensagem de webhook no Discord ou Guilded com o status de exclusão.

Configurar um webhook com integração de terceiros
Antes de criar um bot, configure um servidor com integração de webhook no aplicativo de mensageria de terceiros.Então use o servidor para configurar um webhook no Painel do Criador.
Configurar um servidor
Os seguintes passos mostram como configurar o servidor usando o Guilded ou o Discord.
- Crie um novo servidor Guilded. Se você não estiver familiarizado com o processo, veja Suporte Guilded.
- Sob as configurações de Privacidade , defina o servidor para privado.O servidor cria automaticamente um canal privado #geral como o seu canal padrão.
- Crie uma integração de webhook com o novo servidor e dê-lhe um nome que você possa entender facilmente, como GDPR Hook.Se você não está familiarizado com o processo, veja Suporte Guilded.
- Copie o URL do webhook e armazene-o em um localseguro.Apenas permita que membros de equipe confiáveis acessem-no, pois vazar o URL pode habilitar atores maliciosos a enviar mensagens falsas e potencialmente excluir dados do usuário.
Configurar um webhook no Roblox
Depois de obter o URL do servidor de terceiros, use-o para configurar um webhook no Painel do Criador.certifique-se de executar as seguintes configurações:
- Adicione o URL do servidor Guilded ou Discord como o URL de Webhook .
- Inclua um segredo personalizado Secreto .Embora um segredo seja opcional para concluir a configuração, você deve incluir um para impedir que os atores maliciosos se disfarcem de Roblox e excluam seus dados.Para mais informações sobre o uso de um segredo, veja Verifique a segurança do webhook.
- Selecione Direito de Solicitação de Apagamento sob Gatilhos .
Você pode testar o webhook usando o botão Resposta de Teste 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 as configurações do servidor para solucionar o problema.

Configurar um bot
Depois de adicionar o webhook, use-o para configurar o bot com os seguintes passos:
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 sob Casa do Servidor e selecione Gerenciar Bots .
O servidor cria automaticamente um canal privado #geral como o seu canal padrão.
Clique no botão Criar um bot e adicione um nome de bot. O Guilded redireciona você para a página de configuração do bot.
Selecione a seção API na página de configuração do bot.
Na seção Moedas , clique no botão Gerar Token .
Salve e armazene o token gerado em um localseguro.
Crie uma chave de API da Nuvem Aberta
Para permitir que seu bot de terceiros acesse seus armazenamentos de dados para armazenar dados PII de usuários, crie uma chave de API da Nuvem Aberta que possa acessar suas experiências e adicione a permissão Apagar entrada de armazenamentos de dados para exclusão de dados.Se você usar armazenamentos de dados ordenados para armazenar IIP, também precisa adicionar a permissão Escrever de armazenamentos de dados ordenados.Após a conclusão, copie e salve a chave da API em um local seguro para usá-la em etapas posteriores.
Obtenha identificadores 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ê pretende usar o bot para:
- O ID do Universo , o identificador exclusivo da sua experiência.
- O ID do Local de Início , o identificador exclusivo do local de início de uma experiência.
Para obter esses identificadores:
Navegue até o Painel do Criador.
Passe o mouse sobre uma miniatura de experiência, clique no botão ⋯ e selecione Copiar ID do Universo e Copiar ID do Local de Início , respectivamente.
Adicionar 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 seguinte exemplo usa o Python 3:
Instale bibliotecas Python usando os seguintes comandos:
Instalar Bibliotecaspip3 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.pyBOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Dicionário do ID do local de início para# (ID do universo, lista de (nomes de armazenamento de dados, escopo e chave de entrada)) para# Armazenamentos de Dados Padrão# 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 início para# (ID do universo, lista de (nomes de armazenamento de dados, escopo e chave de entrada)) para# Armazenamento de Dados Ordenado# 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# Previne 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):# Parsa recebe 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 mensagemuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# Exclui dados do armazenamento padrão de dados do usuário[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 armazenados de forma ordenada do usuário[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()No arquivo bot_config.py para a configuração principal do bot:
- Defina BOT_TOKEN para o token gerado pelo seu bot.
- Defina OPEN_CLOUD_API_KEY como a chave da API que você criou.
- Defina ROBLOX_WEBHOOK_SECRET como o segredo que você definiu 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 de Partida copiados como chaves.
- Adicione IDs de Universo como o primeiro elemento do valor do tuple.
- Substitua o segundo elemento do tuple pelo nome, escopo, nome da chave de entrada e ID de usuário associado de seus armazenamentos de dados.Se você usar um esquema de dados diferente, modifique para combinar com o seu próprio esquema de dados de acordo.
Execute o seguinte comando para executar o bot:
Executar Bot da Guildapython3 guilded_bot.pyO bot então começa a ouvir e verificar webhooks do Roblox para solicitações de direito de exclusão e chama o ponto final Open Cloud para excluir o lojade dados correspondente.
Teste
Você pode criar e executar uma mensagem de teste para verificar que seu programa personalizado pode lidar corretamente com solicitações de direito à apagar e excluir dados PII:
Envie um pedido HTTP POST ao seu servidor de webhook da Guilded ou Discord com o seguinte corpo de solicitação:
Exemplo de Solicitaçãocurl -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 segredo de webhook:
- Gere um Roblox-Signature gerando um código HMAC-SHA256 para a chave secreta do seu webhook.
- Defina o tempo atual usando o timestamp UTC em segundos como Timestamp .
Junte 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 é da fonte oficial do Roblox, pois você codificou a mensagem com seu segredo.Então deve excluir os dados PII associados ao seu solicitar / pedir.
Exemplo de Corpo
{
"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/dashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}