[Spring Boot] Telegram api를 사용한 메시지 전송 자동화

Sungjin Cho·2024년 7월 24일
0

Spring Boot

목록 보기
7/15
post-thumbnail

Telegram api를 사용한 메시지 전송 자동화

쇼핑몰 프로젝트를 진행하면서 주문이 들어오거나 특정 갯수 이하로 재고가 떨어지거나 재고가 0개가 되는 경우 텔레그램을 통해 쇼핑몰 관리자에게 메시지를 전송하는 기능이 필요하다.

이를 위해서 telegram api 를 사용하는데 설계 순서는

  1. Telegram 메시지 전송 테스트
    동적으로 데이터가 변화되었을 때 메시지를 전송하는 것이 아닌 하드 코딩 된 값을 전송해보는 telegram api 테스트
  2. db에 트리거를 생성하여 주문이 들어왔을 때 관련 정보를 telegram_msg 테이블에 저장
  3. 하드 코딩 된 값이 아니라 동적으로 값을 가져올 수 있도록 spring boot 코드 작성

이 과정으로 진행하였다.

  1. botToken과 chatId 설정
  • https://api.telegram.org/bot 이 url에 token 값과 sendMessage 를 연결해서 https://api.telegram.org/bot/tokenvalue/sendMessage 이런 url로 api를 요청하는데 이 때 method는 post, httpentity의 body에 chat_id=chattingid&parse_mode=html&text= 라는 string 값을 넣어서 보내면 원하는 채팅에 메시지를 전송할 수 있다.
  • 사용중인 token과 charid값을 가져오도록 properties 파일에 저장 (참고로 prefix는 여러 쇼핑몰에서 사용할 경우를 고려하여 쇼핑몰의 이름을 저장)
  1. 데이터 처리
    spring application 상에서 api 요청을 보내면 telegram의 api에서 자동으로 메시지를 보내는 구조이기 때문에 데이터만 잘 넣어서 api 요청을 보내면 된다. 따라서 send 메서드에서 contents라는 JSONArray를 매개변수로 받아 전송하도록 추상 클래스를 정의하고 클래스를 구현하여 해당 로직을 Spring Batch를 통해 job에서 호출하도록 하였다.

전체 코드

telegram api 통신

 // MyTelegramBot.java

@Component
public abstract class MyTelegramBot {

    protected final String prefix;
    protected final String botToken;
    protected final String chatId;
    protected final String url;
    protected final String payload;
    private static final String method		= "POST";

    private static final Map<String, String> headers = new HashMap<>();

    static {
        headers.put("Content-Type", "application/x-www-form-urlencoded");
    }

    @Autowired
    public MyTelegramBot(@Value("${telegram.bot.prefix}") String prefix,
                         @Value("${telegram.bot.token}") String botToken,
                         @Value("${telegram.chat.id}") String chatId) {
        this.prefix = prefix;
        this.botToken = botToken;
        this.chatId = chatId;
        this.url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
        this.payload = "chat_id=" + chatId + "&parse_mode=html&text=";
    }


    protected void send(String key, JSONArray contents) {
        int size = contents.size();
        JSONObject content = null;
        Map<String, String> map = null;
        for(int i = 0 ; i < size ; i++) {
            map = new HashMap<String,String>();
            content = (JSONObject)contents.get(i);
            Set<String> set = content.keySet();
            Iterator<String> iter = set.iterator();
            String _key = null;
            String _value = null;
            while(iter.hasNext()) {
                _key = iter.next().toString();
                _value = content.get(_key).toString();
                map.put(_key, _value);
            }
            send(key,map);
        }
        post(key,contents);

    }

    private void send(String key, Map<String,String> content) {
        String _payload = getLinkWithContent(key,content);
        try {
            String response = Util.send(method, url, headers, _payload);
            // 여기서 response를 처리할 수 있습니다. 예를 들어, 로깅 등
            System.out.println("Telegram API Response: " + response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected String getLinkWithContent(String key, Map<String,String> content) {
        String templete = getTemplate(key);
        //String _payload = "";
        String link = getATag(key, content);
        if(link.startsWith("<a") || link.startsWith("<A")) {
            link = payload + link+ replace(templete,content) + "</a>";;
            //link = _payload;
        }else {
            link = payload + replace(templete,content);;
        }
        return link;
    }

    protected abstract String getATag(String key, Map<String,String> content);

    protected abstract void post(String key, JSONArray contents);

    /**
     * get message template by key
     * write templete for purpose
     * replacement block %key_block%
     * ex :
     * [주문필요] vendor: %vendor% , item_name : %item_name% , qty: %qty%
     *
     * @param key templete caller
     * @return message templete
     */
    protected abstract String getTemplate(String key);

    /**
     * get replacement block should be matched json key
     *
     *  _map = new JSONObject();
     *  _map.put("vendor", "5S Distributor");
     *  _map.put("item_name", "shin ramen");
     *  _map.put("qty", "3");
     *
     * @param key templete caller
     * @return
     */
    protected abstract JSONArray getContents(String key);

    protected String replace(String template,Map<String,String> map) {
        Set<String> set = map.keySet();
        Iterator<String> iter = set.iterator();
        String key = null;
        String value = null;
        while(iter.hasNext()) {
            key = iter.next().toString();
            value = map.get(key);
            template = template.replaceAll("%"+key+"%", value);
        }
        //return "<a href='http://youtube.com'>"+template+"</a>";
        return template;
    }

    protected Map<String, String> getHeaders() {
        return headers;
    }
}
// Util.java

public class Util {

    public static String send(String method, String url, Map<String, String> headers, String content) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders httpHeaders = new HttpHeaders();

        // Set headers
        headers.forEach(httpHeaders::set);
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // Set HTTP method
        HttpMethod httpMethod = HttpMethod.valueOf(method);

        // Create HttpEntity with headers and body
        HttpEntity<String> entity = new HttpEntity<>(content, httpHeaders);

        // Send request and get response
        ResponseEntity<String> response = restTemplate.exchange(url, httpMethod, entity, String.class);

        // Return response body
        return response.getBody();
    }
}
// Telegram4Mart

@Component
public class Telegram4Mart extends MyTelegramBot {

    @Autowired
    public Telegram4CityMart(@Value("${telegram.bot.prefix}") String prefix,
                             @Value("${telegram.bot.token}") String botToken,
                             @Value("${telegram.chat.id}") String chatId) {
        super(prefix, botToken, chatId);
    }

    protected String getTemplate(String key) {
        String template = "";

        if(key.equalsIgnoreCase("outofstock")){
            //template = "[재고없음] vendor: %vendor% , item_name : %item_name%";
            template = "[Out Of Stock] vendor: %vendor% , item_name : %item_name%";
            //template = reader.getProperty(prefix + "_outofstock_template");
            // gaza_outofstock_template=[재고없음] vendor: %vendor% , item_name : %item_name%
        }

        else if(key.equalsIgnoreCase("safetystock")){
            //template = "[물품주문필요] amount : %amount% php , ordered by:  %orderer%";
            template = "[Need to order Item] amount : %amount% php , ordered by:  %orderer%";
        }

        else if(key.equalsIgnoreCase("ordered")){
            //template = "[주문접수] amount : %amount% php , ordered by:  %orderer%";
            template = "[Order received] amount : %amount% php , ordered by:  %orderer%";
        }

        // 배송출발
        // 배송도착
        // 정산개시
        // 정산종료

        return template;
    }

   

    @Override
    protected void post(String key, JSONArray contents) {
        // TODO Auto-generated method stub
        if(key.equalsIgnoreCase("outofstock")){
            // update query
            // remote.updateUpdate(~)
            // 언제 몇 건이 통보되었는지 출력
            System.out.println("updated for out of stock");
        }

        else if(key.equalsIgnoreCase("safetystock")){
            // update query
            // remote.updateUpdate(~)
            // 언제 몇 건이 통보되었는지 출력
            Date date = new Date();
            //System.out.printf("%tA, %tB %tY %n", date, date, date);
            //System.out.printf("updated for safestock at hours: %tH, minutes: %tM, seconds: %tS %n",  date, date, date);

        }

        else if(key.equalsIgnoreCase("ordered")){
            // update query
            // remote.updateUpdate(~)
            // 언제 몇 건이 통보되었는지 출력
            Date date = new Date();
            System.out.printf("updated for safestock at hours: %tH, minutes: %tM, seconds: %tS %n",  date, date, date);
        }


    }

    @Override
    protected String getATag(String key, Map<String, String> content) {
        String aTag = "";
        if(key.equalsIgnoreCase("outofstock")){
            aTag="<a href='http://citymart.joshi.co.kr/shop/item.php?it_id=" + content.get("it_id") + "'>";
        }

        else if(key.equalsIgnoreCase("safetystock")){
            aTag="<a href='http://citymart.joshi.co.kr/shop/item.php?it_id=" + content.get("it_id") + "'>";

        }

        else if(key.equalsIgnoreCase("ordered")){
            aTag = "<a href='http://citymart.joshi.co.kr/adm/shop_admin/orderform.php?od_id=" + content.get("od_id") + "'>";
        }
        return aTag;
    }
}

Spring Batch 및 Scheduled 적용

위의 코드들이 telegram api와 통신할 부분이고 아래는 Spring Batch에서 위에서 정의한 메서드들을 활용해 job에 넣고 @Scheduled 어노테이션을 사용하여 1초에 한번씩 테이블을 확인해서 전송해야할 메시지가 있는지 체크하는 코드이다.

//TelegramJob.java

@Configuration
@RequiredArgsConstructor
public class TelegramJob {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final Telegram4CityMart telegramBot;
    private final TelegramMsgRepository telegramMsgRepository;
    private final JSONParser jsonParser = new JSONParser();

    @Bean(name = "myTelegramJob")
    public Job myTelegramJob() {
        return new JobBuilder("telegramJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(sendPendingMessagesStep())
                .build();
    }

    @Bean
    public Step sendPendingMessagesStep() {
        return new StepBuilder("sendPendingMessagesStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    List<TelegramMsg> pendingMessages = telegramMsgRepository.findByIsUpdatedFalse();
                    for (TelegramMsg msg : pendingMessages) {
                        try {
                            JSONObject jsonObject = (JSONObject) jsonParser.parse(msg.getMsgInfo());
                            JSONArray jsonArray = new JSONArray();
                            jsonArray.add(jsonObject);
                            telegramBot.send(msg.getKey(), jsonArray);
                            msg.setIsUpdated(true);
                            telegramMsgRepository.save(msg);
                        } catch (ParseException e) {
                            // JSON 파싱 에러 처리
                            e.printStackTrace();
                        }
                    }
                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}
// TelegramJobScheduler

@Component
@Slf4j
public class TelegramJobScheduler {

    private final JobLauncher jobLauncher;
    private final Job telegramJob;

    public TelegramJobScheduler(
            JobLauncher jobLauncher,
            @Qualifier("myTelegramJob") Job telegramJob) {
        this.jobLauncher = jobLauncher;
        this.telegramJob = telegramJob;
    }

    @Scheduled(fixedDelay = 1000) // 1초마다 실행
    public void runTelegramJob() {
        runJob(telegramJob, "telegram");
    }

    private void runJob(Job job, String jobName) {
        try {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addLong("time", System.currentTimeMillis())
                    .toJobParameters();
            jobLauncher.run(job, jobParameters);
        } catch (Exception e) {
            log.error("Error occurred while running {} job: ", jobName, e);
        }
    }
}

0개의 댓글