Rozporządzenie ogólne o ochronie danych (RODO) jest europejskim rozporządzeniem dotyczącym ochrony danych i prywatność, poufność.Zapewnia osobom prawo do żądania usunięcia ich danych osobowych, znane jako prawo do usunięcia.Jeśli przechowujesz jakiekolwiek Osobowo identyfikujące informacje (dane osobowe) użytkowników, takie jak ich ID użytkownika, musisz przestrzegać wymogów RODO, usuwając te informacje po otrzymaniu prośbaużytkownika.
Zamiast obsługiwać żądania ręcznie, możesz skonfigurować webhook i użyć bota w aplikacji wysyłania wiadomości strony trzeciej, aby zautomatyzować proces.Ponieważ magazyny danych są najczęstszym sposobem przechowywania danych PII, ten samouczek dostarcza przykład na to, jak utworzyć bot w Guilded lub Discord, który wykorzystuje otwartą chmurę API dla magazynów danych, aby usunąć dane PII jako rozwiązanie automatyzacji.
Przepływ pracy
Po zakończeniu tego samouczka powinieneś być w stanie stworzyć lokalnie uruchamiany niestandardowy program, który zautomatyzuje obsługę żądań prawa do usuwania od użytkowników.Przepływ pracy dla tego procesu wygląda następująco:
- Wsparcie Roblox otrzymuje prawo do wniosku o usunięcie od użytkownika.
- Webhook Roblox jest uruchamiany, zawierający identyfikator użytkownika i listę identyfikatorów miejsc startu dla doświadczeń, do których dołączył w zapytaniu o wysyłkę.
- Twój bot słucha tych powiadomień o webhookach, weryfikuje ich autentyczność i wykorzystuje Otwartą chmurkową API do przechowywania danych, aby usunąć dane PII przechowywane w magazynach danych.
- Bot odpowiada na wiadomość webhooka w Discordzie lub Guilded ze statusem usunięcia.

Konfiguruj webhook z integracją stron trzecich
Zanim utworzysz bot, skonfiguruj serwer z integracją webhook na aplikacji wysyłania wiadomości strony trzeciej.Następnie użyj serwera, aby skonfigurować webhook na pulpicu nawigacyjnym twórcy.
Ustaw serwer
Następujące kroki pokazują, jak skonfigurować serwer za pomocą Guilded lub Discord.
- Utwórz nowy serwer Guilded. Jeśli nie znasz procesu, zobacz Wsparcie Guilded.
- W ustawieniach Prywatność , ustaw serwer na prywatne.Serwer automatycznie tworzy prywatny kanał #ogólny jako domyślny kanał.
- Stwórz integrację webhook z nowym serwerem i nadaj mu nazwę, którą możesz łatwo zrozumieć, taką jak GDPR Hook.Jeśli nie znasz procesu, zobacz Wsparcie Guilded.
- Skopiuj URL webhooka i przechowaj go w bezpiecznym miejsce.Pozwól tylko zaufanym członkom zespołu na uzyskanie do niego dostępu, ponieważ wyciekanie URL może umożliwić złym aktorom wysyłanie fałszywych wiadomości i potencjalne usunięcie danych użytkownika.
Konfiguruj webhook na Roblox
Po uzyskaniu URL serwera strony trzeciej użyj go do skonfigurowania webhooka na panelu Creator Dashboard.upewnij się, że wykonasz następujące ustawienia:
- Dodaj URL serwera Guilded lub Discord jako URL Webhook .
- Włącz niestandardowy Sekret .Chociaż tajemnica jest opcjonalna do ukończenia konfiguracji, powinieneś dodać jedną, aby zapobiec osobom złym przed udawaniem się za Roblox i usunięciem twoich danych.Aby uzyskać więcej informacji o wykorzystaniu sekretu, zobacz Zweryfikuj bezpieczeństwo webhooka.
- Wybierz Prawo do wniosku o usunięcie pod Wyzwalaczami .
Możesz przetestować webhook za pomocą przycisku Odpowiedź testowa , aby sprawdzić, czy otrzymujesz powiadomienie w kanałach #ogólnych serwera od Roblox.Jeśli nie otrzymujesz powiadomienia, spróbuj ponownie lub sprawdź ustawienia serwera, aby rozwiązać problem.

Konfiguruj robota
Po dodaniu webhooka użyj go do konfiguracji botu za pomocą następujących kroków:
Otwórz listę Wszystkie serwery poprzez kliknięcie jego ikony lub skorzystaj z skrótu:
- CtrlS na Windowsie.
- ⌘S na Macu.
Wybierz swój serwer do otrzymywania powiadomień o prawie do usuwania.
Rozszerz listę pod domem serwera i wybierz zarządzaj botami .
Serwer automatycznie tworzy prywatny kanał #ogólny jako domyślny kanał.
Kliknij przycisk Stwórz bot i dodaj nazwę botu. Guilded przekieruje cię na stronę konfiguracji botów.
Wybierz sekcję API na stronie konfiguracji bota.
W sekcji Żetony , kliknij przycisk Generuj żeton .
Zapisz i przechowaj wygenerowany token w bezpiecznym miejsce.
Utwórz klucz Open Cloud API
Aby umożliwić dostęp twojemu botowi stron trzecich do przechowywania danych użytkowników, stwórz klucz API Open Cloud, który może uzyskać dostęp do twoich doświadczeń i dodać uprawnienie Usuń wpis do przechowywania danych do usunięcia.Jeśli używasz uporządkowanych magazynów danych do przechowywania dane osobowe, musisz również dodać uprawnienie Napisz uporządkowanych magazynów danych.Po zakończeniu skopiuj i zapisz klucz API w bezpiecznym miejscu, aby używać go w późniejszych krokach.
Zdobądź identyfikatory doświadczeń i miejsc
Aby bot zlokalizował dane PII żądane przez użytkowników do usunięcia, uzyskaj następujące identyfikatory wszystkich doświadczeń, z których zamierzasz korzystać z botem:
- ID Wszechświata , unikalny identyfikator twojego doświadczenia.
- ID miejsca startowego Start Place ID , unikalny identyfikator miejsca startu doświadczenia.
Aby uzyskać te identyfikatory:
Przejdź do Pulpitu nawigacyjnego twórcy.
Najedź kursorem na miniaturę miniatura, kliknij przycisk ⋯ i wybierz Kopiuj ID wszechświata oraz Kopiuj początkowe miejsce ID , odpowiednio.
Dodaj kod
Po skonfigurowaniu webhooka, bota i klucza API dla przechowywania danych dodaj je do skryptów, które implementują logikę automatyzacji botów.Poniższy przykład używa Python 3:
Zainstaluj biblioteki Python za pomocą następujących poleceń:
Zainstaluj bibliotekipip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6Skopiuj i zapisz następujące skrypty odpowiadające różnym częściom logiki botów w tym samym katalogu:
bot_config.pyBOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# Słownik ID miejsca startu do# (ID wszechświata, lista nazw (magazynów danych, zakresu i klucza wejściowego)) dla# Standardowe magazyny danych# Dane użytkownika przechowywane w ramach tych wpisów zostaną usunięteSTANDARD_DATA_STORE_ENTRIES = {# Rozpocznij identyfikator miejsca111111111: (# ID wszechświata222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# Słownik ID miejsca startu do# (ID wszechświata, lista nazw (magazynów danych, zakresu i klucza wejściowego)) dla# Zamówione sklepy danych# Dane użytkownika przechowywane w ramach tych wpisów zostaną usunięteORDERED_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# Zapobiega atakowi odtwarzania w ciągu 300-sekundowego okresurequest_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# Weryfikuje podpistimestamp_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# Prawidłowy podpisreturn 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):# Analizuje otrzymane wiadomości dla ID użytkownika i ID gryif 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):# Rozdziela i weryfikuje wiadomośćuser_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# Usuwa dane użytkownika standardowych magazynów danych[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)}")# Usuwa dane przechowywane w zamówionych sklepach danych użytkownika[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()Na pliku bot_config.py dla głównej konfiguracji botu:
- Ustaw BOT_TOKEN na token wygenerowany przez twój bot.
- Ustaw OPEN_CLOUD_API_KEY jako klucz API, który stworzyłeś.
- Ustaw jako sekret, który ustawiłeś podczas konfigurowania webhooka na pulpicie nawigacyjnym twórcy.
- W słownikach STANDARD_DATA_STORE_ENTRIES i ORDERED_DATA_STORE_ENTRIES w celu znalezienia przechowalni danych każdej rekordu do usuwać:
- Dodaj skopiowane ID miejsca startu jako klucze.
- Dodaj ID wszechświata jako pierwszy element wartości tuple.
- Zastąp drugim elementem tuple nazwę, zakres, nazwę klucza wejściowego i powiązany identyfikator użytkownika twoich magazynów danych.Jeśli używasz innego schematu danych, dostosuj go do swojego własnego schematu danych odpowiednio.
Wykonaj następujące polecenie, aby uruchomić bot:
Uruchom zautomatyzowany bot Guildedpython3 guilded_bot.pyBot zaczyna wtedy słuchać i weryfikować webhooki Roblox w celu uzyskania prawa do żądań usunięcia i dzwoni do punktu końcowego chmury otwartej, aby usunąć odpowiednią sklepdanych.
Testuj
Możesz utworzyć i uruchomić wiadomość testową, aby zweryfikować, że twój niestandardowy program może prawidłowo obsługiwać prośby o usunięcie i usuwać dane PII:
Wyślij żądanie HTTP POST do serwera webhook Guilded lub Discord z następującym ciałożądania:
Przykładowe żądaniecurl -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}"}}]}'Jeśli masz sekret webhooka:
- Wygeneruj Roblox-Signature poprzez zastosowanie szyfrowania HMAC-SHA256 do klucza sekretu webhooka.
- Ustaw aktualny czas za pomocą czasu UTC w sekundach jako Timestamp.
Połącz description w następującym formacie:
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}`.Na przykład:
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
Twój program powinien być w stanie zidentyfikować, że twoja wiadomość pochodzi z oficjalnego źródła Roblox, ponieważ zakodowałeś wiadomość swoim sekretem.Następnie powinien usunąć dane PII związane z twoim prośba.
Przykładowe ciało
{
"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://create.roblox.com/dashboard/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}