From f3909bbf4e2c2578831f3036e97166b1d5462c1e Mon Sep 17 00:00:00 2001 From: Allan Jorge Date: Fri, 20 Mar 2020 20:50:40 -0300 Subject: [PATCH] add scheduler --- migrations/.gitkeep | 0 .../down.sql | 6 + .../up.sql | 36 +++ .../down.sql | 2 + .../2020-03-20-151829_create_processes/up.sql | 13 + src/commands.rs | 53 +--- src/database.rs | 13 + src/main.rs | 13 +- src/models.rs | 11 + src/process.rs | 105 +++++++ src/processo.html | 275 ++++++++++++++++++ src/scheduler.rs | 30 ++ src/schema.rs | 10 + 13 files changed, 519 insertions(+), 48 deletions(-) create mode 100644 migrations/.gitkeep create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2020-03-20-151829_create_processes/down.sql create mode 100644 migrations/2020-03-20-151829_create_processes/up.sql create mode 100644 src/database.rs create mode 100644 src/models.rs create mode 100644 src/process.rs create mode 100644 src/processo.html create mode 100644 src/scheduler.rs create mode 100644 src/schema.rs diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2020-03-20-151829_create_processes/down.sql b/migrations/2020-03-20-151829_create_processes/down.sql new file mode 100644 index 0000000..75cbcfd --- /dev/null +++ b/migrations/2020-03-20-151829_create_processes/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE processes diff --git a/migrations/2020-03-20-151829_create_processes/up.sql b/migrations/2020-03-20-151829_create_processes/up.sql new file mode 100644 index 0000000..877179b --- /dev/null +++ b/migrations/2020-03-20-151829_create_processes/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +CREATE TABLE processes ( + id serial PRIMARY KEY, + code text NOT NULL, + telegram_user_id TEXT NOT NULL, + status text NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp NOT NULL DEFAULT NOW(), + + UNIQUE(telegram_user_Id, code) +); + diff --git a/src/commands.rs b/src/commands.rs index e1f6eff..65fca31 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,9 +1,7 @@ -use regex::Regex; -use reqwest; -use select::document::Document; -use select::predicate::{Class, Name, Predicate}; use telegram_bot::*; +use crate::process; + pub async fn start(api: &Api, message: &Message) -> Result<(), Error> { api.send(message.text_reply(format!( "Olá, {}! Esse bot irá lhe ajudar a acompanhar o estado do seu processo.\n\ @@ -25,13 +23,15 @@ pub async fn invalid(api: &Api, message: &Message) -> Result<(), Error> { Ok(()) } -pub async fn code(api: &Api, message: &Message, data: &String) -> Result<(), Error> { +pub async fn code(api: &Api, message: &Message, data: &str) -> Result<(), Error> { let code: String = data.split_whitespace().skip(1).take(1).collect(); let reply = if code.is_empty() { String::from("Você me precisa me enviar algum código") } else { - let process = fetch_citizenship_status(&code).await.unwrap(); + let process = process::start(message.from.id.to_string(), code) + .await + .unwrap(); format!( "Status: {}\n\ Mensagem: {}", @@ -43,44 +43,3 @@ pub async fn code(api: &Api, message: &Message, data: &String) -> Result<(), Err Ok(()) } - -struct Process { - status: String, - info: String, -} - -async fn fetch_citizenship_status(code: &String) -> Result { - let res = reqwest::Client::new() - .post("https://nacionalidade.justica.gov.pt/Home/GetEstadoProcessoAjax") - .form(&[("SenhaAcesso", code)]) - .send() - .await?; - let body = res.text().await?; - - let document = Document::from(body.as_str()); - - let mut process = Process { - status: String::from("Unknown"), - info: String::from(""), - }; - - if let Some(st) = document.find(Class("active1").descendant(Name("p"))).last() { - process.status = st.text(); - } - - if let Some(st) = document.find(Class("active2").descendant(Name("p"))).last() { - process.status = st.text(); - } - - if let Some(st) = document.find(Class("active3").descendant(Name("p"))).last() { - process.status = st.text(); - } - - if let Some(st) = document.find(Class("container")).last() { - let re = Regex::new(r"\s+").unwrap(); - - process.info = re.replace_all(&st.text(), " ").trim().to_string(); - } - - Ok(process) -} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..a5a546c --- /dev/null +++ b/src/database.rs @@ -0,0 +1,13 @@ +extern crate dotenv; + +use diesel::pg::PgConnection; +use diesel::prelude::*; +use dotenv::dotenv; +use std::env; + +pub fn establish_connection() -> PgConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) +} diff --git a/src/main.rs b/src/main.rs index 819584b..e43c50b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,24 @@ +#[macro_use] +extern crate diesel; + use std::env; use futures::StreamExt; use telegram_bot::*; + mod commands; +mod database; +mod models; +mod process; +mod scheduler; +mod schema; #[tokio::main] async fn main() -> Result<(), Error> { let token = env::var("TELEGRAM_BOT_TOKEN").expect("TELEGRAM_BOT_TOKEN not set"); - let api = Api::new(token); + let api = Api::new(&token); + + scheduler::start(token.clone()); // Fetch new updates via long poll method let mut stream = api.stream(); diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..b090e6c --- /dev/null +++ b/src/models.rs @@ -0,0 +1,11 @@ +use chrono::NaiveDateTime; + +#[derive(Queryable, Debug)] +pub struct Process { + pub id: i32, + pub code: String, + pub telegram_user_id: String, + pub status: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..f8c4e90 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,105 @@ +use diesel::prelude::*; +use regex::Regex; +use reqwest; +use select::document::Document; +use select::predicate::{Class, Name, Predicate}; +use std::error; + +use crate::database; +use crate::models::*; +use crate::schema::processes::dsl::*; + +#[derive(Debug)] +pub struct ProcessResponse { + pub status: String, + pub info: String, +} + +pub async fn start( + telegram_id: String, + process_code: String, +) -> Result> { + let p = fetch_for_one(&process_code).await?; + save(&telegram_id, &process_code, &p.status).await; + + Ok(p) +} + +async fn fetch_for_one(process_code: &str) -> Result> { + Ok(fetch_citizenship_status(&process_code).await?) +} + +async fn save(telegram_id: &str, process_code: &str, process_status: &str) { + let connection = database::establish_connection(); + diesel::insert_into(processes) + .values(( + telegram_user_id.eq(telegram_id), + code.eq(process_code), + status.eq(process_status.to_lowercase()), + )) + .on_conflict((telegram_user_id, code)) + .do_update() + .set(status.eq(process_status.to_lowercase())) + .execute(&connection) + .expect("error while inserting process"); +} + +pub async fn fetch_for_all() -> Result, Box> { + let connection = database::establish_connection(); + let user_processes = processes + .filter(status.ne("finished")) + .load::(&connection) + .expect("Error loading processes"); + + let mut updated_processes = Vec::new(); + for process in user_processes { + let process_response = fetch_for_one(&process.code).await?; + if process_response.status.to_lowercase() != process.status { + save( + &process.telegram_user_id, + &process.code, + &process_response.status, + ) + .await; + updated_processes.push((process.telegram_user_id, process_response)); + } + } + + Ok(updated_processes) +} + +async fn fetch_citizenship_status(process_code: &str) -> Result { + let res = reqwest::Client::new() + .post("https://nacionalidade.justica.gov.pt/Home/GetEstadoProcessoAjax") + .form(&[("SenhaAcesso", process_code)]) + .send() + .await?; + let body = res.text().await?; + + let document = Document::from(body.as_str()); + + let mut p = ProcessResponse { + status: String::from("unknown"), + info: String::from(""), + }; + + if let Some(st) = document.find(Class("active1").descendant(Name("p"))).last() { + p.status = st.text(); + } + + if let Some(st) = document.find(Class("active2").descendant(Name("p"))).last() { + p.status = st.text(); + } + + if let Some(st) = document.find(Class("active3").descendant(Name("p"))).last() { + p.status = st.text(); + } + + if let Some(st) = document.find(Class("container")).last() { + let re = Regex::new(r"\s+").unwrap(); + + p.info = re.replace_all(&st.text(), " ").trim().to_string(); + } + + Ok(p) +} diff --git a/src/processo.html b/src/processo.html new file mode 100644 index 0000000..69ce41b --- /dev/null +++ b/src/processo.html @@ -0,0 +1,275 @@ + + + + + Justi�a.gov.pt + + + + + + + +
+ + + +
+
+ + + + + +
+
+
+

+ Qual é o estado do meu processo de nacionalidade +

+
+
+
+ +
+ + + +
+
+ + + + + + + + +
+
+ +
+ +
+ + +
+ + +
+ +
Estado do processo 96972 / 2019
+
(315 - Art. 1º C - Atribuição (nasc. estrangeiro))
+ +
+
O processo encontra-se na Conservatória do Registo Civil de + Tondela
+
ANDREA MORGADO DOS SANTOS
+ + + + +
+
+
Detalhe do estado do processo de nacionalidade
+
+
+
+
1
+

Foi recebido

+
+
+
+
2
+

Foi registado

+
+
+
+
3
+

Aguarda consultas

+
+
+
+
4
+

Verificação documentos

+
+
+
+
5
+

Análise pedido

+
+
+
+
6
+

Despacho

+
+
+
+
7
+

Terminado

+
+
+
+
+
+ +
+
Os serviços estão a preparar a documentação e informação para a + análise do seu pedido.

+
+
+
Sobre o processamento dos pedidos de nacionalidade

+
+
Um pedido de nacionalidade segue os seguintes passos:
+ +
1. Receção do pedido, numa conservatória, consulado ou por correio
+
2. Registo do pedido
+
3. Consulta a entidades externas
+
4. Verificação da documentação entregue
+
5. Análise de que todas as condições legalmente previstas estão reunidas para + conceder a nacionalidade
+
6. Decisão sobre a atribuição ou não da nacionalidade
+
7. Registo do novo cidadão português no Registo Civil de Portugal ou arquivamento + do processo
+ +
+ + +
+
+

+ Voltar +

+
+ +
+ + + +
+
+ +
+ +
+ +
+
+
+
+ + + + + + + + + + +
+
+
+
+
+
+ + + diff --git a/src/scheduler.rs b/src/scheduler.rs new file mode 100644 index 0000000..011e8f0 --- /dev/null +++ b/src/scheduler.rs @@ -0,0 +1,30 @@ +use crate::process; +use std::time::Duration; +use telegram_bot::types::refs::UserId; +use telegram_bot::*; +use tokio; + +pub fn start(token: String) { + // api.send(request: Req) + tokio::spawn(async move { + let api = Api::new(&token); + + loop { + let citizenships = process::fetch_for_all().await.unwrap(); + + for (telegram_user_id, process_response) in citizenships { + let user_id = UserId::new(telegram_user_id.parse::().unwrap()); + let message = SendMessage::new( + user_id, + format!( + "Status: {}\n\ + Mensagem: {}", + process_response.status, process_response.info + ), + ); + api.send(message).await.unwrap(); + } + tokio::time::delay_for(Duration::from_secs(1)).await; + } + }); +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..e95e06e --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,10 @@ +table! { + processes (id) { + id -> Int4, + code -> Text, + telegram_user_id -> Text, + status -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +}