-
Rust: actix를 이용하여 카카오 챗봇 만들기컴퓨터/Rust 2021. 8. 2. 16:06728x90반응형
결과물
Go언어 gin으로 만들기: @참고
Python언어 FastAPI로 만들기: @참고
Python, Go, C, Rust 언어로 카카오 챗봇을 만들어 보았는데
Go랑 Rust로 만드는 걸 추천드립니다.
1. 카카오 챗봇 만들기
준비물
- 카카오 i 챗봇 사용 허락 @링크
- AWS EC2나 구름 IDE 환경 준비하기 @AWS EC2 만들기 링크
- AWS RDS (MySQL), AWS S3
위 준비를 다했으면 학교 일정을 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
자가 제작 카카오 챗봇 JSON 제작 헬퍼 (Rust)
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