~misterio/sistemer-bot

4cc42080bd60ae40423ef5886f093d0ae2be6114 — Gabriel Fontes 2 years ago fc54c73
wip: nova versão, usando o novo endpoint ao invés de scrappar
12 files changed, 359 insertions(+), 342 deletions(-)

M .build.yml
M Cargo.lock
M Cargo.toml
M flake.lock
D src/agora.rs
D src/disciplina.rs
D src/disciplinas.rs
D src/hoje.rs
D src/horarios.rs
M src/lib.rs
M src/main.rs
D src/proxima.rs
M .build.yml => .build.yml +11 -5
@@ 1,16 1,22 @@
image: nixos/unstable

packages:
  - nixos.cachix

environment:
  NIX_CONFIG: "experimental-features = nix-command flakes"
  repo: "sistemer-bot"
  cachix: "misterio"

secrets:
  - f2907d38-97b4-4e7d-9fb9-57b3fb0135af

tasks:
- setup_cachix: |
    cat ~/.cachix_token | cachix authtoken --stdin
    cachix use misterio
    cachix authtoken --stdin < ~/.cachix_token
    cachix use "$cachix"
- build: |
    cd sistemer-bot
    nix --quiet build
    cd "$repo"
    nix build
- upload_cachix: |
    nix path-info sistemer-bot/result/ -r | cachix push misterio
    nix path-info "$repo"/result/ -r | cachix push "$cachix"

M Cargo.lock => Cargo.lock +138 -22
@@ 24,6 24,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3"

[[package]]
name = "aquamarine"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96e14cb2a51c8b45d26a4219981985c7350fc05eacb7b5b2939bceb2ffefdf3e"
dependencies = [
 "itertools",
 "proc-macro-error",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "async-trait"
version = "0.1.52"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 218,6 231,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"

[[package]]
name = "dptree"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d07c51087ee4ee69519cb965ce56d05fb0ef23de5caffc69778d36b1e99a3e7"
dependencies = [
 "futures",
]

[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 246,6 268,16 @@ dependencies = [
]

[[package]]
name = "erasable"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f11890ce181d47a64e5d1eb4b6caba0e7bae911a356723740d058a5d0340b7d"
dependencies = [
 "autocfg",
 "scopeguard",
]

[[package]]
name = "fastrand"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 394,7 426,7 @@ checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [
 "cfg-if 1.0.0",
 "libc",
 "wasi",
 "wasi 0.10.0+wasi-snapshot-preview1",
]

[[package]]


@@ 423,6 455,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"

[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"

[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 539,6 577,15 @@ dependencies = [
]

[[package]]
name = "indoc"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e"
dependencies = [
 "unindent",
]

[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 554,6 601,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"

[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
 "either",
]

[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 582,9 638,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.112"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"

[[package]]
name = "lock_api"


@@ 649,14 705,15 @@ dependencies = [

[[package]]
name = "mio"
version = "0.7.14"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
dependencies = [
 "libc",
 "log",
 "miow",
 "ntapi",
 "wasi 0.11.0+wasi-snapshot-preview1",
 "winapi",
]



@@ 849,6 906,30 @@ dependencies = [
]

[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
 "proc-macro-error-attr",
 "proc-macro2",
 "quote",
 "syn",
 "version_check",
]

[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
 "proc-macro2",
 "quote",
 "version_check",
]

[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 873,6 954,15 @@ dependencies = [
]

[[package]]
name = "rc-box"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0690759eabf094030c2cdabc25ade1395bac02210d920d655053c1d49583fd8"
dependencies = [
 "erasable",
]

[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1077,18 1167,18 @@ dependencies = [

[[package]]
name = "sistemer-bot"
version = "1.1.4"
version = "2.0.0-pre1"
dependencies = [
 "anyhow",
 "chrono",
 "dotenv",
 "indoc",
 "log",
 "pretty_env_logger",
 "regex",
 "reqwest",
 "serde",
 "teloxide",
 "tokio",
 "unidecode",
]

[[package]]


@@ 1105,9 1195,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"

[[package]]
name = "socket2"
version = "0.4.2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [
 "libc",
 "winapi",


@@ 1131,14 1221,28 @@ dependencies = [
]

[[package]]
name = "take_mut"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"

[[package]]
name = "takecell"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"

[[package]]
name = "teloxide"
version = "0.5.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9964854e5ec3a5a44a9f50ebb7641327f1084ab4fc37a6c4a23cc011a388dc2e"
checksum = "1661e7429fceb92080e41548b2fcc5b9cc38db8740316efbd9b53ba693cac0cf"
dependencies = [
 "aquamarine",
 "async-trait",
 "bytes",
 "derive_more",
 "dptree",
 "flurry",
 "futures",
 "log",


@@ 1157,10 1261,11 @@ dependencies = [

[[package]]
name = "teloxide-core"
version = "0.3.4"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114c9057a3a2f74d937ece64029b362f583a69fb4b7405722c6dc03cd5bb4658"
checksum = "f37ad94e503964412cd4894138bf1644454f26060db4fa6bc698befa7d6d6656"
dependencies = [
 "bitflags",
 "bytes",
 "chrono",
 "derive_more",


@@ 1171,10 1276,13 @@ dependencies = [
 "never",
 "once_cell",
 "pin-project",
 "rc-box",
 "reqwest",
 "serde",
 "serde_json",
 "serde_with_macros",
 "take_mut",
 "takecell",
 "thiserror",
 "tokio",
 "tokio-util",


@@ 1184,10 1292,11 @@ dependencies = [

[[package]]
name = "teloxide-macros"
version = "0.4.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fb7e97b8bef2231aea6643558147c7f9c112675c4ca49f24d8fac2edff1216d"
checksum = "7d08322f107110dc4aadf5683bf19df9340eebc567529def2d17c830de198a58"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",


@@ 1243,7 1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
 "libc",
 "wasi",
 "wasi 0.10.0+wasi-snapshot-preview1",
 "winapi",
]



@@ 1264,9 1373,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "tokio"
version = "1.15.0"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
dependencies = [
 "bytes",
 "libc",


@@ 1276,6 1385,7 @@ dependencies = [
 "once_cell",
 "pin-project-lite",
 "signal-hook-registry",
 "socket2",
 "tokio-macros",
 "winapi",
]


@@ 1389,10 1499,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"

[[package]]
name = "unidecode"
version = "0.3.0"
name = "unindent"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc"
checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"

[[package]]
name = "url"


@@ 1445,6 1555,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +6 -6
@@ 1,6 1,6 @@
[package]
name = "sistemer-bot"
version = "1.1.4"
version = "2.0.0-pre1"
description = "Telegram bot for BSI 020"
edition = "2018"
license = "GPL-3.0-or-later"


@@ 9,13 9,13 @@ homepage = "https://misterio.me"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
teloxide = { version = "0.5", features = ["auto-send", "macros"] }
teloxide = { version = "0.7.2", features = ["auto-send", "macros"] }
log = "0.4"
pretty_env_logger = "0.4.0"
tokio = { version =  "1.8", features = ["rt-multi-thread", "macros"] }
pretty_env_logger = "0.4"
tokio = { version =  "1.17", features = ["rt-multi-thread", "macros"] }
dotenv = "0.15"
reqwest = "0.11"
anyhow = "1.0"
regex = "1.5"
chrono = "0.4"
unidecode = "0.3"
serde = { version = "1.0", features = ["derive"] }
indoc = "1.0"

M flake.lock => flake.lock +6 -6
@@ 2,11 2,11 @@
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1642104392,
        "narHash": "sha256-m71b7MgMh9FDv4MnI5sg9MiBVW6DhE1zq+d/KlLWSC8=",
        "lastModified": 1648390671,
        "narHash": "sha256-u69opCeHUx3CsdIerD0wVSR+DjfDQjnztObqfk9Trqc=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "5aaed40d22f0d9376330b6fa413223435ad6fee5",
        "rev": "ce8cbe3c01fd8ee2de526ccd84bbf9b82397a510",
        "type": "github"
      },
      "original": {


@@ 24,11 24,11 @@
    },
    "utils": {
      "locked": {
        "lastModified": 1638122382,
        "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
        "lastModified": 1648297722,
        "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=",
        "owner": "numtide",
        "repo": "flake-utils",
        "rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
        "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade",
        "type": "github"
      },
      "original": {

D src/agora.rs => src/agora.rs +0 -19
@@ 1,19 0,0 @@
use crate::{disciplina, hoje};
use anyhow::Result;
use chrono::offset::Local;
use regex::Regex;

pub async fn get_agora() -> Result<String> {
    let horarios_texto = hoje::get_horarios_hoje().await?;
    let agora = Local::now().time();

    for linha in horarios_texto.lines() {
        let (inicio, fim) = crate::get_intervalo(linha)?;
        if inicio <= agora && agora <= fim {
            let nome_disciplina = Regex::new("^.*<a href=\"#(.*)\">.*$")?.replace(linha, "$1");
            log::info!("Disciplina: {:?}", nome_disciplina);
            return disciplina::get_disciplina(&nome_disciplina).await;
        }
    }
    Ok("Nenhuma aula agora!".into())
}

D src/disciplina.rs => src/disciplina.rs +0 -29
@@ 1,29 0,0 @@
use anyhow::Result;
use regex::Regex;

pub async fn get_disciplina(disciplina: &str) -> Result<String> {
    let fulltext = reqwest::get("https://misterio.me/notes/bsi/disciplinas-2021-2.html")
        .await?
        .text()
        .await?;

    let start_pattern = Regex::new(&format!("<h2.*{}.*</h2>", unidecode::unidecode(&disciplina.to_lowercase())))?;
    let stop_pattern = Regex::new("<hr />")?;

    let mut output: String = "".into();
    let mut adding = false;
    for line in fulltext.lines() {
        if start_pattern.is_match(&unidecode::unidecode(&line.to_lowercase())) {
            output.push_str(&crate::sanitize_line(line, false)?);
            adding = true;
        } else if adding {
            if stop_pattern.is_match(line) {
                return Ok(output);
            } else {
                output.push_str(&crate::sanitize_line(line, false)?);
            }
        }
    }

    Ok("Disciplina não encontrada!".into())
}

D src/disciplinas.rs => src/disciplinas.rs +0 -20
@@ 1,20 0,0 @@
use anyhow::Result;
use regex::Regex;

pub async fn list_disciplinas() -> Result<Vec<String>> {
    let fulltext = reqwest::get("https://misterio.me/notes/bsi/disciplinas-2021-2.html")
        .await?
        .text()
        .await?;

    let disciplina_pattern = Regex::new("<h2 id=\"S")?;

    let mut output = Vec::new();
    for line in fulltext.lines() {
        if disciplina_pattern.is_match(line) {
            output.push(crate::sanitize_line(line, true)?);
        }
    }

    Ok(output)
}

D src/hoje.rs => src/hoje.rs +0 -41
@@ 1,41 0,0 @@
use crate::horarios;
use anyhow::Result;
use chrono::{offset::Local, Datelike, Duration, Weekday};

fn nome_dia(weekday: Weekday) -> String {
    match weekday {
        Weekday::Mon => "Segunda",
        Weekday::Tue => "Terça",
        Weekday::Wed => "Quarta",
        Weekday::Thu => "Quinta",
        Weekday::Fri => "Sexta",
        Weekday::Sat => "Sábado",
        Weekday::Sun => "Domingo",
    }
    .into()
}

pub async fn get_horarios_hoje() -> Result<String> {
    let texto_horarios = horarios::get_horarios().await?;
    let nome_hoje = nome_dia(Local::today().weekday());
    let nome_amanha = nome_dia((Local::today() + Duration::days(1)).weekday());

    let mut output: String = "".into();
    let mut adding = false;
    for line in texto_horarios.lines() {
        if line.contains(&nome_hoje) {
            adding = true;
        } else if line.contains(&nome_amanha) {
            adding = false;
        } else if adding {
            output.push_str(&crate::sanitize_line(line.trim(), false)?);
        }
    }

    let output = if output.is_empty() {
        "Sem aulas hoje!".into()
    } else {
        output
    };
    Ok(output)
}

D src/horarios.rs => src/horarios.rs +0 -28
@@ 1,28 0,0 @@
use anyhow::Result;
use regex::Regex;

pub async fn get_horarios() -> Result<String> {
    let fulltext = reqwest::get("https://misterio.me/notes/bsi/disciplinas-2021-2.html")
        .await?
        .text()
        .await?;

    let start_pattern = Regex::new("<h2 id=\"horários\">")?;
    let stop_pattern = Regex::new("<hr />")?;

    let mut output: String = "".into();
    let mut adding = false;
    for line in fulltext.lines() {
        if start_pattern.is_match(&line.to_lowercase()) {
            output.push_str(&crate::sanitize_line(line, false)?);
            adding = true;
        } else if adding {
            if stop_pattern.is_match(line) {
                return Ok(output);
            } else {
                output.push_str(&crate::sanitize_line(line, false)?);
            }
        }
    }
    Ok(output)
}

M src/lib.rs => src/lib.rs +194 -43
@@ 1,44 1,195 @@
pub mod agora;
pub mod disciplina;
pub mod disciplinas;
pub mod hoje;
pub mod horarios;
pub mod proxima;

use anyhow::Result;
use chrono::NaiveTime;
use regex::Regex;

fn get_intervalo(input: &str) -> Result<(NaiveTime, NaiveTime)> {
    let inicio = Regex::new("^(.*) - .*:.*$")?.replace(input, "$1");
    let fim = Regex::new("^.* - (.*):.*$")?.replace(input, "$1");
    let parse = |x| NaiveTime::parse_from_str(x, "%H:%M");
    Ok((parse(&inicio)?, parse(&fim)?))
}

fn sanitize_line(line: &str, remove_bold: bool) -> Result<String> {
    let line = format!("{}\n", line);
    // Replace headers with <b>
    let line = Regex::new("h\\d")?.replace_all(&line, "b");
    // Remove <ul> entire lines
    let line = Regex::new("^ *</?ul>\n")?.replace_all(&line, "");
    let line = Regex::new("</?ul>")?.replace_all(&line, "");
    // Remove <li>
    let line = Regex::new("^ *</?li>\n")?.replace_all(&line, "");
    let line = Regex::new("</?li>")?.replace_all(&line, "");
    // Remove <p>
    let line = Regex::new("</?p>")?.replace_all(&line, "");
    // Remove email links
    let line = Regex::new("<a href=\"mailto:.*>(.*)</a>")?.replace(&line, "$1");
    // Fix calculadora links
    let line = Regex::new("/notes/bsi/calculadora")?
        .replace(&line, "https://misterio.me/notes/bsi/calculadora");

    let line = if remove_bold {
        Regex::new("<b.*>(.*)</b>")?.replace(&line, "$1")
    } else {
        line
    };

    Ok(line.into())
pub use anyhow::Result;

use anyhow::anyhow;
use indoc::formatdoc;
use serde::Deserialize;
const URL: &str = "http://127.0.0.1:4000";

#[derive(Deserialize)]
pub struct Disciplina {
    codigo: String,
    nome: String,
    #[serde(default)]
    optativa: bool,
    professor: Option<Professor>,
    plataforma: Option<Informacao>,
    presenca: Option<Informacao>,
    sala: Option<Informacao>,
    avaliacoes: Option<Avaliacoes>,
}

impl Disciplina {
    pub async fn listar_disciplinas(turma: &str) -> Result<Vec<Self>> {
        let url = format!("{}/disciplinas/{}.json", URL, turma);
        let response = reqwest::get(&url).await?;

        let disciplinas = response.json().await?;
        Ok(disciplinas)
    }
    pub async fn buscar_disciplina(turma: &str, busca: &str) -> Result<Self> {
        let disciplinas = Self::listar_disciplinas(turma).await?;
        let encontrado = disciplinas
            .into_iter()
            .find(|x| x.codigo == busca)
            .ok_or(anyhow!("Disciplina não encontrada"))?;
        Ok(encontrado)
    }

    fn titulo(&self) -> String {
        let codigo = &self.codigo;
        let nome = &self.nome;
        format!("*{codigo} - {nome}*")
    }

    fn sobre(&self) -> String {
        let tipo = if self.optativa {
            "optativa"
        } else {
            "obrigatória"
        };
        let prof = self
            .professor
            .as_ref()
            .map(|p| p.info())
            .unwrap_or("TBD".into());
        let plataforma = self
            .plataforma
            .as_ref()
            .map(|p| p.info())
            .unwrap_or("TBD".into());
        let sala = self.sala.as_ref().map(|p| p.info()).unwrap_or("TBD".into());
        let presenca = self
            .presenca
            .as_ref()
            .map(|p| p.info())
            .unwrap_or("TBD".into());

        formatdoc!(
            "
            *Sobre*
            _Tipo_: {tipo}
            _Prof_: {prof}
            _Plataforma_: {plataforma}
            _Sala_: {sala}
            _Presença_: {presenca}"
        )
    }

    pub fn info(&self) -> String {
        let titulo = self.titulo();
        let sobre = self.sobre();
        let avaliacoes = self
            .avaliacoes
            .as_ref()
            .map(|a| a.info())
            .unwrap_or_default();

        formatdoc!(
            "
            {titulo}

            {sobre}
            {}{avaliacoes}",
            if avaliacoes.is_empty() { "" } else { "\n" }
        )
    }
}

#[derive(Deserialize)]
struct Professor {
    nome: String,
    email: String,
}

impl Professor {
    pub fn info(&self) -> String {
        format!("{} ({})", self.nome, self.email)
    }
}

#[derive(Deserialize)]
struct Informacao {
    info: String,
    url: Option<String>,
}

impl Informacao {
    pub fn info(&self) -> String {
        match &self.url {
            Some(url) => format!("[{}]({})", self.info, url),
            None => format!("{}", self.info),
        }
    }
}

#[derive(Deserialize)]
struct Avaliacoes {
    criterio: Option<String>,
    provas: Vec<Avaliacao>,
    atividades: Vec<Avaliacao>,
}
impl Avaliacoes {
    pub fn info(&self) -> String {
        let provas = self
            .provas
            .iter()
            .map(|x| format!("  {}", x.info()))
            .collect::<Vec<String>>()
            .join("\n");

        let atividades = self
            .atividades
            .iter()
            .map(|x| format!("  {}", x.info()))
            .collect::<Vec<String>>()
            .join("\n");

        let criterio = self.criterio.as_deref().unwrap_or("TBD");

        formatdoc!(
            "
            _Criterio_: {criterio}
            _Provas_:
            {provas}
            _Atividades_:
            {atividades}"
        )
    }
}

#[derive(Deserialize)]
struct Avaliacao {
    nome: String,
    data: Option<String>,
    assunto: Option<String>,
}

impl Avaliacao {
    pub fn info(&self) -> String {
        let nome = &self.nome;
        let data = match &self.data {
            Some(d) => format!(": {d}"),
            None => "".into(),
        };
        let assunto = match &self.assunto {
            Some(a) => format!("- {a}"),
            None => "".into(),
        };
        format!("{nome}{data}{assunto}")
    }
}

#[derive(Deserialize)]
struct Aulas {
    segunda: Option<Aula>,
    terca: Option<Aula>,
    quarta: Option<Aula>,
    quinta: Option<Aula>,
    sexta: Option<Aula>,
}

#[derive(Deserialize)]
struct Aula {
    inicio: String,
    fim: String,
}

M src/main.rs => src/main.rs +4 -104
@@ 1,107 1,7 @@
use teloxide::prelude::*;
use teloxide::types::{KeyboardButton, KeyboardMarkup, ParseMode::Html, ReplyMarkup};
use teloxide::utils::command::BotCommand;

use anyhow::Result;
use dotenv::dotenv;
use regex::Regex;

use sistemer_bot::{agora, disciplina, disciplinas, hoje, horarios, proxima};

#[derive(BotCommand)]
#[command(rename = "lowercase", description = "Comandos que tenho no momento:")]
enum Command {
    #[command(description = "exibe essa ajuda.")]
    Help,
    #[command(description = "mostra info de uma disciplina.")]
    Disciplina(String),
    #[command(description = "mostra info de uma disciplina.")]
    D(String),
    #[command(description = "lista os horários.")]
    Horarios,
    #[command(description = "informações da aula atual.")]
    Agora,
    #[command(description = "informações da próxima aula.")]
    Proxima,
    #[command(description = "quais as aulas de hoje.")]
    Hoje,
}

fn disciplina_row(disciplina: String) -> Vec<KeyboardButton> {
    let disciplina = Regex::new(".* - ").unwrap().replace_all(&disciplina, "");
    vec![KeyboardButton {
        text: format!("/d {}", disciplina),
        request: None,
    }]
}

async fn answer_command(cx: UpdateWithCx<AutoSend<Bot>, Message>, command: Command) -> Result<()> {
    match command {
        Command::Help => cx.answer(Command::descriptions()).await?,
        Command::Disciplina(disciplina) | Command::D(disciplina) => {
            if disciplina.is_empty() {
                let username = format!(
                    "@{}, ",
                    cx.update
                        .from()
                        .and_then(|u| u.clone().username)
                        .unwrap_or_default()
                );
                cx.answer(&format!("Certo, {}qual disciplina?", username))
                    .reply_markup(ReplyMarkup::Keyboard(KeyboardMarkup {
                        keyboard: disciplinas::list_disciplinas()
                            .await?
                            .into_iter()
                            .map(disciplina_row)
                            .collect(),
                        one_time_keyboard: Some(true),
                        selective: Some(true),
                        ..Default::default()
                    }))
                    .send()
                    .await?
            } else {
                cx.answer(disciplina::get_disciplina(&disciplina).await?)
                    .parse_mode(Html)
                    .await?
            }
        }
        Command::Horarios => {
            cx.answer(horarios::get_horarios().await?)
                .parse_mode(Html)
                .await?
        }
        Command::Agora => {
            cx.answer(agora::get_agora().await?)
                .parse_mode(Html)
                .await?
        }
        Command::Proxima => {
            cx.answer(proxima::get_proxima().await?)
                .parse_mode(Html)
                .await?
        }
        Command::Hoje => {
            cx.answer(hoje::get_horarios_hoje().await?)
                .parse_mode(Html)
                .await?
        }
    };

    Ok(())
}

async fn run() {
    teloxide::enable_logging!();

    let bot = Bot::from_env().auto_send();
    let bot_name: String = "Sistemer_Bot".into();

    teloxide::commands_repl(bot, bot_name, answer_command).await;
}
use sistemer_bot::{Disciplina, Result};

#[tokio::main]
async fn main() {
    dotenv().ok();
    run().await;
async fn main() -> Result<()> {
    print!("{}", Disciplina::buscar_disciplina("bsi020", "SCC0541").await?.info());
    Ok(())
}

D src/proxima.rs => src/proxima.rs +0 -19
@@ 1,19 0,0 @@
use crate::{disciplina, hoje};
use anyhow::Result;
use chrono::offset::Local;
use regex::Regex;

pub async fn get_proxima() -> Result<String> {
    let horarios_texto = hoje::get_horarios_hoje().await?;
    let agora = Local::now().time();

    for linha in horarios_texto.lines() {
        let (inicio, _) = crate::get_intervalo(linha)?;
        if inicio > agora {
            let nome_disciplina = Regex::new("^.*<a href=\"#(.*)\">.*$")?.replace(linha, "$1");
            log::info!("Disciplina: {:?}", nome_disciplina);
            return disciplina::get_disciplina(&nome_disciplina).await;
        }
    }
    Ok("Nenhuma aula hoje mais tarde!".into())
}