ABOUT ME

-

Total
-
  • Rust: actix를 이용하여 카카오 챗봇 만들기
    컴퓨터/Rust 2021. 8. 2. 16:06
    728x90
    반응형

    결과물

     

    백엔드 + 프론트엔드 + 카카오톡 챗봇 프로젝트

    Rust 언어

    github.com

    예제 카카오톡 챗봇 (Rust언어)

    Go언어  gin으로 만들기: @참고

    Python언어 FastAPI로 만들기: @참고

     

    Python, Go, C, Rust 언어로 카카오 챗봇을 만들어 보았는데

    Go랑 Rust로 만드는 걸 추천드립니다.

     

    1. 카카오 챗봇 만들기

    준비물

     

    위 준비를 다했으면 학교 일정을 MySQL에 저장해서,

    사용자가 "달력", "일정"... 이란 발화 문을 제시하면 MySQL에서 일정을 불러와서 보여줄 것이다.

    결과물

     

    2. 시나리오 만들기

    원하는 시나리오를 하나 만든다.

     

    발화 문 지정

    엔티티를 만들어서, "일정", "달력"과 같은 부분을 만들어서 엔티티를 받아서 하거나

    단순 내가 만든 발화 문에 있는 문구와 일치할 시 원하는 request를 줄 수 있다.

    단순히, "일정", "달력"

     

    3. 스킬 서버 만들기

    스킬 서버는 request를 받고 보낼 http 서버이다.

    일정을 보여주기 위해 사용할 end-point는 /schedule이다.

     

    스킬 서버를 만든 후 시나리오로 다시 가서 서버를 선택하고 스킬 데이터를 사용함을 체크한다.

    스킬 데이터 사용을 봇 응답으로 설정하면, 직접 응답 메시지를 json으로 보내주어야 한다.

     

    3-1. 메시지 스타일 종류

    json으로 직접 return 할 경우 최대 출력 크기를 맞춰야 정상적으로 메시지가 보인다.

    SimpleText

     

    SimpleImage

    간단 이미지만 보내기

    BasicCard

    Carousel에서도 쓰이는 기본 카드형 메시지이다.

    CommerceCard

    BasicCard에서 제품을 판매할 수 있는 스타일이다.

    ListCard

    헤더와 총 5개의 목록을 만들어서 보낼 수 있는 카드이다.

    Carousel

    BasicCard를 10개 가로로 보여줄 수 있는 메시지이다.

     

    4. MySQL과 Actix 웹 프레임워크

    async를 지원하는 가장 빠른 Rust 웹 프레임워크 actix-rs에 MySQL을 사용하는 법은 아래와 같다.

     

    폴더를 하나 만들어서 db 코드를 모아준다.

    db (db 코드 폴더)
        └---- connection.rs (db 연결)
        └---- mod.rs (db 폴더 모듈화)
        └---- models.rs (db 모델 struct)
        └---- query.rs (CRUD)
        └---- schema.rs (db 테이블 rust로 표시)

     

    src/lib.rs

    #![feature(proc_macro_hygiene, decl_macro)]
    
    extern crate actix_http;
    extern crate actix_rt;
    extern crate actix_web;
    
    #[macro_use]
    extern crate r2d2;
    #[macro_use]
    extern crate diesel;
    
    #[macro_use]
    extern crate serde_derive;
    #[macro_use]
    extern crate serde_json;
    
    extern crate rand;
    
    mod db;
    mod routes;
    
    pub use db::connection;
    pub use routes::chatbot;
    
    pub const SERVER: &str = "0.0.0.0:8008";
    pub const CARD_IMAGES: [&str; 2] = ["ajou_carousel", "ajou_carousel_1"];
    pub const MY_USER_AGENT: &str = "User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36";
    

     

    diesel.toml

    # For documentation on how to configure this file,
    # see diesel.rs/guides/configuring-diesel-cli
    
    [print_schema]
    file = "src/db/schema.rs"

     

    Cargo.toml

    actix (웹 프레임워크), serde (Json), reqwest (HTTP request)

    r2d2 (db), diesel (db), kakao-rs (챗봇 Json 만들기), rand (랜덤 index)

    [package]
    name = "rustserver"
    version = "1.0.0"
    edition = "2018"
    
    [dependencies]
    actix-rt = "2"
    actix-http = "3"
    actix-web = "4" 
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    serde_derive = "1.0"
    reqwest = { version = "0.11", features = ["json", "blocking"] }
    dotenv = "0.15.0"
    r2d2 = "*"
    diesel = { version = "2", features = ["mysql", "r2d2"] }
    kakao-rs = "0.3"
    rand = "0.8"
    
    [profile.dev]
    opt-level = 0
    
    [profile.release]
    opt-level = 3

     

    db/connection.rs

    Pool을 이용하여 actix에 추가할 것이다.

    .env 파일에 mysql 주소를 저장해두었다. (.gitignore에 .env를 추가해서 숨기기)

    형식: DATABASE_URL=mysql://id:password@주소/db이름

    use diesel::mysql::MysqlConnection;
    use diesel::r2d2::{self, ConnectionManager};
    
    use dotenv::dotenv;
    use std::env;
    
    // pub const DATABASE_FILE: &'static str = env!("DATABASE_URL");
    pub type DbPool = r2d2::Pool<ConnectionManager<MysqlConnection>>;
    
    pub fn init_pool() -> DbPool {
        dotenv().ok(); // Grabbing ENV vars
    
        // Pull DATABASE_URL env var
        let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    
        let manager = ConnectionManager::<MysqlConnection>::new(database_url);
    
        r2d2::Pool::builder()
            .build(manager)
            .expect("Failed to create pool.")
    }

     

    db/schema.rs

    MySQL ajou_sched 테이블

    diesel::table! {
        ajou_sched (id) {
            id -> Integer,
            start_date -> Varchar,
            end_date -> Varchar,
            content -> Varchar,
        }
    }

    db/models.rs

    MySQL ajou_sched 테이블

    위 콜럼들을 그대로 Rust로 옮겨준다.

    #![allow(proc_macro_derive_resolution_fallback)]
    
    use crate::db::schema::ajou_sched;
    
    #[derive(Queryable, AsChangeset, Serialize, Deserialize)]
    #[table_name = "ajou_sched"]
    pub struct Schedule {
        pub id: i32,
        pub start_date: String,
        pub end_date: String,
        pub content: String,
    }

    db/query.rs

    SELECT * FROM ajou_sched

    (Vec에 담겨있으니 unwrap() 후 사용)

    #![allow(proc_macro_derive_resolution_fallback)]
    
    use diesel;
    use diesel::prelude::*;
    
    use crate::db::models::Schedule;
    
    use crate::db::schema::ajou_sched::dsl::*;
    
    pub async fn show_scheds(conn: &MysqlConnection) -> QueryResult<Vec<Schedule>> {
        //posts.filter(published.eq(true))
        ajou_sched.load::<Schedule>(&*conn)
    }

     

    routes/chatbot.rs

    actix에서 post를 사용하고

    db를 받을 때는 그냥 actix_web::web::Data<내가만든DB>를 사용하면 된다.

    #![allow(proc_macro_derive_resolution_fallback)]
    use crate::db::connection::DbPool;
    use crate::db::models::Schedule;
    use crate::db::query;
    use crate::CARD_IMAGES;
    
    use kakao_rs::prelude::*;
    
    use actix_web::{post, web, HttpResponse, Responder};
    use rand::Rng;
    use serde_json::Value;
    use unicode_segmentation::UnicodeSegmentation;
    
    #[post("/schedule")]
    pub async fn get_schedule(conn: web::Data<DbPool>) -> impl Responder {
        let mut result = Template::new();
        let mut carousel = Carousel::new().set_type(BasicCard::id());
    
        let mut rng = rand::thread_rng();
    
        for sched in query::show_scheds(&conn.get().unwrap()).await.unwrap() {
            // println!("id: {}, content: {}", sched.id, sched.content);
    
            let basic_card = BasicCard::new()
                .set_title(format!("{}", sched.content))
                .set_desc(format!("{} ~ {}", sched.start_date, sched.end_date))
                .set_thumbnail(format!(
                    "https://raw.githubusercontent.com/Alfex4936/kakaoChatbot-Ajou/main/imgs/{}.png",
                    CARD_IMAGES[rng.gen_range(0..CARD_IMAGES.len())]
                ));
    
            carousel.add_card(basic_card.build_card());
        }
    
        result.add_output(carousel.build());
    
        HttpResponse::Ok()
            .content_type("application/json")
            .body(serde_json::to_string(&result).unwrap())
    }

     

    src/main.rs

    actix 4.0 beta-8 기준으로 add_data에 db를 clone 해주면 된다.

    4.0을 사용한 이유는 tokio v1+ 런타임 라이브러리를 사용하기 위함

    use actix_web::{middleware, web, App, HttpServer};
    use rustserver;
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        // std::env::set_var("RUST_LOG", "info,actix_web=info");
        // start http server
        HttpServer::new(|| {
            App::new()
                .app_data(web::Data::new(rustserver::connection::init_pool()))
                .wrap(middleware::Logger::default())
                .service(rustserver::chatbot::get_schedule)  // POST /schedule
        })
        .bind(rustserver::SERVER)?
        .run()
        .await
    }

     

    post에서 발화 문을 체크하고 싶으면 그냥 serde_jso::Value를 이용하면 편하다.

    use actix_web::{post, web, Responder};
    use serde_json::Value;
    
    #[post("/endpoint")]
    pub fn function(kakao: web::Json<Value>) -> impl Responder {
        println!("{}", kakao["userRequest"]["utterance"].as_str().unwrap()); // 발화문
        
        unimplemented!()
    }

     

    async 테스트를 만들려면 actix-rt::test를 사용하면 된다.

    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[actix_rt::test]
        async fn weather_test() {
            let weather = weather_parse().await.unwrap();
            println!("{:#?}", weather);
        }
    }

     

    AWS EC2 인스턴스

    배포를 하고 이제 서버를 실행시킨다.

    graphic interface가 없기 때문에, s3에 저장해서 불러와서 사용 중이다.

    HTML 파싱 같은 기능 때문에 Go버전이 더 빠른 것 같기도 하다.

    ubuntu:~$ aws s3 sync s3://bucket/rust-server .
    
    ubuntu:~$ cd rust-server && cargo run --release
    
        Finished release [optimized] target(s) in 3m 51s
         Running `target\release\rustserver`

    결과물

     

    풀 소스 @Github

     

    GitHub - 카카오톡 챗봇

    Rust 언어

    github.com

    자가 제작 카카오 챗봇 JSON 제작 헬퍼 (Rust)

     

    GitHub - Alfex4936/kakao-rs

    Rust언어용 Kakao i open builder 카카오 챗봇 JSON 응답 제작 도우미 모듈

    github.com

     

    728x90

    '컴퓨터 > Rust' 카테고리의 다른 글

    Rust: reqwest GET/POST snippet  (0) 2021.08.19
    Rust: scraper를 이용한 네이버 날씨 파싱  (0) 2021.07.28
    Rust: serde json Enum 공부  (0) 2021.07.28

    댓글