상호 보팅과 댓글을 보팅으로 후원하는 booming-kr phase 2.5 개발 중

in #kr15 days ago (edited)

지정된 사용자들 간에 보팅(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()
Sort:  

Upvoted! Thank you for supporting witness @jswit.