[개발] Node & Steem - 6편 리스팀 알림 봇을 만들어볼까? #2 whitelist 데이터 수집

in #kr-dev7 years ago (edited)

node_door.png

지난번부터 Node & Steem을 연재하면서의 목표는 제가 node.js를 배우면서 실제 서비스를 구현하는 방법과 과정을 담아 보시는 분들도 구성하는 과정을 보며 '아 ~ 이렇게 서비스를 만드는구나! ' 하는 것을 보여주는 것이 목표입니다. 이 서비스는 실제로 스팀잇에서 서비스를 개시하게 될 것이구요. 보시는 분들은 스팀의 봇 서비스는 대충 이런식으로 만들어졌겠구나 하는 감을 잡으실 수 있겠습니다. 물론 구현하는 개발자의 스킬과 개성에 따라 아키텍쳐는 다 다르겠지만 우로 치든 좌로 치든 이런식으로 목표에 접근하는 과정 자체는 비슷하지 않을까 생각합니다.

지난 리스팀 알림 봇 1편에 이어 계속해서 만들어나가보도록 하겠습니다. 제가 만들어나가는 건 리스팀 알림 봇이지만 여기서 조금만 수정한다면 댓글 봇, 보팅봇을 만드는 건 크게 어려운 일이 아닐 것입니다. 중간 중간에 자기만의 색채를 가미한 봇을 만들어보는 것도 꽤나 재미있는 일이겠지요. 그럼 지난시간에 이어 개발하는 과정을 시작하겠습니다.


1. 스팀블록 데이터에서 원하는 Operation만 추출해보기.

지난 시간에는 스팀블록 데이터를 열어서 살펴보는 시간을 가졌지요. 그러면 원하는 데이터만 추출해서 보도록 하겠습니다. 두가집니다. 하나는 리스팀알람을 등록하는 @리스팀 켬, @리스팀 끔의 코멘트를 인식해야하고 두번째는 리스팀 오퍼레이션을 인식하는 것입니다. 한번 해보죠.

먼저 지난시간에 뜯어본 스팀 블럭 데이터에 대해서 간단히 설명 드리면...

스팀블럭 데이터는 트랜잭션들로 이뤄져있고 이 트랜잭션 하나에는 오퍼레이션들로 묶여있습니다. 그래서 1 블럭 당 오퍼레이션이 대략 30~70개(+-@)가 들어가는데 이 오퍼레이션은 보팅, 코멘트(포스팅과 댓글), steem과 sbd 전송, 사용자 정보 업데이트, 리스팀 등 스팀 내에서 이뤄지는 행위들의 기록이 모여있습니다. 아마도 그 데이터 사이즈에 따라 오퍼레이션 갯수가 다른듯 합니다. 장문의 포스팅이 들어가면 그 갯수는 확 줄어들겠죠? 비트코인 블럭에 거래 데이터가 쌓이는 것과 비슷한 원리라고 이해가 됩니다.

제가 쥬시 같은 툴을 통해 스팀 블럭의 raw 데이터를 본건 아니지만 steem-js 상에 api를 통해 노드에서 정제되어 나온 데이터에는 이런식으로 보입니다. 허접한 그림으로 한번 보면 이렇습니다.




실제 트랜잭션 1당 대부분 오퍼레이션이 1개나 2개 정도로 보입니다. 제가 node.js 코딩으로 블럭 데이터의 오퍼레이션들을 쪼개 보는 스크립트를 작성하였습니다. 한번 보시죠~ 처음보면 다소 복잡하겠지만 지난 과정부터의 스크립트를 보신분이라면 조금씩 추가, 수정 된 것을 아실 수 있겠습니다.


try {
    fiber(function() {
        logger.info( "steem init!!" );
        // steem 데이터 조회!!
        //obj = await(steem.api.getAccounts(['nhj12311'], defer()));
        var lastBlockNumber = 0;
        var workBlockNumber = 0;
        var startChk = false;
        //console.log(steem.api);
        var release = steem.api.streamBlockNumber('head',function(err, blockNumber){
          lastBlockNumber = blockNumber;
          while( lastBlockNumber > workBlockNumber ){
            if( workBlockNumber == 0 ){
              // 이 부분은 향후 workBlockNumber DB 데이터로 초기화
              workBlockNumber = lastBlockNumber;
            }else{
                workBlockNumber++;
            }
            //logger.info( 'lastBlockNumber : ' + lastBlockNumber + ", workBlockNumber : " + workBlockNumber);
            fiber(function() {
              var block = await( steem.api.getBlock(workBlockNumber, defer()) );
              for(var txIdx = 0; txIdx < block.transactions.length; txIdx++ ){
                for(var opIdx = 0; opIdx < block.transactions[txIdx].operations.length; opIdx++ ){
                  var operation = block.transactions[txIdx].operations[opIdx];
                  // 리스팀을 인식하는 부분.
                  if( "custom_json" == operation[0] ){
                      if( operation[1].json ){
                        var custom_json = JSON.parse(operation[1].json);
                        if( "reblog" == custom_json[0] || "resteem" == custom_json[0] ){
                            logger.info( custom_json );
                        }
                      }

                  }// if( "custom_json" == operation[0] ){
                  // 포스팅과 댓글은 comment
                  else if( "comment" == operation[0] ){
                    if( operation[1].parent_author != ""){
                        if( operation[1].body.includes( [ "@리스팀" ] ) ){
                            logger.info( "@" + operation[1].author + " : " + operation[1].body);
                        }
                    }
                  }
                } // for operations
              } // for transactions
            }); // fiber
          } // if( lastBlockNumber > workBlockNumber ){
        }); // streamBlockNumber.
    });
} catch(err) {
  logger.error(err);
}

comment에서 @리스팀 이라는 텍스트를 인식하고, custom_json 부분에서 reblog를 인식하여 화면에 뿌려주는 코드입니다. 잘 안보이실수도 있겠지만 리스팀과 @리스팀 멘트를 읽어와 화면에 뿌려주는 gif입니다.

gif2.gif

이렇게 원하는 오퍼레이션을 인식해 봤습니다. 보팅봇이나 다른 댓글 봇들도 이런식으로 하면 비스무리 하게 만들수 있을것 같은 느낌입니다.


2. 리스팀 알람 대상 유저의 white list 테이블을 만들고 적재하기.

'@리스팀 켬'으로 댓글을 한 유저에 한 해 리스팀 알림 댓글을 받을 수 있도록 테이블을 구성하고 적재해야합니다. 어떤 정보를 적재할까요? 계정명, 댓글 주소, on, off 여부, 등록 및 수정 시간 정도 남기면 될 듯 합니다.

단, 서비스의 확장성을 고려해서 서비스 구분코드를 하나 더 만듭니다. 왜냐면 리스팀 알람만으로 기능을 끝내진 않을 것이니까요. 제가 자주 사용하는 용어로는 svcDvcd 정도로 사용하면 무리가 없을 듯 보입니다. 일반적으로 일을 할때에는 해당 표준을 따라서 테이블 구성을 하면 되는데요. 이런 개인 취미 프로젝트는 자기가 최대한 보기 편하게 짧고 굵게 구성하면 좋습니다. 최대한 짧으면서 의미가 이해 될 수 있도록요. 누군가와 협력하거나 인수인계하는데 c1, c2 이렇게 하면 참으로 오래 사실 수 있는 욕을 들으실 수 있습니다. 아주 바다거북이 급으로 무병장수 할수 있어요. ㅋㅋ.

(1). 테이블 만들기

테이블 명칭은 스팀(생략) 서비스 계정 관리(SVC_ACCT_MNG)정도로 하겠습니다.

SVC_ACCT_MNG

컬럼명데이터타입길이코멘트
seqint10일련번호
dvcdint3구분코드
acct_nmvarchar50계정명
perm_linkvarchar500요청링크
use_ynchar1사용여부
reg_dttmtimestamp-등록일시
mdf_dttmtimestamp-수정일시

이렇게 테이블 구조를 구상하고 지난 번에 설치한 HeidiSQL을 켜서 테이블을 만들어봅니다. 이렇게요.

seq와 reg_dttm, mdf_dttm에 기본값을 캡처와 같이 설정해줍니다. 잘 안보이시는 분들을 위해
seq는 auto_increment
reg_dttm은 current_timestamp
mdf_dttm은 current_timestamp on update current_timestamp

디테일은 설명은 생략합니다. 네이버나 구글에만 쳐도 수십개의 글이 나오기 때문에 구현 자체에 포커싱을 맞추도록 하겠습니다. 제가 보기에 중요한
건 이런식으로 서비스를 만들수 있구나. 정도가 아닐까 싶습니다. 뭔가 재미있지 않습니까? 이 작은 세상에서 무언가의 창조자가 된다는 것이 저는 참 좋습니다. 그래서 개발을 좋아하게 된 것 같습니다.

(2). 서비스 등록 멘트 발견 시 데이터 저장하기

이제 테이블을 만들었으니 데이터를 등록해봅시다. 본문 상의 소스인 139 line에서 등록해주면 될것 같습니다.

// 포스팅과 댓글은 comment
else if( "comment" == operation[0] ){
  if( operation[1].parent_author != ""){
      if( operation[1].body.includes( [ "@리스팀" ] ) ){
          // 바로 요기!
          logger.info( "@" + operation[1].author + " : " + operation[1].body);
      }
  }
}

지난번에 test 테이블을 통해 등록하는 건 해봤으니 그대로 가져다가 쓰면 됩니다. 테이블명과 항목들을 바꿔서 넣어주기만 하면 끝나겠죠? 단순한 논리로 관리하면서 됩니다. 이미 등록된 데이터를 y|n으로만 관리해주면 되니 데이터가 있으면 업데이트 없으면 등록입니다. 등록 가즈아!

한줄 씩 코딩을 넣어봅시다. 한꺼번에 다 완성하지 말고요. 조금씩 살을 붙인 다는 생각으로 기능을 추가합시다.

1. useYn으로 켬과 끔을 인식해서

// 포스팅과 댓글은 comment
else if( "comment" == operation[0] ){
  if( operation[1].parent_author != ""){
      if( operation[1].body.includes( [ "@리스팀" ] ) ){
          logger.info( "@" + operation[1].author + " : " + operation[1].body);
          var useYn = "";
          if( operation[1].body.indexOf( ["@리스팀 켬", "@리스팀켬", "@리스팀 on"] ) ){
            useYn = "Y";
          }else if( operation[1].body.includes( ["@리스팀 끔", "@리스팀끔", "@리스팀 off"] ) ){
            useYn = "N";
          }else{
            // 없으면 넘김.
            logger.info("comment continue");
            continue;
          }
          // dvcd : 첫번째 서비스인 리스팀 알림 서비스번호는 1번으로.
          var selQry = "select * from svc_acct_mng where dvcd = 1 and acct_nm = '"+ operation[1].author +"' ";
          logger.info("selQry : " + selQry);
          var selRslt = await(conn.query(selQry, defer() ));
          logger.info(selRslt);
      }
  }
} // else if( "comment" == operation[0] ){

테스트를 하는 모습. 개발도 이렇게 한땀 한땀 한답니다. 전 머리가 나빠서.. ㅋㅋㅋ

제가 포스팅 댓글로 테스트를 한 결과 :

16:56:07 - info: @nhj12311 : @리스팀 켬
16:56:07 - info: selQry : select * from svc_acct_mng where dvcd = 1 and acct_nm = 'nhj12311' 
16:56:07 - info: 

데이터가 없으니 결과가 안나오는군요. 손으로 데이터를 등록해줍시다. 제 계정 데이터가 있는 케이스를 손으로 만들어주는 것이죠.

그리고 다시 테스트!

17:41:32 - info: @nhj12311 : @리스팀 켬
17:41:32 - info: selQry : select * from svc_acct_mng where dvcd = 1 and acct_nm = 'nhj12311' 
17:41:32 - info:  seq=1, dvcd=1, acct_nm=nhj12311, perm_link=0, use_yn=Y, reg_dttm=Thu Feb 08 2018 17:18:31 GMT+0900 (대한민국 표준시), mdf_dttm=Thu Feb 08 2018 17:18:31 GMT+0900 (대한민국 표준시)

손으로 넣으니 이미 있는 데이터로 나옵니다. 있는지 없는지 판별하여 등록과 수정 작업을 진행합시다.


// 포스팅과 댓글은 comment
else if( "comment" == operation[0] ){
  if( operation[1].parent_author != ""){
      if( operation[1].body.includes( [ "@리스팀" ] ) ){
          logger.info( "@" + operation[1].author + " : " + operation[1].body);
          var useYn = "";
          if( operation[1].body.contains( ["@리스팀 켬", "@리스팀켬", "@리스팀 on"] ) ){
            useYn = "Y";
          }else if( operation[1].body.contains( ["@리스팀 끔", "@리스팀끔", "@리스팀 off"] ) ){
            useYn = "N";
          }else{
            // 없으면 넘김.
            logger.info("comment continue");
            continue;
          }
          // dvcd : 첫번째 서비스인 리스팀 알림 서비스번호는 1번으로.
          var selQry = "select * from svc_acct_mng where dvcd = 1 and acct_nm = '"+ operation[1].author +"' ";
          logger.info("selQry : " + selQry);
          var selRslt = await(conn.query(selQry, defer() ));
          logger.info(selRslt);
          var regQry = "";
          // 있으면 업데이트
          if( selRslt.length > 0 ){
            regQry = "update svc_acct_mng set use_yn = '"+ useYn +"' , perm_link = '" + operation[1].permlink + "' where dvcd = 1 and acct_nm = '"+ operation[1].author +"' ";
          }else{  // 없으면 등록!!!
            regQry = "insert into svc_acct_mng ( dvcd, acct_nm, perm_link, use_yn ) values (1, '"+ operation[1].author +"', '" + operation[1].permlink + "', '"+useYn+"' ) ";
          }
          logger.info("regQry : " + regQry);
          var regRslt = await(conn.query(regQry, defer() ));
          logger.info(regRslt);
      }
  }
} // else if( "comment" == operation[0] ){

다시 테스트를 열라게 해보면서 만든 스크립트입니다~

테스트 결과 로그

14:00:30 - info: @nhj12311 : @리스팀 끔
14:00:30 - info: selQry : select * from svc_acct_mng where dvcd = 1 and acct_nm = 'nhj12311' 
14:00:30 - info:  seq=1, dvcd=1, acct_nm=nhj12311, perm_link=re-nhj12311-node-and-steem-5-1-20180209t044654412z, use_yn=Y, reg_dttm=Thu Feb 08 2018 17:18:31 GMT+0900 (대한민국 표준시), mdf_dttm=Fri Feb 09 2018 13:47:12 GMT+0900 (대한민국 표준시)
14:00:30 - info: regQry : update svc_acct_mng set use_yn = 'N' , perm_link = 're-nhj12311-re-nhj12311-re-nhj12311-node-and-steem-5-1-20180209t050012505z' where dvcd = 1 and acct_nm = 'nhj12311' 
14:00:31 - info:  fieldCount=0, affectedRows=1, insertId=0, serverStatus=34, warningCount=0, message=(Rows matched: 1  Changed: 1  Warnings: 0, protocol41=true, changedRows=1
14:01:21 - info: @nhj12311 : @리스팀 켬
14:01:21 - info: selQry : select * from svc_acct_mng where dvcd = 1 and acct_nm = 'nhj12311' 
14:01:21 - info:  seq=1, dvcd=1, acct_nm=nhj12311, perm_link=re-nhj12311-re-nhj12311-re-nhj12311-node-and-steem-5-1-20180209t050012505z, use_yn=N, reg_dttm=Thu Feb 08 2018 17:18:31 GMT+0900 (대한민국 표준시), mdf_dttm=Fri Feb 09 2018 14:00:30 GMT+0900 (대한민국 표준시)
14:01:21 - info: regQry : update svc_acct_mng set use_yn = 'Y' , perm_link = 're-nhj12311-re-nhj12311-re-nhj12311-re-nhj12311-node-and-steem-5-1-20180209t050059621z' where dvcd = 1 and acct_nm = 'nhj12311' 
14:01:21 - info:  fieldCount=0, affectedRows=1, insertId=0, serverStatus=34, warningCount=0, message=(Rows matched: 1  Changed: 1  Warnings: 0, protocol41=true, changedRows=1

없는 상태에서의 등록 테스트를 위해서 부계정으로도 해봅니다.



잘 등록 되는군요. 이제 오늘까지 작업 한 내용을 토대로 서버에 배포하고 리스팀 등록(신청)을 받을 수 있게 된것 같습니다. 화이트리스트 수집 기능은 이로서 70% 정도는 완성된 것 같습니다.

제가 만드는 소스는 항상 github에 공개되어있으므로 언제든 가져가서 수정하셔서 사용하셔도 되고 참고하셔도 좋습니다.

https://github.com/nhj12311/steem_nhj

다음 포스팅에서 진행해봐야 할 내용은 댓글 다는 봇 계정을 신설하고 리스팀 등록 및 해제 시 안내 댓글과 리스팀 발생 시에 알림 댓글을 다는 기능을 구현하면 기본적인 리스팀 알림 봇의 기능 구현이 끝나게 됩니다. 봇 계정을 신설하고 댓글을 적절히 달 수 있는 방법을 구현해봅시다. 20초 룰이 있는 이상 적절히 스케줄링하여 댓글을 달 수 있도록 방법을 고안하여 구현하는 과정만 남았습니다. 각자 다음 포스팅 전까지 그 방법을 고민해보는 것도 재미있을 것 같습니다. 여러가지 방법이 있을테니까요.

그럼 다음 시간에 완성된 리스팀 안내 봇으로 다시 만나요~😄


node & steem - 지난 회차 살펴보기
1편 - nodejs 개발환경을 구성해보자. 윈도우 개발 + Github 저장소 + 리눅스 운영
2편 - 콜백 지옥을 탈출해보자. - synchronize.js 편
3편 - 로깅 처리와 DB(mysql)설치 및 설정
4편 - DB 설정과 운영서버까지 설정 마무으리!
5편 리스팀 알림 봇을 만들어볼까? #1


ps. 제게 공식적인 첫 대문이라는 걸 선물해주신 @forhappywomen님과 그려주신 @carrotcake님께 무한한 감사 인사드립니다. 이런걸 받다니 제 포스팅력이 무한 상승할 것만 같은 느낌이 듭니다. 😄😄😄

Sort:  

스스로 홍보하는 프로젝트에서 나왔습니다.
오늘도 좋은글 잘 읽었습니다.
오늘도 여러분들의 꾸준한 포스팅을 응원합니다.

upvote my comment please

좋은 정보 감사합니다
나중에 스팀 블럭구조도 공부해봐야겠습니다 흐흐

와 멋있습니다 ㅎ블럭을 뜯어보시다니요ㅎ
혹시 댓글에서 on한 포스팅만 적용하는 것이 아니라, 아이디를 신청받아 모든 글을 모니터링 해줄 수 도 있겠죠? 리스팀이 잦은 유명한 분들은 필수적일 것 같습니다.

이건 포스팅 당 적용이 아니라 서비스가 유지되는한 계정 별로 영구적인 적용의 컨셉입니당 ㅋㅋ 말씀대로 모든글이 해당 됩니다~

좋은 글에는 보팅이 제맛!!~ 팔로잉합니다
개발글 감사드려요!!~~

대단하십니다... 저와는 다른 영역으로ㅜ날아간듯한 느낌이 ㄷㄷㄷ

우왕~ 이런것도 가능하군요. 신기방기합니다. 잘보았습니다~^^

와 신기하네요 정말~!!^^ 봇을 이렇게~~ 다음편도 기대됩니당

이런거 하시는 분들 보면 그저 대단하다라는 말 밖에 안나오네요.ㅎ