BOJ 슬랙봇 만들기 (a.k.a. EkJoo-Bot)

GuruneLee·2021년 12월 5일
4

Toy Projects

목록 보기
1/2
post-thumbnail

text 알고리즘을 안 푼 친구를 신나게 갈궈보자

기획

기획 의도
: 친구들과 함께 만든 ps워크스페이스에서 오늘 문제 안푼 놈들에게 꼽을 주기위해서!

기능

등록되어 있는 사용자들에 대해, 특정 시간대 마다 오늘 하루 문제를 풀지 않은 사람들을 메시지에 담아 꼽을 줍니다.

ex) 1pm. 네! 뭐 점심먹고 얼마 안됐으니깐요! @Chlee4858 @Hyj879 님~
ex) 7pm. 식사는 맛있게 하셨을테고, 알고리즘 문제는 어떻게 되가나요 @Chlee4858 님???
ex) 1am. 잘시간이 다가와요!! 하하, 너 말고 @Chlee4858.
ex) 6am. @Chlee4858. 아웃.

시간대는 점심 후, 저녁 후, 자정 후, 동트기 직전 이 가장 괜찮은 것 같다.

기술 스택

Java 11

: java 로 백준 크롤링 구현해놓은 것이 있어서, 이걸 가져와서 구현한다. DB는 쓰지 않도록 한다. 왜냐면 지금은 사람 수가 적기 때문이다.

  • DB를 도입하면 할 수 있는게 늘어난다

gradle 7.3.1

: maven을 사용해 본 적이 없을 뿐더러, gradle이 훨씬 보기 편하다

SimpleSlackAPI

: Itiviti/simple-slack-api 스타가 400개가 넘게 있는 java slack 라이브러리! 사용이 간편하다 해서 가져와보았다.

  • 바뀐 API가 반영이 안된 듯 하다

개발환경 세팅하기

gradle Java 프로젝트 만들고 빌드해보기

  1. gradle로 java 프로젝트 만들기 && intellij 로 프로젝트 열기
    a. gradle version 7.3.1
    b. 깃 블로그 보고 따라했어용
  2. gradle 빌드 시행착오
    a. java가 제대로 설치가 되어있지 않아서 다시 설치함 링크 -> java 가 문제가 아니라, gradle이 컴파일해줄때 사용한 버전이 11이 아니었던거임 (아마 17인 것 같다)
    //error message
    Error: LinkageError occurred while loading main class algoBot.App
    java.lang.UnsupportedClassVersionError: algoBot/App has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0
    b. 해결) gradle에 java11 사용한다고 명시하기 gradle문서 introduction살펴보기
    //in build.gradle
    java {
      toolchain {
          languageVersion = JavaLanguageVersion.of(11)
      }
    }
  3. gradle 빌드
    빌드: gradle clean build
    실행: java -jar app/build/libs/app.jar
  • 라이브러리가 jar 파일에 포함되지 않는 문제
    - 해결 블로그
    • jar 파일을 만들기 위해선 여러 노력을 함께 해줘야 한다는 것을 기억하자
  • 다음엔, gradle wrapper 를 써보자
  • Spring 을 도입하는것은 어떨까?

Slack API

slack api docs

slack api 사용해보기 (ft. 고마운 블로그, Slack API Tester)

  1. chatbot 생성하기
    api.slack.com/apps로 접근해 app을 생성한다. OAuth&Permissions로 접근해 'channels:read'와 'chat:write'권한을 준다.
    이후 사이드바의 Install App으로 가서 InstallToWorkspace를 클릭하고, 토큰을 복사해놓자
  2. Slack 채널생성
    알림봇을 적용할 공개채널을 생성한다 (반드시 공개 채널이어야 한다고 하네요).
    채널에 알림봇 app을 추가한다.
  3. 채널 ID 알아내기
    전송하려는 채널의 ID를 알아내기 위해선, 전체 채널의 리스트 정보를 조회하는 API를 먼저 확인해야 한다. api.slack.com/methods/conversations.list/test에 접근하여 Tester 탭으로 들어가보자.
    token에 아까 저장해놨던 토큰을 붙여넣고, Test Method를 클릭하면 하단에 채널의 리스트 정보가 호출된다. 이중에서 채널의 이름을 찾아 id필드의 값을 얻어놓자.
    (채널 이름이 한글이면, 여기에 들어가서 문자열의 결과값의 %를 \로 바꾸어보자)
  4. Chat API 호출
    토큰과 채널ID이 함께라면 이제 두렵지 않다.
    api.slack.com/methods/chat.postMessage에 접근해 token과 channel을 든든이들로 채워주고, text에 테스트할 메시지를 넣어서 잘 가는지 테스트해보자
    잘간다!!!!!!!!!!!

api-docs 읽어보기

Simple Slack API 사용해보기

기각 (API 가 바뀐걸 반영하지 않은 듯 함)

슬랙 공식 API에 직접 요청보내기

slack api chat.postMessage 의 tester와 reference docs를 보고 직접 요청을 보내봤다.
(Slack 봇은 그대로 사용하였다)

PostMan으로 메시지 전송 요청 보내기

POST https://slack.com/api/chat.postMessage
Headers
	- ContentType : application/json, application/x-www-form-urlencoded
    - Authorization : Bearer {slack 봇 토큰}
Query Params
	- channel : {채널ID}
	- text : {보낼 메시지}
  • token을 입력하는 부분에서 헤맸는데, 'token={토큰}' 형태가 아니라 'Autorization=Bearer {토큰}' 형태로 헤더에 담아야 한다
    - Authentication token bearing required scopes. Tokens should be passed as an HTTP Authorization header or alternatively, as a POST parameter. 라고 문서에 명시되어 있다

Java 코드로 메시지 전송 요청 보내기 (java.net.http)

public void send() throws Exception {
        //create a connection to a given URL using POST method
        URL url = new URL("https://slack.com/api/chat.postMessage");
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
        
        //Setting Headers
        httpURLConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        httpURLConnection.setRequestProperty("Authorization", "Bearer {slack Bot 토큰}");
        httpURLConnection.setRequestMethod("POST");

        //Adding Request Params
        Map<String, String> params = new HashMap<>();
        params.put("channel", "{채널ID}");
        params.put("text", "test text");
        httpURLConnection.setDoOutput(true);
        DataOutputStream out = new DataOutputStream(httpURLConnection.getOutputStream());
        out.writeBytes(ParameterStringBuilder.getParamsString(params));
        out.flush();
        out.close();

        //Configuring TimeOut
        httpURLConnection.setConnectTimeout(5000);
        httpURLConnection.setReadTimeout(5000);

        //Reading the Response
        BufferedReader in = new BufferedReader(
                new InputStreamReader(httpURLConnection.getInputStream())
        );
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();

        //Disconnect
        httpURLConnection.disconnect();

        //Print the content
        System.out.println("content = " + content);
    }
  • TroubleShooting
    - header 세팅하는 부분은 httpURLConnection 인스턴스를 사용하기 전에 완료 해야한다 (이미 연결했다고 에러남) (stackoverflow참고)
  • 위 코드에 대한 자세한 설명+공부내용은 따로 쓴 포스트를 참고하자

개발을 시작해보자

필요한 기능 및 세부 정책

필요한 기능

  1. BOJ에서 해당 시간에 문제를 풀었는지 안풀었는지 알아내기
    a. 꼽을 줄 BOJ 사용자 이름을 등록해두기
    b. 사용자 정보중에 해당 시간에 풀었는지 알아내는 방법 생각하기
    c. 오늘의 기준과, 오늘 풀었다는 사실을 저장놔야하고, 오늘의 시간에 이를 초기화해야 한다
    d. 매일 반복하기
  2. Slack 에 꼽메시지 보내기
    a. 보낼 메시지 생성해서 저장해놓기
    b. 해당시간에 풀지 않은 사람의 아이디를 담아 Slack 메시지 보내기
    c. 매일 반복하기

작동 방식

우선 DB는 사용하지 않는다. 따로 설치해야 하기도 하고, 사람이 많지 않아서 우선은 필요없을 듯 하다.
-> DB 도입하면 더 많은 것을 할 수 있을 듯

  1. (1-a) resource 폴더에 사용자정보 파일 생성하기 How gradle read file from resources folder
    • Json 이나 yml 파일형식이면 읽어오기 편할듯
  2. (1-b) BOJ에서 문제를 제출한 시간을 알아낼 수 있음 Jsoup CookBook
    - http 요청 명세
    GET 'https://www.acmicpc.net/status'
    query params
      problem_id : 특정 문제만 보려면 값을 넣고, 아니면 value를 안쓰고 보내면 됨
      user_id : BOJ 아이디
      language_id : -1 이면 언어 선택 안한거
      reseult_id : 4가 '맞았습니다!' 가 뜬 문제만 보는거
    • 예시 : https://www.acmicpc.net/status?problem_id=&user_id=dlckdgk4858&language_id=-1&result_id=4
    • 요청을 보내면 table\[id="status-table"]>tbody>tr 에서 td>a\[class="real-time-update show-date"] 인 요소에서 title 을 확인하면 날짜와 시간을 알 수 있다.
      (아마 show-date 가 시간을 보여주는 그룹인가보다)
    <table class="table table-striped table-bordered" id="status-table">
      ...
      <tbody>
        ...
        <tr id="solution-36065509" data-can-view="1">
          ...
          <td>
            <a href="javascript:void(0);" rel="tooltip" data-placement="top" title="2021-12-06 02:42:25" data-timestamp="1638726145" class="real-time-update show-date " data-method="from-now">22시간 전</a>
          </td>
        </tr>
        ...
      </tbody>
    </table>
  3. (2-a) 이것도 resource 폴더에 Json 이나 yml 로 저장해놓자
  4. (1-c,2-c) 매일 반복하기

구현

GitHub Source

GuruneLee/BOJ-Slack-Bot

algoBot
├── App.java
├── boj
│   └── BOJClient.java
├── helper
│   └── httpHelper
│       ├── HttpConnection.java
│       ├── HttpHelper.java
│       └── ParameterStringBuilder.java
├── slack
│   ├── SlackBot.java
│   └── SlackMessage.java
└── timer
    ├── HowAreUToday.java
    └── Scheduler.java

Run 해보자

  1. 실행
    : 앱을 시작하면, 다음과 같이 Timer-0에 오늘의 시간대가 설정된다. 그 후, Timer-0가 Timer-1에 오늘의 시간대 마다 작업을 하도록 스케쥴링을 한다.
  2. 메시지:
    잘 작동 된다면... 다음과 같이 Slack에 메시지가 가는 것을 볼 수 있다.

    (꼽을 더 제대로 주고 싶다. 아숩.)

개선할 점

  1. Java Timer 가 작동을 안해요
    : 사진처럼 7시, 11시에 각각 실행 되었어야 할 동작이 11시에 한 번에 실행된다. (심지어 딱 11시도 아니다)

    Http 요청에 Timeout 을 넣어주지 않아서 문제가 생긴줄 알았는데, 넣어도 시간이 안맞고, 심지어 단순히 텍스트만 출력하도록 해도 시간이 잘 안맞았다.

다른 예상되는 원인은 Timer 를 이중으로 써서 그런 듯 한데... 다음 코드를 보면 mainTimer의 schedule 함수에 dailyTimer 가 있다. 쓰레드에 대해 더 공부를 해야 풀 수 있는 문제인 것 같다.
(단순하게 타이머를 짜도 되긴 하는데... 더 테스트 해보자)

public void schedule() {
    long cycle = 24*60*60*1000L;
    HowAreUToday.setTodayDate();

    TimerTask theFirstTask = new TimerTask() {
        @Override
        public void run() {
            initDailySchedule();
        }
    };

    mainTimer.scheduleAtFixedRate(theFirstTask, toDate(LocalDateTime.of(HowAreUToday.TODAY_DATE, LocalTime.of(6,0))), cycle);
}

public void initDailySchedule() {
    HowAreUToday.setDailyTime();

    for (LocalDateTime time : HowAreUToday.getDailyTimeList()) {
        if (time.isAfter(LocalDateTime.now()))
            dailyTimer.schedule(getTaskAt(time), toDate(time));
    }
}

private TimerTask getTaskAt(LocalDateTime todo) {
    return new TimerTask() {
        @Override
        public void run() {
            try {
                logger.info("{}에 할 일이 {}에 실행되었습니다.",
                        todo.format(DateTimeFormatter.ofPattern("MM월dd일 HH시mm분")),
                        LocalDateTime.now().format(DateTimeFormatter.ofPattern("MM월dd일 HH시mm분"))
                );
                List<String> beingNotSolveMembersToday = bojClient.crawlBeingNotSolveMembersToday();
                SlackMessage message = new SlackMessage();
                message.setContentByDailyTime(todo, beingNotSolveMembersToday);
                slackBot.sendMessage(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
}

text 수많은 테스트....ㅠ

  1. DB가 없다
    : 현재는 모든 정보를 메모리나 파일로 저장해야한다. '며칠동안 안풀었는지' '몇일 연속 풀었는지' 등의 고도화된 정보를 보내기 위해서는 DB가 있어야 더 편할 듯 하다

  2. Slack API 더 활용하기
    : 꼽을 더 제대로 주기 위해서는 slack 채널에서 개인 DM을 보내는 등의 조치가 필요하다. 이를 위해서는 슬랙 봇의 권한을 더 열어서 더 구현을 하면 좋을 듯 하다. (재밌겠당 ㅎ)

이정도의 개선점이 당장 생각난다.
이 외에도 spring 을 도입해서 더 나은 성능을 보장한다거나, http 요청을 더 고도화해서 안정적으로 요청을 처리한다거나 하는 등의 것들이 있다.

마치며

친구들에게 꼽을 주겠다는 일념 하나로 개발한 첫 봇이다. Java 와 Gradle 의 사용법에 대해 더 알아갔고, 지금까지 Spring 으로만 개발을 해와서 겪을 수 없었던 것을 공부할 수 있었다. 특히 http 요청을 날려 페이지를 수집하는 것에 에로사항이 좀 있었는데, 이에 대해 생각해 볼 수 있어서 좋았다.

확실히 개발은 누구 괴롭힐 생각으로 하면 제일 재밌는것 같다.

text

profile
Today, I Shoveled AGAIN....

1개의 댓글

comment-user-thumbnail
2021년 12월 8일

백준봇의 꼽질에 정신이 번쩍듭니다!

답글 달기