일반 데이터 개인정보규정(GDPR)은 데이터 보호와 프라이버시에 관한 유럽 규정입니다.개인에게 삭제할 개인 데이터, 즉 지우기 권한을 요청할 수 있는 권한을 부여합니다.사용자의 사용자 ID와 같은 사용자 식별 정보를 저장하는 경우 GDPR 요구 사항을 준수해야 합니다 . 사용자의 요청을 받으면 이 정보를 삭제하여 준수해야 합니다.
요청을 수동으로 처리하는 대신, 웹훅을 설정하고 타사 메시징 응용 프로그램 내에서 봇을 사용하여 프로세스를 자동화할 수 있습니다.PII 데이터를 저장하는 가장 일반적인 방법으로 데이터 저장소가 되는 가운데, 이 자습서에서는 Open Cloud API for data stores를 사용하여 PII 데이터를 자동화 솔루션으로 삭제하는 Guilded 또는 Discord 내의 봇을 만드는 방법에 대한 예를 제공합니다.
워크플로
이 자습서를 완료하면 사용자의 삭제 요청 처리를 자동화하는 로컬 실행 사용자 지정 프로그램을 만들 수 있어야 합니다.이 프로세스의 워크플로는 다음과 같습니다:
- Roblox 지원은 사용자로부터 지우기 요청을 받습니다.
- Roblox 웹훅이 트리거되어 사용자 ID와 페이로드에 참여한 경험의 시작 장소 ID 목록이 포함됩니다.
- 봇이 이러한 웹훅 알림을 수신하고, 진위를 확인하고, 데이터 저장소에 저장된 PII 데이터를 삭제하기 위해 Open Cloud API for data stores를 사용합니다.
- 봇은 삭제 상태로 Discord 또는 Guilded의 웹훅 메시지에 응답합니다.

타사 통합으로 웹훅 구성
봇을 생성하기 전에 타사 메시징 앱에서 웹훅 통합 서버를 설정합니다.Before creating a bot, set up a server with webhook integration on the third-party messaging application.그런 다음 서버를 사용하여 크리에이터 대시보드에서 웹훅을 구성합니다.
서버 설정
다음 단계에서는 Guilded 또는 Discord를 사용하여 서버를 설정하는 방법을 보여줍니다.
- 새로운 Guilded 서버를 생성합니다. 프로세스에 익숙하지 않은 경우 Guilded 지원을 참조하십시오.
- 프라이버시 설정에서 서버를 비공개로 설정합니다.서버는 기본 채널로 사설 #일반 채널을 자동으로 생성합니다.
- 새 서버와 웹훅 통합을 만들고 이름을 GDPR Hook와 같이 쉽게 이해할 수 있는 이름을 지정하십시오.프로세스에 익숙하지 않은 경우 Guilded 지원을 참조하십시오.
- 웹훅 URL을 복사하여 안전한 플레이스저장합니다.URL을 누출하면 악의적인 행위자가 가짜 메시지를 보내고 사용자 데이터를 삭제할 수 있으므로 URL을 누출하지 않도록 신뢰할 수 있는 팀 멤버만 액세스할 수 있도록 허용합니다. Only allow trusted team members to access it, as leaking the URL can enable bad actors to send fake messages and potentially delete your user data.
Roblox에서 웹훅 구성
제3자 서버 URL을 획득한 후, 그것을 사용하여 크리에이터 대시보드에 웹훅 구성을 구성합니다.다음 설정을 수행했는지 확인하십시오:
- Guilded 또는 Discord 서버 URL을 웹훅 URL 으로 추가합니다.
- 사용자 지정 비밀 을 포함하십시오.비밀은 구성을 완료하는 데 선택적이지만, 나쁜 행위자가 Roblox를 가장하고 데이터를 삭제하지 못하도록 하려면 하나를 포함해야 합니다.비밀의 사용에 대한 자세한 정보는 웹훅 보안 확인을 참조하십시오.
- 삭제 요청의 오른쪽을 트리거 아래에서 선택합니다.
웹훅을 테스트하려면 테스트 응답 버튼을 사용하여 Roblox에서 서버의 #일반 채널에서 알림을 받는지 확인할 수 있습니다.알림을 받지 못하면 다시 시도하거나 서버 설정을 확인하여 오류를 해결하십시오.

봇 구성
웹훅을 추가한 후 다음 단계를 사용하여 봇을 구성하십시오:
아이콘을 클릭하거나 단축키를 사용하여 모든 서버 목록을 열거나
- CtrlS 에서 Windows에서.
- ⌘S 에서 Mac에서.
지우기 알림을 받기 위한 서버를 선택합니다.
서버 홈 아래의 목록을 확장하고 봇 관리 를 선택합니다.
서버는 기본 채널로 사설 #일반 채널을 자동으로 생성합니다.
클릭하십시오 봇 생성 버튼 및 봇 이름을 추가하십시오. 길드는 봇 구성 페이지로 리디렉션합니다.
봇 구성 페이지에서 API 섹션을 선택하십시오.
토큰 섹션에서 토큰 생성 버튼을 클릭하십시오
생성된 토큰을 안전한 플레이스저장하고 저장합니다.
오픈 클라우드 API 키 생성
사용자의 PII 데이터를 저장하기 위해 타사 봇에 데이터 저장소에 액세스할 수 있도록 하려면 경험에 액세스할 수 있는 오픈 클라우드 API 키를 만들어 데이터 삭제의 경우 데이터 저장소에 삭제 항목 추가 권한을 추가합니다.II를 저장하기 위해 순서 데이터 저장소를 사용하는 경우 순서 데이터 저장소의 쓰기 권한도 추가해야 합니다.완료 후 API 키를 안전한 위치에 복사하여 나중에 단계에서 사용합니다.
경험과 장소의 식별자 받기
봇이 사용자가 삭제를 요청한 PII 데이터를 찾도록 하려면, 봇을 사용할 모든 경험의 다음 식별자를 얻으십시오:
- 유니버스 ID , 경험의 고유 식별자.
- 시작 장소 ID , 경험의 시작 장소의 고유 식별자.
이러한 식별자를 얻으려면:
탐색하여 크리에이터 대시보드로 이동합니다.
경험의 썸네일 섬네일이동하고 ⋯ 버튼을 클릭하고, 차례로 유니버스 ID 복사 및 시작 장소 ID 복사 를 선택합니다.
스크립트 추가
웹훅, 봇 및 API 키를 데이터 저장소에 설정한 후에는 봇의 자동화 논리를 구현하는 스크립트에 추가하십시오.다음 예제에서는 Python 3를 사용합니다:
다음 명령을 사용하여 파이썬 라이브러리 설치:
라이브러리 설치pip3 install guilded.py==1.8.0pip3 install requestspip3 install urllib3==1.26.6동일한 디렉터리에 봇 논리의 다른 부분에 해당하는 다음 스크립트를 복사하고 저장합니다.
보트_config.pyBOT_TOKEN = ""OPEN_CLOUD_API_KEY = ""ROBLOX_WEBHOOK_SECRET = ""# 시작 장소 ID의 사전에서# (유니버스 ID, (데이터 저장소 이름, 범위 및 엔트리 키) 목록) 에 대한# 표준 데이터 저장소# 이러한 항목에 저장된 사용자 데이터가 삭제됩니다STANDARD_DATA_STORE_ENTRIES = {# 시작 장소 ID111111111: (# 유니버스 ID222222222,[("StandardDataStore1", "Scope1", "Key1_{user_id}"),("StandardDataStore1", "Scope1", "Key2_{user_id}"),("StandardDataStore2", "Scope1", "Key3_{user_id}")]),33333333: (444444444,[("StandardDataStore3", "Scope1", "Key1_{user_id}")])}# 시작 장소 ID의 사전에서# (유니버스 ID, (데이터 저장소 이름, 범위 및 엔트리 키) 목록) 에 대한# 정렬된 데이터 저장소# 이러한 항목에 저장된 사용자 데이터가 삭제됩니다ORDERED_DATA_STORE_ENTRIES = {111111111: (222222222,[("OrderedDataStore1", "Scope2", "Key4_{user_id}")])}데이터_스토어_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# 300초 창 내에서 재생 공격 방지request_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# 서명 검사timestamp_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# 유효한 서명return 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):# 사용자 ID 및 게임 ID에 대한 받은 메시지 분석if 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):# 메시지 파싱 및 유효성 검사user_id, start_place_ids = message_parser.parse_message(message)if not user_id or not start_place_ids:return# 표준 데이터 저장소 사용자 데이터 삭제[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)}")# 주문된 데이터 저장소 사용자 데이터 삭제[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()봇의 주 구성에 대한 bot_config.py 파일에서:
- 봇에서 생성된 토큰에 BOT_TOKEN를 설정합니다.
- 설정 OPEN_CLOUD_API_KEY 생성한 API 키로 설정합니다.
- 크리에이터 대시보드에서 웹훅을 구성할 때 설정한 비밀로 ROBLOX_WEBHOOK_SECRET를 설정합니다.
- 삭제할 각 레코드의 데이터 저장소를 찾기 위한 STANDARD_DATA_STORE_ENTRIES 및 ORDERED_DATA_STORE_ENTRIES 사전에서:
- 복사한 시작 장소 ID를 키로 추가합니다.
- 튜플 값의 첫 요소로 유니버스 ID를 추가합니다.
- 튜플의 두 번째 요소를 데이터 저장소의 이름, 범위, 입력 키 이름 및 연관된 사용자 ID로 바꿉니다.다른 데이터 스키마를 사용하는 경우 해당 데이터 스키마에 맞게 수정하여 자체 데이터 스키마와 일치시킵니다.
다음 명령을 실행하여 봇을 실행합니다:
길드 봇 실행python3 guilded_bot.py그런 다음 봇은 삭제 요청과 호출에 대한 Roblox 웹훅을 수신하고 확인하기 시작하며 해당 데이터 상점삭제하기 위해 Open Cloud 끝점을 호출합니다.
테스트
사용자 지정 프로그램이 삭제 요청과 PII 데이터를 적절하게 처리할 수 있는지 확인하기 위해 테스트 메시지를 만들고 실행할 수 있습니다.
다음 요청 본문으로 Guilded 또는 Discord 웹후크 서버에 HTTP POST 요청을 보내세요:
예제 요청curl -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}"}}]}'웹훅 비밀이 있는 경우:
- 웹훅 비밀 키에 HMAC-SHA256 인코딩을 적용하여 Roblox-Signature를 생성하십시오.
- 초 단위 UTC 타임스탬프를 사용하여 현재 시간을 Timestamp로 설정합니다.
다음 형식으로 description를 함께 배치하십시오:
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}`.예를 들어:
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
프로그램은 메시지가 비밀로 인코딩되었기 때문에 메시지가 공식 Roblox 소스에서 온 것임을 식별할 수 있어야 합니다.그런 다음 요청과 관련된 PII 데이터를 삭제해야 합니다.
예시 본문
{
"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/dashboards/assets/webhooks/roblox_logo_metal.png",
"text": "Roblox-Signature: UIe6GJ78MHCmU/zUKBYP3LV0lAqwWRFR6UEfPt1xBFw=, Timestamp: 1683927229"
}
}
]
}