블루스카이 라벨러 개발기
블루스카이 유저가 급증하면서 성인 포스트 위주의 계정들이 우후죽순 등장하기 시작했습니다. 블루스카이의 기본 모더레이션 시스템도 제법 잘 작동하고 있지만, 미디어만 가려 주기 때문에 성인 포스트의 글은 피드에 그대로 노출되는 문제가 발생하기 시작했습니다.
SNS 사용자가 성인 콘텐츠를 작성해서는 안 된다고 생각하지는 않지만, 예를 들어 공공장소에서 피드를 볼 때는 내용을 가려 놓을 수 있는 쪽이 서로에게 편리하지 않을까요? 그래서 대충 직접 해결해 보기로 했습니다.
블루스카이의 모더레이션 시스템
블루스카이는 기본 모더레이션 시스템 외에 다양한 모더레이션 서비스를 만들거나 구독할 수 있는 구조로 되어 있습니다. (자세한 내용은 공식 블로그를 참조하세요.) 블루스카이에서 제공하는 오픈소스 모더레이션 도구인 Ozone을 서버에 설치하는 것만으로 자체 모더레이션 서비스를 만들 수 있으며, Ozone은 모더레이션 라벨을 정의하고, 신고를 접수하고 처리하는 데에 필요한 모든 도구를 제공합니다.
블루스카이의 커스텀 라벨은 다음과 같은 형식으로 지정됩니다.
{
"labelValues": [
"gnl-adult"
],
"labelValueDefinitions": [
{
"blurs": "content",
"locales": [
{
"lang": "ko",
"name": "성인물 관련",
"description": "한국어로 된 성인물 혹은 해당 콘텐츠와 주로 상호작용하는 계정에 라벨을 지정합니다."
},
{
"lang": "en",
"name": "Adult contents (KR)",
"description": "Adult contents in Korean language and accounts frequently interacting with or posting them."
}
],
"severity": "inform",
"adultOnly": true,
"identifier": "gnl-adult",
"defaultSetting": "warn"
}
]
}
blurs
는 라벨이 적용되는 포스트에"warn"
설정을 적용했을 때에 포스트가 어떻게 표시되는지 결정합니다."none"
: 가리지 않음"media"
: 미디어만 가림"content"
: 글과 미디어를 가림
severity
는 라벨이 적용되는 포스트나 계정에 적용되는 안내 배지의 종류를 결정합니다."none"
: 안내하지 않음"inform"
: 중립적 안내"alert"
: 경고
defaultSetting
은 라벨이 적용되는 포스트나 계정을 기본적으로 어떻게 처리할지를 결정합니다. 라벨을 구독하는 사용자들은 각자 필요에 따라 설정을 변경할 수 있습니다."none"
: 별도의 처리 없음"warn"
: 경고를 표시하되 경고를 누르면 내용을 볼 수 있음"hide"
: 피드에 아예 표시하지 않음
시간과 사람이 모자란다
Ozone이 있다면 사용자들이 라벨러에 신고를 하고, 관리자와 모더레이터들이 열심히 신고를 처리하는 방식으로 라벨러를 충분히 운용할 수 있습니다. 하지만 블루스카이가 예상보다 훨씬 커져 버렸다는 게 문제였습니다. 그래서 자동 라벨링 스크립트를 구축하기로 마음먹었죠.
기본적으로 제가 떠올린 성인 계정을 구분하는 방법은 다음과 같았습니다.
- 성인 계정을 3개 이상 팔로우하는 계정은 성인 계정일 것이다.
- 성인 계정이 주로 사용하는 태그를 포스트하는 계정은 성인 계정일 것이다.
이 중 첫 번째 방법이 그리 좋지 않다는 것을 깨닫기까지는 그리 오랜 시간이 걸리지 않았습니다. 케빈 베이컨의 6단계 법칙에 따라 팔로잉을 따라가다 보면 순식간에 블루스카이의 모든 사람이 성인 계정이 된다는 결론에 이르렀죠. 그러니 2번 방법을 사용할 수밖에 없었습니다. 그렇다면 한국어 포스트를 실시간으로 보면서 특정 키워드나 해시태그를 사용하는 포스트가 올라올 때마다 라벨을 지정할 방법이 필요합니다.
커스텀 피드
처음에는 Firehose를 사용하여 블루스카이의 이벤트 스트림에 실시간으로 구독하는 방법을 고민했지만, 제 서버로 네트워크 전체에서 발생하는 초당 수백 개의 이벤트를 처리할 수 있을 것 같지 않아서 블루스카이의 커스텀 피드 기능을 사용하기로 했습니다.
피드 개발도 나름대로 시간과 노동력이 드는 일이지만, 다행히 정규표현식 등을 사용한 커스텀 피드 제작을 지원하는 서드파티 서비스인 SkyFeed를 사용하여 특정 키워드가 포함된 게시물을 모아 보여주는 피드를 빠르게 만들 수 있었습니다.
이제 이 피드를 일정 시간 간격(현재 제 라벨러는 3분 간격)으로 폴링하여 라벨링을 수행하는 크론잡 스크립트만 만들면 완성입니다.
블루스카이 SDK
간단하게 파이썬의 atproto
라이브러리를 사용하여 로그인, 피드 쿼리, 라벨을 처리하는 방법을 알아봅시다.
로그인
from atproto_client import Client, models
import os
from dotenv import load_dotenv
load_dotenv()
labeler_did = os.getenv("LABELER_DID") # 라벨러 계정의 DID
handle = os.getenv("ACCT_HANDLE") # 로그인할 계정의 DID 또는 핸들
pw = os.getenv("ACCT_PW") # 로그인할 계정의 비밀번호
client = Client()
client.login(handle, pw)
# 모더레이션 API를 사용하고자 하는 경우 라벨러 계정의 DID를 'atproto_labeler' 프록시 헤더로 설정해야 합니다.
client.configure_proxy_header('atproto_labeler', labeler_did)
# 반대로 피드를 받아올 때에 라벨을 적용하고 싶은 경우 다음과 같이 설정합니다.
client.configure_labelers_header([labeler_did])
피드 쿼리
# 이전 코드에 이어서...
feed_uri = "at://[피드를 소유한 계정의 DID]/app.bsky.feed.generator/[피드 ID]"
feed_result = client.app.bsky.feed.get_feed(
models.AppBskyFeedGetFeed.Params(
feed=feed_uri,
limit=100,
cursor=None
)
)
cursor = feed_result.cursor # 다음 페이지를 쿼리하고 싶다면 이 커서를 사용합니다.
posts = [item.post for item in feed_result.feed] # 포스트 목록을 가져옵니다.
라벨 지정
# 이전 코드에 이어서...
for post in posts:
client.tools.ozone.moderation.emit_event(
models.ToolsOzoneModerationEmitEvent.Data(
created_by=client.me.did # 내 계정의 DID
event=models.ToolsOzoneModerationDefs.ModEventLabel(
create_label_vals=["추가할 라벨 identifier"],
negate_label_vals=["제거할 라벨 identifier"]
),
subject=models.ComAtprotoRepoStrongRef.Main(cid=post.cid, uri=post.uri),
)
)
각 API 엔드포인트에 대한 내용은 블루스카이 공식 문서와 AT 프로토콜 파이썬 SDK 문서에서 확인할 수 있습니다. AT 프로토콜의 모든 API는 기본적으로 Lexicon으로 정의되기 때문에, 해당 문서도 읽어 보면 좋습니다.
소감
모더레이션이라는 작업에 대한 소감을 말할 수 있게 되기에는 아직 라벨러를 오랜 시간 구동해 보지 않았지만, 라벨이 잘못 지정되는 경우와 사용자들이 라벨을 부정적인 경고의 의미로 받아들이는 경우가 있어 고민이 되기도 합니다. (기본적으로 블루스카이의 라벨은 '분류 도구'에 해당하고, 라벨이 적용된다고 해서 블루스카이 이용에 문제가 생기지는 않습니다. 사용자들이 프로필에서 자신의 시간대를 알려줄 수 있는 시간대 라벨도 있죠.)
한편 라벨러를 개발하는 것이 생각보다 훨씬 쉬웠다는 점이 인상깊었습니다. Lexicon 개념을 간단히 이해하고 나면 공식 문서만 보고도 블루스카이의 모든 API를 무리 없이 사용할 수 있었고, 오랜만에 가벼운 사이드 프로젝트 느낌으로 즐겁게 개발할 수 있었습니다. 여러분도 블루스카이의 오픈 소스 생태계에 원하는 기능이 있다면, 직접 만들어 보시는 건 어떨까요?
Dani Soohan Park (@heartade)
Follow this blog at Fediverse: @heartade@blog.heartade.dev
Follow my shorter shoutouts at Fediverse: @heartade@social.silicon.moe
Follow me at Bluesky: @heartade.dev