상호 보팅과 댓글을 보팅으로 후원하는 booming-kr phase 2.5 개발 중
지정된 사용자들 간에 보팅(50% 이상)과 댓글을 다는 경우 @booming-kr이 해당 댓글에 보팅해주는 자동화 코드를 개발하고 있습니다.
사용 설명
이 시스템은 @booming-kr 계정이 A와 B 계정의 상호작용을 자동 감지하여 댓글에 보팅해주는 방식으로 작동합니다.
즉, A와 B가 서로의 원글에 댓글과 보팅(50% 이상)을 하면, 조건에 맞춰 booming-kr이 5분 뒤 지정된 비율로 해당 댓글에 자동 보팅합니다.
👉 댓글은 반드시 1단계 댓글이어야 하며, 자가 댓글/보팅은 무효입니다.
자주 묻는 질문(FAQ)
Q1. 5분이 지나도 보팅이 안 들어오는데요?
다음 항목을 확인해 주세요.
- 댓글이 대댓글(2단계 이상) 이 아닌가요? (→ 1단계 댓글만 인정)
- 보팅 비중이 임계치(50%)에 미달하진 않았나요?
- 서로(B↔C) 의 글에 한 상호작용이 맞나요? (자가 상호작용은 제외)
- 같은 댓글이 이미 예약/보팅 처리되지는 않았나요?
- 블록체인 반영이 잠시 지연될 수 있습니다. 시스템이 재조회/재시도합니다.
Q2. 댓글 본문에 제한이 있나요?
- 없습니다. 다만 스팸/커뮤니티 정책 위반은 피하세요.
Q3. 여러 댓글을 달면 어떤 댓글이 대상이 되나요?
- 조건 1: 조건을 충족한 그 댓글
- 조건 2: 해당 원글에서 본인이 남긴 ‘가장 최근’ 댓글
아래와 같은 Python 코드를 사용하였습니다.
# pip install beem
from beem import Steem
from beem.blockchain import Blockchain
from beem.comment import Comment
from beem.instance import set_shared_steem_instance
from beem.account import Account
from beem.exceptions import AccountDoesNotExistsException
from datetime import datetime, timezone, timedelta
import time
import sys
# ========= 설정값 =========
STEEM_NODES = ["https://api.steemit.com"] # 반드시 STEEM 노드 사용
A_ACCOUNT = "A_account" # A 계정 (보팅 주체)
POSTING_KEY = "your_posting_key" # A posting key (보팅에 필요)
# 서로 감시할 계정 2명과, 계정별 A의 보팅 비율(%) 설정
ALLOWED = {
"B_account": 100, # 100%
"C_account": 75 # 75%
}
WATCH_AUTHORS = tuple(ALLOWED.keys()) # ("B_account", "C_account")
# 강한 보팅 임계치 (내부 raw 스케일: 10000 = 100%)
THRESHOLD_GE_RAW = 5000 # ≥50% (댓글 트리거에 사용)
THRESHOLD_GT_RAW = 5000 # >50% (보팅 트리거에 사용; 필요시 5000으로 두되 비교는 >)
# 예약 보팅 지연 시간
VOTE_DELAY_SEC = 5 * 60 # 5분
# 먼저 보팅 → 나중 댓글 순서 보완용 보팅 캐시
VOTE_CACHE_TTL_SEC = 3 * 24 * 3600
vote_cache = {} # key: (voter, post_author, post_permlink) -> last_seen_epoch_sec
# ========= 유틸 =========
def parse_ts_utc_kst(ts_str: str):
try:
if ts_str.endswith("Z"):
dt_utc = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
else:
dt_utc = datetime.fromisoformat(ts_str).replace(tzinfo=timezone.utc)
except Exception:
dt_utc = datetime.now(timezone.utc)
kst = dt_utc.astimezone(timezone(timedelta(hours=9)))
return dt_utc, kst
def is_top_post(author: str, permlink: str) -> bool:
try:
post = Comment(f"@{author}/{permlink}")
return (post.get("parent_author") in ("", None))
except Exception:
return False
def pct_from_weight(weight_raw: int):
try:
return float(weight_raw) / 100.0
except Exception:
return None
def send_log(title: str, message: str):
print(f"\n==== {title} ====\n{message}\n")
# ========= Steem 인스턴스 =========
stm = Steem(node=STEEM_NODES, keys=[POSTING_KEY], num_retries=5, timeout=30)
set_shared_steem_instance(stm)
try:
_ = Account(A_ACCOUNT, steem_instance=stm)
except AccountDoesNotExistsException:
raise SystemExit(f"@{A_ACCOUNT} 계정을 {STEEM_NODES[0]}에서 찾을 수 없습니다.")
bchain = Blockchain(steem_instance=stm, mode="head")
# ========= 상태(중복/스케줄) =========
seen_comments = set() # "author/permlink"
seen_votes = set() # "voter/author/permlink"
scheduled_ids = set() # "author/permlink" (이미 예약된 댓글)
pending_votes = [] # [{execute_at, author, permlink, weight_pct}]
# ========= 캐시/확인 함수들 =========
def cleanup_vote_cache():
now = time.time()
dead = [k for k, ts in vote_cache.items() if now - ts > VOTE_CACHE_TTL_SEC]
for k in dead:
vote_cache.pop(k, None)
def record_strong_vote(voter: str, post_author: str, post_permlink: str, weight_raw: int):
# 서로 감시하는 두 계정 간 상호작용이며, 원글에 대한 ≥50% 보팅이면 캐시
if voter in WATCH_AUTHORS and post_author in WATCH_AUTHORS and voter != post_author:
if weight_raw >= THRESHOLD_GE_RAW:
vote_cache[(voter, post_author, post_permlink)] = time.time()
def cached_strong_vote_exists(voter: str, post_author: str, post_permlink: str) -> bool:
cleanup_vote_cache()
return (voter, post_author, post_permlink) in vote_cache
def check_active_votes_over_threshold(voter: str, post_author: str, post_permlink: str,
threshold_raw: int = THRESHOLD_GE_RAW,
retries: int = 4, delay: float = 2.0) -> bool:
# active_votes 지연 반영 대비 재시도
for attempt in range(retries):
try:
post = Comment(f"@{post_author}/{post_permlink}")
for v in post["active_votes"]:
if v.get("voter") == voter and int(v.get("percent", 0)) >= threshold_raw:
return True
except Exception as e:
print(f"[WARN] vote check failed ({attempt+1}/{retries}): {e}", file=sys.stderr)
time.sleep(delay)
return False
def has_user_commented_on_post(user: str, post_author: str, post_permlink: str) -> bool:
# 해당 원글의 1-depth 댓글에 user 댓글 존재 여부
try:
post = Comment(f"@{post_author}/{post_permlink}")
for rep in post.get_replies():
if rep["author"] == user:
return True
except Exception as e:
print(f"[WARN] replies check failed: {e}", file=sys.stderr)
return False
def get_latest_comment_by_user_on_post(user: str, post_author: str, post_permlink: str):
# 해당 원글에서 user가 단 가장 최근 댓글(1-depth)
try:
post = Comment(f"@{post_author}/{post_permlink}")
latest = None
latest_created = None
for rep in post.get_replies():
if rep["author"] != user:
continue
created = rep.get("created")
if latest is None or (created and latest_created and created > latest_created) or (created and not latest_created):
latest = Comment(f"@{rep['author']}/{rep['permlink']}")
latest_created = created
return latest
except Exception as e:
print(f"[WARN] get_latest_comment_by_user_on_post failed: {e}", file=sys.stderr)
return None
# ========= 예약/실행 =========
def schedule_vote_on_comment(author: str, permlink: str, weight_pct: int):
cid = f"{author}/{permlink}"
if cid in scheduled_ids:
return
execute_at = time.time() + VOTE_DELAY_SEC
pending_votes.append({
"execute_at": execute_at,
"author": author,
"permlink": permlink,
"weight_pct": weight_pct
})
scheduled_ids.add(cid)
dt_exec_kst = datetime.fromtimestamp(execute_at, tz=timezone(timedelta(hours=9)))
send_log("보팅 예약 생성",
f"- 대상 댓글: @{author}/{permlink}\n"
f"- 예약 비율: {weight_pct}%\n"
f"- 실행 예정(KST): {dt_exec_kst.strftime('%Y-%m-%d %H:%M:%S %Z')}")
def process_pending_votes():
now = time.time()
i = 0
while i < len(pending_votes):
job = pending_votes[i]
if now >= job["execute_at"]:
author = job["author"]
permlink = job["permlink"]
weight_pct = job["weight_pct"]
try:
c = Comment(f"@{author}/{permlink}")
# 이미 A가 보팅했는지 확인
already = any(v["voter"] == A_ACCOUNT and int(v.get("percent", 0)) > 0 for v in c["active_votes"])
if not already:
c.upvote(weight=weight_pct, voter=A_ACCOUNT)
send_log("보팅 실행 완료", f"- @{A_ACCOUNT} → @{author}/{permlink}\n- 비율: {weight_pct}%")
else:
send_log("보팅 생략(이미 보팅함)", f"- @{A_ACCOUNT} → @{author}/{permlink}")
except Exception as e:
print(f"[ERROR] 예약 보팅 실패 @{author}/{permlink}: {e}", file=sys.stderr)
# 실패 시 2분 후 재시도 (원치 않으면 아래 두 줄 주석 처리)
job["execute_at"] = time.time() + 120
i += 1
continue
pending_votes.pop(i) # 성공/생략 시 제거
else:
i += 1
# ========= 메인 루프 =========
def run():
print(f"👀 Watching mutual interactions between {WATCH_AUTHORS} via {STEEM_NODES[0]}")
# comment + vote 모두 구독
stream = bchain.stream(opNames=["comment", "vote"], raw_ops=False)
for op in stream:
try:
op_type = op.get("type")
# ----- 보팅 이벤트(조건 2): X가 Y의 '원글'에 >50% 보팅 && X가 이미 그 원글에 댓글 -----
if op_type == "vote":
voter = op.get("voter")
post_author = op.get("author")
permlink = op.get("permlink")
weight_raw = int(op.get("weight", 0))
# B/C 상호 간, 서로의 원글에 대한 보팅만 고려
if voter not in WATCH_AUTHORS or post_author not in WATCH_AUTHORS or voter == post_author:
process_pending_votes()
continue
# 원글 보팅만 대상
if not is_top_post(post_author, permlink):
process_pending_votes()
continue
# 먼저 캐시 기록(≥50%)
record_strong_vote(voter, post_author, permlink, weight_raw)
# 알림/스케줄 조건: >50% && 이미 댓글 존재 → X의 가장 최신 댓글에 예약
vid = f"{voter}/{post_author}/{permlink}"
if vid in seen_votes:
process_pending_votes()
continue
seen_votes.add(vid)
if weight_raw > THRESHOLD_GT_RAW and has_user_commented_on_post(voter, post_author, permlink):
latest = get_latest_comment_by_user_on_post(voter, post_author, permlink)
if latest:
weight_pct = ALLOWED.get(voter, 100)
schedule_vote_on_comment(latest["author"], latest["permlink"], weight_pct)
# ----- 댓글 이벤트(조건 1): X가 Y의 '원글'에 댓글 && X가 그 원글에 ≥50% 보팅 -----
elif op_type == "comment":
parent_author = op.get("parent_author")
author = op["author"] # 댓글 작성자(X)
if parent_author not in WATCH_AUTHORS or author not in WATCH_AUTHORS or author == parent_author:
process_pending_votes()
continue
parent_permlink = op["parent_permlink"]
# 반드시 원글에 대한 1-depth 댓글이어야 함
if not is_top_post(parent_author, parent_permlink):
process_pending_votes()
continue
cid = f"{author}/{op['permlink']}"
if cid in seen_comments:
process_pending_votes()
continue
seen_comments.add(cid)
# X가 해당 원글에 ≥50% 보팅했는지: 캐시 우선 → active_votes 재조회
voted_ok = cached_strong_vote_exists(author, parent_author, parent_permlink) or \
check_active_votes_over_threshold(author, parent_author, parent_permlink,
threshold_raw=THRESHOLD_GE_RAW,
retries=4, delay=2.0)
if voted_ok:
# 방금 단 그 댓글에 예약
weight_pct = ALLOWED.get(author, 100)
schedule_vote_on_comment(author, op["permlink"], weight_pct)
# 매 루프마다 예약 보팅 처리
process_pending_votes()
except KeyboardInterrupt:
print("종료 신호를 감지했습니다. 프로그램을 종료합니다.")
break
except Exception as e:
print(f"[ERROR] {e}", file=sys.stderr)
time.sleep(1)
process_pending_votes()
if __name__ == "__main__":
run()
Upvoted! Thank you for supporting witness @jswit.