From b600a035c377b2a729cbc9fb1d1fc22bf301a4c3 Mon Sep 17 00:00:00 2001 From: Chikage Date: Fri, 13 Oct 2023 06:20:43 +0000 Subject: [PATCH] 1 --- .cargo/config.toml | 4 +- .devcontainer/devcontainer.json | 31 ++ .github/workflows/docker-image.yml | 60 +-- .vscode/settings.json | 4 +- Cargo.lock | 1 + Cargo.toml | 38 +- Dockerfile | 38 +- LICENSE | 42 +- README.md | 38 +- matrix_bot_core/src/lib.rs | 8 +- matrix_bot_core/src/matrix/client.rs | 212 +++++----- matrix_bot_core/src/matrix/e2ee.rs | 536 ++++++++++++------------- matrix_bot_core/src/matrix/mod.rs | 6 +- matrix_bot_core/src/matrix/room.rs | 236 +++++------ plugins/qbittorrent/Cargo.toml | 35 +- plugins/qbittorrent/src/lib.rs | 32 +- plugins/qbittorrent/src/qbit/binary.rs | 101 ++++- plugins/qbittorrent/src/qbit/mod.rs | 2 +- plugins/qbittorrent/src/setting.rs | 112 +++--- plugins/webhook/Cargo.toml | 36 +- plugins/webhook/src/lib.rs | 142 +++---- plugins/webhook/src/setting.rs | 104 ++--- plugins/yande_popular/Cargo.toml | 40 +- plugins/yande_popular/src/db.rs | 94 ++--- plugins/yande_popular/src/lib.rs | 182 ++++----- plugins/yande_popular/src/resize.rs | 50 +-- plugins/yande_popular/src/setting.rs | 140 +++---- plugins/yande_popular/src/yande.rs | 470 +++++++++++----------- 28 files changed, 1443 insertions(+), 1351 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.cargo/config.toml b/.cargo/config.toml index 7c6233a..a25638f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ -[target.aarch64-unknown-linux-musl] -rustflags = ["-C", "link-arg=-lc"] +[target.aarch64-unknown-linux-musl] +rustflags = ["-C", "link-arg=-lc"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f8fff10 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye" + + // Use 'mounts' to make the cargo cache persistent in a Docker Volume. + // "mounts": [ + // { + // "source": "devcontainer-cargo-cache-${devcontainerId}", + // "target": "/usr/local/cargo", + // "type": "volume" + // } + // ] + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 41eef6e..804940b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,30 +1,30 @@ -name: buildDockerImage - -on: - push: - branches: - - 'master' - -jobs: - build_matrix: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - id: docker_build - uses: docker/build-push-action@v5 - with: - push: true - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - tags: chikage/matrix_bot:latest +name: buildDockerImage + +on: + push: + branches: + - 'master' + +jobs: + build_matrix: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + id: docker_build + uses: docker/build-push-action@v5 + with: + push: true + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + tags: chikage/matrix_bot:latest diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d9636b..db699b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ -{ - "rust-analyzer.showUnlinkedFileNotification": false +{ + "rust-analyzer.showUnlinkedFileNotification": false } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 63b3441..59be3ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2400,6 +2400,7 @@ dependencies = [ "log", "matrix_bot_core", "qbit-rs", + "reqwest", "serde", "tokio", "toml 0.8.2", diff --git a/Cargo.toml b/Cargo.toml index e56bd04..736eb7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,19 @@ -[workspace] -members = [ - "matrix_bot", - "matrix_bot_core", - - # plugins - "plugins/yande_popular", - "plugins/webhook", - "plugins/qbittorrent", -] -resolver = "2" - - -[profile.release] -codegen-units = 1 -lto = true -opt-level = 'z' -panic = "abort" -strip = true +[workspace] +members = [ + "matrix_bot", + "matrix_bot_core", + + # plugins + "plugins/yande_popular", + "plugins/webhook", + "plugins/qbittorrent", +] +resolver = "2" + + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 'z' +panic = "abort" +strip = true diff --git a/Dockerfile b/Dockerfile index 6b852ad..ae9c2dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,20 @@ -FROM rust:1.72.1-alpine3.18 as builder -COPY . /app -WORKDIR /app -RUN apk add --no-cache --virtual .build-deps \ - make \ - musl-dev \ - openssl-dev \ - perl \ - pkgconfig \ - openssl-libs-static \ - && cargo build --bin matrix_bot --release - -FROM alpine:3.18 -LABEL maintainer="Chikage " \ - org.opencontainers.image.source="https://github.com/Chikage0o0/matrix_bot" -COPY --from=builder /app/target/release/matrix_bot \ - /usr/local/bin/matrix_bot -VOLUME ["/matrix_bot"] -ENV DATA_PATH=/matrix_bot +FROM rust:1.72.1-alpine3.18 as builder +COPY . /app +WORKDIR /app +RUN apk add --no-cache --virtual .build-deps \ + make \ + musl-dev \ + openssl-dev \ + perl \ + pkgconfig \ + openssl-libs-static \ + && cargo build --bin matrix_bot --release + +FROM alpine:3.18 +LABEL maintainer="Chikage " \ + org.opencontainers.image.source="https://github.com/Chikage0o0/matrix_bot" +COPY --from=builder /app/target/release/matrix_bot \ + /usr/local/bin/matrix_bot +VOLUME ["/matrix_bot"] +ENV DATA_PATH=/matrix_bot ENTRYPOINT ["/usr/local/bin/matrix_bot"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index bfc9ef4..8a0cb96 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2023 Chikage0o0 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2023 Chikage0o0 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6d813bf..c35f337 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -# Matrix Bot - -## 介绍 -利用Matrix接口,实现Webhook、Yande 图片推送到Matrix的功能,支持E2EE信息推送。 - -## 使用 -#### 命令行 -```bash -./matrix_bot -S "https://ssdfsad" -u "sdfsadf" -p "1asdfasdf" -``` -#### Docker -```bash -docker run -d --name matrix_bot \ - -e HOMESERVER_URL="https://xxx.xxx" \ - -e USERNAME="x" \ - -e PASSWORD="x" \ - -v ./matrix_bot:/matrix_bot \ - --restart unless-stopped chikage/matrix_bot:latest -``` +# Matrix Bot + +## 介绍 +利用Matrix接口,实现Webhook、Yande 图片推送到Matrix的功能,支持E2EE信息推送。 + +## 使用 +#### 命令行 +```bash +./matrix_bot -S "https://ssdfsad" -u "sdfsadf" -p "1asdfasdf" +``` +#### Docker +```bash +docker run -d --name matrix_bot \ + -e HOMESERVER_URL="https://xxx.xxx" \ + -e USERNAME="x" \ + -e PASSWORD="x" \ + -v ./matrix_bot:/matrix_bot \ + --restart unless-stopped chikage/matrix_bot:latest +``` 插件的配置文件在`/matrix_bot/plugins`目录下 \ No newline at end of file diff --git a/matrix_bot_core/src/lib.rs b/matrix_bot_core/src/lib.rs index 24b0b1b..fbe4f2a 100644 --- a/matrix_bot_core/src/lib.rs +++ b/matrix_bot_core/src/lib.rs @@ -1,4 +1,4 @@ -pub mod matrix; -pub mod sdk { - pub use matrix_sdk::config::SyncSettings; -} +pub mod matrix; +pub mod sdk { + pub use matrix_sdk::config::SyncSettings; +} diff --git a/matrix_bot_core/src/matrix/client.rs b/matrix_bot_core/src/matrix/client.rs index 73e00c7..e0ac7eb 100644 --- a/matrix_bot_core/src/matrix/client.rs +++ b/matrix_bot_core/src/matrix/client.rs @@ -1,106 +1,106 @@ -use std::{fs, ops::Deref, path::Path}; - -use anyhow::Result; -use matrix_sdk::{self, config::SyncSettings}; - -use url::Url; - -#[derive(Debug, Clone)] -pub struct Client(pub matrix_sdk::Client); - -unsafe impl Send for Client {} -unsafe impl Sync for Client {} - -impl Deref for Client { - type Target = matrix_sdk::Client; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Client { - pub async fn login( - homeserver_url: &str, - username: &str, - password: &str, - session_file: impl AsRef, - db_path: impl AsRef, - ) -> Result { - let homeserver_url = Url::parse(homeserver_url).expect("Couldn't parse the homeserver URL"); - - std::fs::create_dir_all(&db_path)?; - std::fs::create_dir_all(&session_file.as_ref().parent().unwrap())?; - - let mut client = matrix_sdk::Client::builder() - .homeserver_url(&homeserver_url) - .sled_store(&db_path, None)? - .build() - .await - .map_err(|e| { - log::error!("client build error: {}", e); - e - })?; - - if !client.logged_in() { - if session_file.as_ref().exists() - && Self::restore_login(&client, &session_file).await.is_ok() - && client.logged_in() - && client.sync_once(SyncSettings::new()).await.is_ok() - { - log::info!("Restored login from session file"); - } else { - drop(client); - // 清理数据库 - fs::remove_dir_all(&db_path)?; - client = matrix_sdk::Client::builder() - .homeserver_url(&homeserver_url) - .sled_store(db_path, None)? - .build() - .await - .map_err(|e| { - log::error!("client build error: {}", e); - e - })?; - Self::login_username(&client, &session_file, username, password).await?; - client.sync_once(SyncSettings::new()).await?; - }; - - log::info!("Logged in as {}", username); - } - - Ok(Client(client)) - } - - async fn restore_login( - client: &matrix_sdk::Client, - session_file: impl AsRef, - ) -> Result<()> { - let session = fs::read_to_string(session_file)?; - let session = serde_json::from_str(&session)?; - client.restore_login(session).await.map_err(|e| { - log::error!("restore login error: {}", e); - e - })?; - Ok(()) - } - - async fn login_username( - client: &matrix_sdk::Client, - session_file: impl AsRef, - username: &str, - password: &str, - ) -> Result<()> { - client - .login_username(username, password) - .initial_device_display_name("matrix_bot") - .send() - .await?; - let session = client.session(); - if let Some(session) = session { - let session = serde_json::to_string(&session)?; - fs::write(session_file, session)?; - }; - Ok(()) - } -} +use std::{fs, ops::Deref, path::Path}; + +use anyhow::Result; +use matrix_sdk::{self, config::SyncSettings}; + +use url::Url; + +#[derive(Debug, Clone)] +pub struct Client(pub matrix_sdk::Client); + +unsafe impl Send for Client {} +unsafe impl Sync for Client {} + +impl Deref for Client { + type Target = matrix_sdk::Client; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Client { + pub async fn login( + homeserver_url: &str, + username: &str, + password: &str, + session_file: impl AsRef, + db_path: impl AsRef, + ) -> Result { + let homeserver_url = Url::parse(homeserver_url).expect("Couldn't parse the homeserver URL"); + + std::fs::create_dir_all(&db_path)?; + std::fs::create_dir_all(&session_file.as_ref().parent().unwrap())?; + + let mut client = matrix_sdk::Client::builder() + .homeserver_url(&homeserver_url) + .sled_store(&db_path, None)? + .build() + .await + .map_err(|e| { + log::error!("client build error: {}", e); + e + })?; + + if !client.logged_in() { + if session_file.as_ref().exists() + && Self::restore_login(&client, &session_file).await.is_ok() + && client.logged_in() + && client.sync_once(SyncSettings::new()).await.is_ok() + { + log::info!("Restored login from session file"); + } else { + drop(client); + // 清理数据库 + fs::remove_dir_all(&db_path)?; + client = matrix_sdk::Client::builder() + .homeserver_url(&homeserver_url) + .sled_store(db_path, None)? + .build() + .await + .map_err(|e| { + log::error!("client build error: {}", e); + e + })?; + Self::login_username(&client, &session_file, username, password).await?; + client.sync_once(SyncSettings::new()).await?; + }; + + log::info!("Logged in as {}", username); + } + + Ok(Client(client)) + } + + async fn restore_login( + client: &matrix_sdk::Client, + session_file: impl AsRef, + ) -> Result<()> { + let session = fs::read_to_string(session_file)?; + let session = serde_json::from_str(&session)?; + client.restore_login(session).await.map_err(|e| { + log::error!("restore login error: {}", e); + e + })?; + Ok(()) + } + + async fn login_username( + client: &matrix_sdk::Client, + session_file: impl AsRef, + username: &str, + password: &str, + ) -> Result<()> { + client + .login_username(username, password) + .initial_device_display_name("matrix_bot") + .send() + .await?; + let session = client.session(); + if let Some(session) = session { + let session = serde_json::to_string(&session)?; + fs::write(session_file, session)?; + }; + Ok(()) + } +} diff --git a/matrix_bot_core/src/matrix/e2ee.rs b/matrix_bot_core/src/matrix/e2ee.rs index be94259..b1b6476 100644 --- a/matrix_bot_core/src/matrix/e2ee.rs +++ b/matrix_bot_core/src/matrix/e2ee.rs @@ -1,268 +1,268 @@ -use matrix_sdk::{ - self, - encryption::verification::{format_emojis, SasVerification, Verification}, - event_handler::EventHandlerHandle, - ruma::{ - events::{ - key::verification::{ - done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, - key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, - request::ToDeviceKeyVerificationRequestEvent, - start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, - }, - room::message::{MessageType, OriginalSyncRoomMessageEvent}, - }, - UserId, - }, -}; -use tokio::task::JoinHandle; - -use super::client::Client; - -mod web_server { - use std::{ - net::{SocketAddr, TcpListener}, - sync::OnceLock, - }; - - use axum::{extract::Path, http::StatusCode, routing::get, Router}; - use tokio::sync::broadcast; - - pub static TX: OnceLock> = OnceLock::new(); - pub static PORT: OnceLock = OnceLock::new(); - - pub async fn axum_verify_server() { - TX.get_or_init(|| { - let (tx, _) = broadcast::channel(64); - tx - }); - - let port = PORT.get_or_init(|| { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - listener.local_addr().unwrap().port() - }); - - let app: Router = Router::new().route("/verify/:code", get(verify_code)); - let addr = SocketAddr::from(([127, 0, 0, 1], *port)); - let server = axum::Server::bind(&addr) - .serve(app.into_make_service()) - .with_graceful_shutdown(async move { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C handler") - }); - - server.await.unwrap(); - } - - async fn verify_code(Path(code): Path) -> StatusCode { - let result = TX.get().unwrap().send(code); - - if result.is_err() { - return StatusCode::INTERNAL_SERVER_ERROR; - } - - StatusCode::OK - } -} - -async fn wait_for_confirmation(client: matrix_sdk::Client, sas: SasVerification) { - let emoji = sas.emoji().expect("The emoji should be available now"); - - println!("\nDo the emojis match: \n{}", format_emojis(emoji)); - - let code = uuid::Uuid::new_v4().simple().to_string(); - - println!("Please run the command to allow:"); - println!( - "local: wget -O - http://127.0.0.1:{}/verify/{}", - web_server::PORT.get().unwrap(), - code - ); - - // 最多等待 5 分钟 - let timeout = std::time::Instant::now(); - let mut rx = web_server::TX - .get_or_init(|| { - let (tx, _) = tokio::sync::broadcast::channel(64); - tx - }) - .subscribe(); - - while timeout.elapsed().as_secs() < 5 * 60 { - if let Ok(msg) = rx.try_recv() { - log::warn!("msg: {}", msg); - if msg == code { - println!("Code matches"); - sas.confirm().await.unwrap(); - - if sas.is_done() { - print_result(&sas); - print_devices(sas.other_device().user_id(), &client).await; - } - break; - } - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } -} - -fn print_result(sas: &SasVerification) { - let device = sas.other_device(); - - println!( - "Successfully verified device {} {} {:?}", - device.user_id(), - device.device_id(), - device.local_trust_state() - ); -} - -async fn print_devices(user_id: &UserId, client: &matrix_sdk::Client) { - println!("Devices of user {}", user_id); - - for device in client - .encryption() - .get_user_devices(user_id) - .await - .unwrap() - .devices() - { - println!( - " {:<10} {:<30} {:<}", - device.device_id(), - device.display_name().unwrap_or("-"), - device.is_verified() - ); - } -} - -pub fn sync(client: &Client) -> matrix_sdk::Result<(Vec, JoinHandle<()>)> { - let client = client.clone(); - let mut handlers = Vec::new(); - handlers.push(client.add_event_handler( - |ev: ToDeviceKeyVerificationRequestEvent, client: matrix_sdk::Client| async move { - let request = client - .encryption() - .get_verification_request(&ev.sender, &ev.content.transaction_id) - .await - .expect("Request object wasn't created"); - - request - .accept() - .await - .expect("Can't accept verification request"); - }, - )); - - handlers.push(client.add_event_handler( - |ev: ToDeviceKeyVerificationStartEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: ToDeviceKeyVerificationKeyEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client, sas)); - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: ToDeviceKeyVerificationDoneEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: OriginalSyncRoomMessageEvent, client: matrix_sdk::Client| async move { - if let MessageType::VerificationRequest(_) = &ev.content.msgtype { - let request = client - .encryption() - .get_verification_request(&ev.sender, &ev.event_id) - .await - .expect("Request object wasn't created"); - - request - .accept() - .await - .expect("Can't accept verification request"); - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: OriginalSyncKeyVerificationStartEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: OriginalSyncKeyVerificationKeyEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client.clone(), sas)); - } - }, - )); - - handlers.push(client.add_event_handler( - |ev: OriginalSyncKeyVerificationDoneEvent, client: matrix_sdk::Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } - } - }, - )); - - let handler = tokio::spawn(web_server::axum_verify_server()); - - Ok((handlers, handler)) -} +use matrix_sdk::{ + self, + encryption::verification::{format_emojis, SasVerification, Verification}, + event_handler::EventHandlerHandle, + ruma::{ + events::{ + key::verification::{ + done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, + key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, + request::ToDeviceKeyVerificationRequestEvent, + start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, + }, + room::message::{MessageType, OriginalSyncRoomMessageEvent}, + }, + UserId, + }, +}; +use tokio::task::JoinHandle; + +use super::client::Client; + +mod web_server { + use std::{ + net::{SocketAddr, TcpListener}, + sync::OnceLock, + }; + + use axum::{extract::Path, http::StatusCode, routing::get, Router}; + use tokio::sync::broadcast; + + pub static TX: OnceLock> = OnceLock::new(); + pub static PORT: OnceLock = OnceLock::new(); + + pub async fn axum_verify_server() { + TX.get_or_init(|| { + let (tx, _) = broadcast::channel(64); + tx + }); + + let port = PORT.get_or_init(|| { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + listener.local_addr().unwrap().port() + }); + + let app: Router = Router::new().route("/verify/:code", get(verify_code)); + let addr = SocketAddr::from(([127, 0, 0, 1], *port)); + let server = axum::Server::bind(&addr) + .serve(app.into_make_service()) + .with_graceful_shutdown(async move { + tokio::signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler") + }); + + server.await.unwrap(); + } + + async fn verify_code(Path(code): Path) -> StatusCode { + let result = TX.get().unwrap().send(code); + + if result.is_err() { + return StatusCode::INTERNAL_SERVER_ERROR; + } + + StatusCode::OK + } +} + +async fn wait_for_confirmation(client: matrix_sdk::Client, sas: SasVerification) { + let emoji = sas.emoji().expect("The emoji should be available now"); + + println!("\nDo the emojis match: \n{}", format_emojis(emoji)); + + let code = uuid::Uuid::new_v4().simple().to_string(); + + println!("Please run the command to allow:"); + println!( + "local: wget -O - http://127.0.0.1:{}/verify/{}", + web_server::PORT.get().unwrap(), + code + ); + + // 最多等待 5 分钟 + let timeout = std::time::Instant::now(); + let mut rx = web_server::TX + .get_or_init(|| { + let (tx, _) = tokio::sync::broadcast::channel(64); + tx + }) + .subscribe(); + + while timeout.elapsed().as_secs() < 5 * 60 { + if let Ok(msg) = rx.try_recv() { + log::warn!("msg: {}", msg); + if msg == code { + println!("Code matches"); + sas.confirm().await.unwrap(); + + if sas.is_done() { + print_result(&sas); + print_devices(sas.other_device().user_id(), &client).await; + } + break; + } + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } +} + +fn print_result(sas: &SasVerification) { + let device = sas.other_device(); + + println!( + "Successfully verified device {} {} {:?}", + device.user_id(), + device.device_id(), + device.local_trust_state() + ); +} + +async fn print_devices(user_id: &UserId, client: &matrix_sdk::Client) { + println!("Devices of user {}", user_id); + + for device in client + .encryption() + .get_user_devices(user_id) + .await + .unwrap() + .devices() + { + println!( + " {:<10} {:<30} {:<}", + device.device_id(), + device.display_name().unwrap_or("-"), + device.is_verified() + ); + } +} + +pub fn sync(client: &Client) -> matrix_sdk::Result<(Vec, JoinHandle<()>)> { + let client = client.clone(); + let mut handlers = Vec::new(); + handlers.push(client.add_event_handler( + |ev: ToDeviceKeyVerificationRequestEvent, client: matrix_sdk::Client| async move { + let request = client + .encryption() + .get_verification_request(&ev.sender, &ev.content.transaction_id) + .await + .expect("Request object wasn't created"); + + request + .accept() + .await + .expect("Can't accept verification request"); + }, + )); + + handlers.push(client.add_event_handler( + |ev: ToDeviceKeyVerificationStartEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.transaction_id.as_str()) + .await + { + println!( + "Starting verification with {} {}", + &sas.other_device().user_id(), + &sas.other_device().device_id() + ); + print_devices(&ev.sender, &client).await; + sas.accept().await.unwrap(); + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: ToDeviceKeyVerificationKeyEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.transaction_id.as_str()) + .await + { + tokio::spawn(wait_for_confirmation(client, sas)); + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: ToDeviceKeyVerificationDoneEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.transaction_id.as_str()) + .await + { + if sas.is_done() { + print_result(&sas); + print_devices(&ev.sender, &client).await; + } + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: OriginalSyncRoomMessageEvent, client: matrix_sdk::Client| async move { + if let MessageType::VerificationRequest(_) = &ev.content.msgtype { + let request = client + .encryption() + .get_verification_request(&ev.sender, &ev.event_id) + .await + .expect("Request object wasn't created"); + + request + .accept() + .await + .expect("Can't accept verification request"); + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: OriginalSyncKeyVerificationStartEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) + .await + { + println!( + "Starting verification with {} {}", + &sas.other_device().user_id(), + &sas.other_device().device_id() + ); + print_devices(&ev.sender, &client).await; + sas.accept().await.unwrap(); + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: OriginalSyncKeyVerificationKeyEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) + .await + { + tokio::spawn(wait_for_confirmation(client.clone(), sas)); + } + }, + )); + + handlers.push(client.add_event_handler( + |ev: OriginalSyncKeyVerificationDoneEvent, client: matrix_sdk::Client| async move { + if let Some(Verification::SasV1(sas)) = client + .encryption() + .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) + .await + { + if sas.is_done() { + print_result(&sas); + print_devices(&ev.sender, &client).await; + } + } + }, + )); + + let handler = tokio::spawn(web_server::axum_verify_server()); + + Ok((handlers, handler)) +} diff --git a/matrix_bot_core/src/matrix/mod.rs b/matrix_bot_core/src/matrix/mod.rs index 6fb3f1b..53f1c4e 100644 --- a/matrix_bot_core/src/matrix/mod.rs +++ b/matrix_bot_core/src/matrix/mod.rs @@ -1,3 +1,3 @@ -pub mod client; -pub mod e2ee; -pub mod room; +pub mod client; +pub mod e2ee; +pub mod room; diff --git a/matrix_bot_core/src/matrix/room.rs b/matrix_bot_core/src/matrix/room.rs index 5b38faa..fcd34b4 100644 --- a/matrix_bot_core/src/matrix/room.rs +++ b/matrix_bot_core/src/matrix/room.rs @@ -1,118 +1,118 @@ -use anyhow::{anyhow, Result}; -use image::GenericImageView; -use matrix_sdk::ruma::events::room::message::RoomMessageEvent; -use matrix_sdk::ruma::OwnedEventId; -use matrix_sdk::{ - attachment::AttachmentConfig, room::Joined, - ruma::events::room::message::RoomMessageEventContent, -}; -use matrix_sdk::{config::SyncSettings, Client}; -use mime_guess::mime; - -use std::{convert::TryInto, fs, path::Path}; - -#[derive(Debug, Clone)] -pub struct Room(pub Joined); - -impl Room { - pub async fn new(client: &Client, room_id: &str) -> Result { - if !client.logged_in() { - return Err(anyhow!("Not logged in")); - } - client - .sync_once(SyncSettings::new()) - .await - .map_err(|e| anyhow!("Can't sync: {}", e))?; - let room = client.get_joined_room(room_id.try_into()?); - - if let Some(room) = room { - Ok(Room(room)) - } else { - if let Some(room) = client.get_invited_room(room_id.try_into()?) { - room.accept_invitation() - .await - .map_err(|e| anyhow!("Can't accept invitation:{e}"))?; - let room = client - .get_joined_room(room_id.try_into()?) - .ok_or(anyhow!("Can't find room {}", room_id))?; - Ok(Room(room)) - } else { - Err(anyhow!("Can't find room {}", room_id)) - } - } - } - - pub async fn send_attachment(&self, file_path: impl AsRef) -> Result<()> { - let (filename, mime, file, config) = Self::prepare_send_attachment(file_path)?; - self.0 - .send_attachment(&filename, &mime, &file, config) - .await?; - Ok(()) - } - - fn prepare_send_attachment<'a>( - file_path: impl AsRef, - ) -> Result<(String, mime_guess::Mime, Vec, AttachmentConfig<'a>)> { - let file = fs::read(&file_path)?; - let filename = file_path - .as_ref() - .file_name() - .unwrap_or(std::ffi::OsStr::new("image.jpg")) - .to_str() - .unwrap_or("image.jpg"); - let mime = mime_guess::from_path(&file_path).first_or_octet_stream(); - - let config = match mime.type_() { - mime::IMAGE => { - // 从文件Bytes获取图片信息 - let image = image::load_from_memory(&file)?; - let (width, height) = image.dimensions(); - let blurhash = blurhash::encode(4, 3, width, height, image.to_rgba8().as_raw())?; - - let info = matrix_sdk::attachment::BaseImageInfo { - height: Some(height.try_into()?), - width: Some(width.try_into()?), - size: Some(file.len().try_into()?), - blurhash: Some(blurhash), - }; - - AttachmentConfig::new().info(matrix_sdk::attachment::AttachmentInfo::Image(info)) - } - _ => AttachmentConfig::default(), - }; - - Ok((filename.to_string(), mime, file, config)) - } - - pub async fn send_msg(&self, msg: &str, is_markdown: bool) -> Result<()> { - let msg = if is_markdown { - RoomMessageEventContent::text_markdown(msg) - } else { - RoomMessageEventContent::text_plain(msg) - }; - self.0.send(msg, None).await?; - - Ok(()) - } - - pub async fn send_relates_msg( - &self, - msg: &str, - event_id: &OwnedEventId, - is_markdown: bool, - ) -> Result<()> { - let msg = if is_markdown { - RoomMessageEventContent::text_markdown(msg) - } else { - RoomMessageEventContent::text_plain(msg) - }; - let timeline_event = self.0.event(event_id).await?; - let event_content = timeline_event.event.deserialize_as::()?; - let original_message = event_content.as_original().unwrap(); - let msg = msg.make_reply_to(original_message); - - self.0.send(msg, None).await?; - - Ok(()) - } -} +use anyhow::{anyhow, Result}; +use image::GenericImageView; +use matrix_sdk::ruma::events::room::message::RoomMessageEvent; +use matrix_sdk::ruma::OwnedEventId; +use matrix_sdk::{ + attachment::AttachmentConfig, room::Joined, + ruma::events::room::message::RoomMessageEventContent, +}; +use matrix_sdk::{config::SyncSettings, Client}; +use mime_guess::mime; + +use std::{convert::TryInto, fs, path::Path}; + +#[derive(Debug, Clone)] +pub struct Room(pub Joined); + +impl Room { + pub async fn new(client: &Client, room_id: &str) -> Result { + if !client.logged_in() { + return Err(anyhow!("Not logged in")); + } + client + .sync_once(SyncSettings::new()) + .await + .map_err(|e| anyhow!("Can't sync: {}", e))?; + let room = client.get_joined_room(room_id.try_into()?); + + if let Some(room) = room { + Ok(Room(room)) + } else { + if let Some(room) = client.get_invited_room(room_id.try_into()?) { + room.accept_invitation() + .await + .map_err(|e| anyhow!("Can't accept invitation:{e}"))?; + let room = client + .get_joined_room(room_id.try_into()?) + .ok_or(anyhow!("Can't find room {}", room_id))?; + Ok(Room(room)) + } else { + Err(anyhow!("Can't find room {}", room_id)) + } + } + } + + pub async fn send_attachment(&self, file_path: impl AsRef) -> Result<()> { + let (filename, mime, file, config) = Self::prepare_send_attachment(file_path)?; + self.0 + .send_attachment(&filename, &mime, &file, config) + .await?; + Ok(()) + } + + fn prepare_send_attachment<'a>( + file_path: impl AsRef, + ) -> Result<(String, mime_guess::Mime, Vec, AttachmentConfig<'a>)> { + let file = fs::read(&file_path)?; + let filename = file_path + .as_ref() + .file_name() + .unwrap_or(std::ffi::OsStr::new("image.jpg")) + .to_str() + .unwrap_or("image.jpg"); + let mime = mime_guess::from_path(&file_path).first_or_octet_stream(); + + let config = match mime.type_() { + mime::IMAGE => { + // 从文件Bytes获取图片信息 + let image = image::load_from_memory(&file)?; + let (width, height) = image.dimensions(); + let blurhash = blurhash::encode(4, 3, width, height, image.to_rgba8().as_raw())?; + + let info = matrix_sdk::attachment::BaseImageInfo { + height: Some(height.try_into()?), + width: Some(width.try_into()?), + size: Some(file.len().try_into()?), + blurhash: Some(blurhash), + }; + + AttachmentConfig::new().info(matrix_sdk::attachment::AttachmentInfo::Image(info)) + } + _ => AttachmentConfig::default(), + }; + + Ok((filename.to_string(), mime, file, config)) + } + + pub async fn send_msg(&self, msg: &str, is_markdown: bool) -> Result<()> { + let msg = if is_markdown { + RoomMessageEventContent::text_markdown(msg) + } else { + RoomMessageEventContent::text_plain(msg) + }; + self.0.send(msg, None).await?; + + Ok(()) + } + + pub async fn send_relates_msg( + &self, + msg: &str, + event_id: &OwnedEventId, + is_markdown: bool, + ) -> Result<()> { + let msg = if is_markdown { + RoomMessageEventContent::text_markdown(msg) + } else { + RoomMessageEventContent::text_plain(msg) + }; + let timeline_event = self.0.event(event_id).await?; + let event_content = timeline_event.event.deserialize_as::()?; + let original_message = event_content.as_original().unwrap(); + let msg = msg.make_reply_to(original_message); + + self.0.send(msg, None).await?; + + Ok(()) + } +} diff --git a/plugins/qbittorrent/Cargo.toml b/plugins/qbittorrent/Cargo.toml index aa5d49c..90070d6 100644 --- a/plugins/qbittorrent/Cargo.toml +++ b/plugins/qbittorrent/Cargo.toml @@ -1,17 +1,18 @@ -[package] -name = "qbittorrent" -version = "0.1.0" -edition = "2021" - - -[dependencies] -anyhow = "1" -matrix_bot_core = { path = "../../matrix_bot_core" } -log = "0.4.14" -toml = "0.8.2" -serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1.33.0", default-features = false, features = [] } -qbit-rs = { version = "0.3.7" } - -[dev-dependencies] -env_logger = "0.10.0" +[package] +name = "qbittorrent" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = "1" +matrix_bot_core = { path = "../../matrix_bot_core" } +log = "0.4.14" +toml = "0.8.2" +serde = { version = "1.0.188", features = ["derive"] } +tokio = { version = "1.33.0", default-features = false, features = [] } +qbit-rs = { version = "0.3.7" } +reqwest = { version = "0.11.6", features = ["blocking"] } + +[dev-dependencies] +env_logger = "0.10.0" diff --git a/plugins/qbittorrent/src/lib.rs b/plugins/qbittorrent/src/lib.rs index 19b4797..d90116c 100644 --- a/plugins/qbittorrent/src/lib.rs +++ b/plugins/qbittorrent/src/lib.rs @@ -1,16 +1,16 @@ -use anyhow::Result; -use matrix_bot_core::matrix::client::Client; - -mod qbit; -mod setting; - -#[allow(unused_variables)] -pub async fn run(client: Client, plugin_folder: impl AsRef) -> Result<()> { - log::info!("start yande_popular"); - - let setting = setting::get_or_init(plugin_folder)?; - - if setting.use_internal_qbit {} - - Ok(()) -} +use anyhow::Result; +use matrix_bot_core::matrix::client::Client; + +mod qbit; +mod setting; + +#[allow(unused_variables)] +pub async fn run(client: Client, plugin_folder: impl AsRef) -> Result<()> { + log::info!("start yande_popular"); + + let setting = setting::get_or_init(plugin_folder)?; + + if setting.use_internal_qbit {} + + Ok(()) +} diff --git a/plugins/qbittorrent/src/qbit/binary.rs b/plugins/qbittorrent/src/qbit/binary.rs index ed92c17..0d57ccc 100644 --- a/plugins/qbittorrent/src/qbit/binary.rs +++ b/plugins/qbittorrent/src/qbit/binary.rs @@ -1,21 +1,80 @@ -use anyhow::Result; - -#[allow(dead_code)] -fn get_download_link() -> Result { - if !(cfg!(target_os = "linux")) { - return Err(anyhow::anyhow!("only support linux")); - } - - let mut link = - "https://github.com/userdocs/qbittorrent-nox-static/releases/latest/download/".to_string(); - if cfg!(target_arch = "x86_64") { - link.push_str("x86_64-qbittorrent-nox"); - } else if cfg!(target_arch = "aarch64") { - link.push_str("aarch64-qbittorrent-nox"); - } else if cfg!(target_arch = "x86") { - link.push_str("x86-qbittorrent-nox"); - } else { - return Err(anyhow::anyhow!("only support x86_64 and aarch64")); - } - Ok(link) -} +use anyhow::Result; + +use std::{path::Path, process::Child}; + +#[allow(dead_code)] +fn get_download_link() -> Result { + if !(cfg!(target_os = "linux")) { + return Err(anyhow::anyhow!("only support linux")); + } + + let mut link = + "https://github.com/userdocs/qbittorrent-nox-static/releases/latest/download/".to_string(); + if cfg!(target_arch = "x86_64") { + link.push_str("x86_64-qbittorrent-nox"); + } else if cfg!(target_arch = "aarch64") { + link.push_str("aarch64-qbittorrent-nox"); + } else if cfg!(target_arch = "x86") { + link.push_str("x86-qbittorrent-nox"); + } else { + return Err(anyhow::anyhow!("only support x86_64 and aarch64")); + } + Ok(link) +} + +fn download_binary(path: impl AsRef) -> Result<()> { + let link = get_download_link()?; + let resp = reqwest::blocking::get(&link)?; + let binary = resp.bytes()?; + std::fs::create_dir_all(path.as_ref().parent().unwrap())?; + std::fs::write(path, binary)?; + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(path, Permissions::from_mode(0o755))?; + } + Ok(()) +} + +pub fn run(runtime_folder: impl AsRef, port: u16) -> Result { + let binary_path = runtime_folder.as_ref().join("qbittorrent-nox"); + if !binary_path.exists() { + download_binary(&binary_path)?; + } + + let qbittorrent_folder = runtime_folder.as_ref().join("config"); + let download_folder = runtime_folder.as_ref().join("download"); + if !download_folder.exists() { + std::fs::create_dir_all(&download_folder)?; + } + + let qbitconfig = qbittorrent_folder + .join("qBittorrent") + .join("config") + .join("qBittorrent.conf"); + if !qbitconfig.exists() { + std::fs::create_dir_all(qbitconfig.parent().unwrap())?; + std::fs::write( + &qbitconfig, + format!( + r#"[LegalNotice] + Accepted=true + + [BitTorrent] + Session\DefaultSavePath={} + "#, + download_folder.to_string_lossy() + ), + )?; + } + + let mut cmd = std::process::Command::new(binary_path); + cmd.arg(format!("--webui-port={}", port)).arg(format!( + "--profile={}", + qbittorrent_folder.to_string_lossy() + )); + + let child = cmd.spawn()?; + + Ok(child) +} diff --git a/plugins/qbittorrent/src/qbit/mod.rs b/plugins/qbittorrent/src/qbit/mod.rs index 96eab66..4150e4d 100644 --- a/plugins/qbittorrent/src/qbit/mod.rs +++ b/plugins/qbittorrent/src/qbit/mod.rs @@ -1 +1 @@ -pub mod binary; +pub mod binary; diff --git a/plugins/qbittorrent/src/setting.rs b/plugins/qbittorrent/src/setting.rs index c9839ff..0cf1e97 100644 --- a/plugins/qbittorrent/src/setting.rs +++ b/plugins/qbittorrent/src/setting.rs @@ -1,56 +1,56 @@ -use std::path::{Path, PathBuf}; - -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Setting { - pub room_setting: Vec, - pub qbit_user: String, - pub qbit_pass: String, - pub qbit_url: String, - pub use_internal_qbit: bool, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct RoomSetting { - pub download_path: PathBuf, - pub file_size_limit: u128, - pub room_id: String, - pub db_path: PathBuf, -} - -pub fn get_or_init(path: impl AsRef) -> Result { - let setting_path = path.as_ref().join("qbittorrent.toml"); - // load setting, if not exists, create it and exit - let setting = if !setting_path.exists() { - log::info!("create setting file: {}", setting_path.to_string_lossy()); - let setting = Setting { - room_setting: vec![RoomSetting { - download_path: path.as_ref().join("qbittorrent").join("download"), - file_size_limit: 0, - room_id: "".to_string(), - db_path: path.as_ref().join("qbittorrent").join("db"), - }], - qbit_user: "admin".to_string(), - qbit_pass: "adminadmin".to_string(), - qbit_url: "http://127.0.0.1:8080".to_string(), - use_internal_qbit: true, - }; - let toml = toml::to_string_pretty(&setting).unwrap(); - std::fs::write(&setting_path, toml)?; - log::error!( - "please edit setting file: {}", - setting_path.to_string_lossy() - ); - return Err(anyhow::anyhow!( - "please edit setting file: {}", - setting_path.to_string_lossy() - )); - } else { - log::info!("load setting file: {}", setting_path.to_string_lossy()); - let toml = std::fs::read_to_string(&setting_path)?; - toml::from_str(&toml)? - }; - Ok(setting) -} +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Setting { + pub room_setting: Vec, + pub qbit_user: String, + pub qbit_pass: String, + pub qbit_url: String, + pub use_internal_qbit: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RoomSetting { + pub download_path: PathBuf, + pub file_size_limit: u128, + pub room_id: String, + pub db_path: PathBuf, +} + +pub fn get_or_init(path: impl AsRef) -> Result { + let setting_path = path.as_ref().join("qbittorrent.toml"); + // load setting, if not exists, create it and exit + let setting = if !setting_path.exists() { + log::info!("create setting file: {}", setting_path.to_string_lossy()); + let setting = Setting { + room_setting: vec![RoomSetting { + download_path: path.as_ref().join("qbittorrent").join("download"), + file_size_limit: 0, + room_id: "".to_string(), + db_path: path.as_ref().join("qbittorrent").join("db"), + }], + qbit_user: "admin".to_string(), + qbit_pass: "adminadmin".to_string(), + qbit_url: "http://127.0.0.1:8080".to_string(), + use_internal_qbit: true, + }; + let toml = toml::to_string_pretty(&setting).unwrap(); + std::fs::write(&setting_path, toml)?; + log::error!( + "please edit setting file: {}", + setting_path.to_string_lossy() + ); + return Err(anyhow::anyhow!( + "please edit setting file: {}", + setting_path.to_string_lossy() + )); + } else { + log::info!("load setting file: {}", setting_path.to_string_lossy()); + let toml = std::fs::read_to_string(&setting_path)?; + toml::from_str(&toml)? + }; + Ok(setting) +} diff --git a/plugins/webhook/Cargo.toml b/plugins/webhook/Cargo.toml index 29fde19..650747b 100644 --- a/plugins/webhook/Cargo.toml +++ b/plugins/webhook/Cargo.toml @@ -1,18 +1,18 @@ -[package] -name = "webhook" -version = "0.1.0" -edition = "2021" - - -[dependencies] -anyhow = "1" -matrix_bot_core = { path = "../../matrix_bot_core" } -log = "0.4.14" -toml = "0.8.2" -serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1.33.0", default-features = false, features = [] } -axum = "0.6.20" -tower-http = { version = "0.4.4", features = ["auth"] } - -[dev-dependencies] -env_logger = "0.10.0" +[package] +name = "webhook" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = "1" +matrix_bot_core = { path = "../../matrix_bot_core" } +log = "0.4.14" +toml = "0.8.2" +serde = { version = "1.0.188", features = ["derive"] } +tokio = { version = "1.33.0", default-features = false, features = [] } +axum = "0.6.20" +tower-http = { version = "0.4.4", features = ["auth"] } + +[dev-dependencies] +env_logger = "0.10.0" diff --git a/plugins/webhook/src/lib.rs b/plugins/webhook/src/lib.rs index 000358c..321ff4e 100644 --- a/plugins/webhook/src/lib.rs +++ b/plugins/webhook/src/lib.rs @@ -1,71 +1,71 @@ -use std::{collections::HashMap, net::SocketAddr}; - -use anyhow::Result; -use axum::{ - extract::{Path, State}, - http::StatusCode, - routing::post, - Json, Router, -}; -use matrix_bot_core::matrix::{client::Client, room::Room}; - -use crate::setting::Setting; -mod setting; - -pub async fn run(client: Client, setting_folder: impl AsRef) -> Result<()> { - log::info!("start webhook"); - - let setting = Setting::get_or_init(setting_folder)?; - let token = setting.token.clone(); - let port = setting.port; - let setting = setting.to_hashmap(&client).await?; - - let mut app = Router::new() - .route("/send/:room_id", post(send)) - .fallback(not_found) - .with_state(setting); - - if let Some(token) = &token { - app = app.layer(tower_http::validate_request::ValidateRequestHeaderLayer::bearer(token)); - } - - log::info!("listen on {}", port); - - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - let server = axum::Server::bind(&addr).serve(app.into_make_service()); - - server.await.unwrap(); - - Ok(()) -} - -#[derive(Debug, serde::Deserialize)] -struct Msg { - msg: String, -} -async fn send( - State(room): State>, - Path(room_id): Path, - Json(msg): Json, -) -> StatusCode { - if let Some(room) = room.get(&room_id) { - log::info!("send msg: {}", msg.msg); - match room.send_msg(&msg.msg, true).await { - Ok(_) => { - log::info!("send msg success"); - StatusCode::OK - } - Err(e) => { - log::error!("send msg failed: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - } - } - } else { - log::error!("room not found: {}", room_id); - StatusCode::NOT_FOUND - } -} - -async fn not_found() -> StatusCode { - StatusCode::NOT_FOUND -} +use std::{collections::HashMap, net::SocketAddr}; + +use anyhow::Result; +use axum::{ + extract::{Path, State}, + http::StatusCode, + routing::post, + Json, Router, +}; +use matrix_bot_core::matrix::{client::Client, room::Room}; + +use crate::setting::Setting; +mod setting; + +pub async fn run(client: Client, setting_folder: impl AsRef) -> Result<()> { + log::info!("start webhook"); + + let setting = Setting::get_or_init(setting_folder)?; + let token = setting.token.clone(); + let port = setting.port; + let setting = setting.to_hashmap(&client).await?; + + let mut app = Router::new() + .route("/send/:room_id", post(send)) + .fallback(not_found) + .with_state(setting); + + if let Some(token) = &token { + app = app.layer(tower_http::validate_request::ValidateRequestHeaderLayer::bearer(token)); + } + + log::info!("listen on {}", port); + + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + let server = axum::Server::bind(&addr).serve(app.into_make_service()); + + server.await.unwrap(); + + Ok(()) +} + +#[derive(Debug, serde::Deserialize)] +struct Msg { + msg: String, +} +async fn send( + State(room): State>, + Path(room_id): Path, + Json(msg): Json, +) -> StatusCode { + if let Some(room) = room.get(&room_id) { + log::info!("send msg: {}", msg.msg); + match room.send_msg(&msg.msg, true).await { + Ok(_) => { + log::info!("send msg success"); + StatusCode::OK + } + Err(e) => { + log::error!("send msg failed: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + } + } + } else { + log::error!("room not found: {}", room_id); + StatusCode::NOT_FOUND + } +} + +async fn not_found() -> StatusCode { + StatusCode::NOT_FOUND +} diff --git a/plugins/webhook/src/setting.rs b/plugins/webhook/src/setting.rs index 051e058..82d1878 100644 --- a/plugins/webhook/src/setting.rs +++ b/plugins/webhook/src/setting.rs @@ -1,52 +1,52 @@ -use std::{collections::HashMap, path::Path}; - -use anyhow::Result; -use matrix_bot_core::matrix::{client::Client, room::Room}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Setting { - pub room_id: Vec, - pub token: Option, - pub port: u16, -} - -impl Setting { - pub async fn to_hashmap(self, client: &Client) -> Result> { - let mut hashmap = HashMap::new(); - for room_id in self.room_id { - let room = Room::new(&client, &room_id).await?; - hashmap.insert(room_id, room); - } - Ok(hashmap) - } - - pub fn get_or_init(path: impl AsRef) -> Result { - let setting_path = path.as_ref().join("webhook.toml"); - - // load setting, if not exists, create it and exit - let setting: Setting = if !setting_path.exists() { - log::info!("create setting file: {}", setting_path.to_string_lossy()); - let settings = Setting { - room_id: vec!["".to_string()], - token: Some("123456".to_string()), - port: 0, - }; - let toml = toml::to_string_pretty(&settings).unwrap(); - std::fs::write(&setting_path, toml)?; - log::error!( - "please edit setting file: {}", - setting_path.to_string_lossy() - ); - return Err(anyhow::anyhow!( - "please edit setting file: {}", - setting_path.to_string_lossy() - )); - } else { - log::info!("load setting file: {}", setting_path.to_string_lossy()); - let toml = std::fs::read_to_string(&setting_path)?; - toml::from_str(&toml)? - }; - Ok(setting) - } -} +use std::{collections::HashMap, path::Path}; + +use anyhow::Result; +use matrix_bot_core::matrix::{client::Client, room::Room}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Setting { + pub room_id: Vec, + pub token: Option, + pub port: u16, +} + +impl Setting { + pub async fn to_hashmap(self, client: &Client) -> Result> { + let mut hashmap = HashMap::new(); + for room_id in self.room_id { + let room = Room::new(&client, &room_id).await?; + hashmap.insert(room_id, room); + } + Ok(hashmap) + } + + pub fn get_or_init(path: impl AsRef) -> Result { + let setting_path = path.as_ref().join("webhook.toml"); + + // load setting, if not exists, create it and exit + let setting: Setting = if !setting_path.exists() { + log::info!("create setting file: {}", setting_path.to_string_lossy()); + let settings = Setting { + room_id: vec!["".to_string()], + token: Some("123456".to_string()), + port: 0, + }; + let toml = toml::to_string_pretty(&settings).unwrap(); + std::fs::write(&setting_path, toml)?; + log::error!( + "please edit setting file: {}", + setting_path.to_string_lossy() + ); + return Err(anyhow::anyhow!( + "please edit setting file: {}", + setting_path.to_string_lossy() + )); + } else { + log::info!("load setting file: {}", setting_path.to_string_lossy()); + let toml = std::fs::read_to_string(&setting_path)?; + toml::from_str(&toml)? + }; + Ok(setting) + } +} diff --git a/plugins/yande_popular/Cargo.toml b/plugins/yande_popular/Cargo.toml index 2bbfcf0..61f96ba 100644 --- a/plugins/yande_popular/Cargo.toml +++ b/plugins/yande_popular/Cargo.toml @@ -1,20 +1,20 @@ -[package] -name = "yande_popular" -version = "0.1.0" -edition = "2021" - - -[dependencies] -anyhow = "1" -matrix_bot_core = { path = "../../matrix_bot_core" } -image_compressor = { git = "https://github.com/Chikage0o0/image_compressor/", branch = "main" } -reqwest = { version = "0.11.6" } -log = "0.4.14" -select = "0.6.0" -sled = { version = "0.34.7" } -toml = "0.8.2" -serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1.33.0", default-features = false, features = [] } - -[dev-dependencies] -env_logger = "0.10.0" +[package] +name = "yande_popular" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = "1" +matrix_bot_core = { path = "../../matrix_bot_core" } +image_compressor = { git = "https://github.com/Chikage0o0/image_compressor/", branch = "main" } +reqwest = { version = "0.11.6" } +log = "0.4.14" +select = "0.6.0" +sled = { version = "0.34.7" } +toml = "0.8.2" +serde = { version = "1.0.188", features = ["derive"] } +tokio = { version = "1.33.0", default-features = false, features = [] } + +[dev-dependencies] +env_logger = "0.10.0" diff --git a/plugins/yande_popular/src/db.rs b/plugins/yande_popular/src/db.rs index 42dc533..41da908 100644 --- a/plugins/yande_popular/src/db.rs +++ b/plugins/yande_popular/src/db.rs @@ -1,47 +1,47 @@ -use std::path::Path; - -use sled::Db; - -#[derive(Debug)] -pub struct DB(Db); - -impl DB { - pub fn open(path: impl AsRef) -> Self { - let db = sled::open(path).unwrap(); - DB(db) - } - - pub fn insert(&self, key: &str) -> sled::Result<()> { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - self.0.insert(key, ×tamp.to_be_bytes())?; - Ok(()) - } - - pub fn contains(&self, key: &str) -> sled::Result { - self.0.contains_key(key) - } - - pub fn auto_remove(&self) -> sled::Result<()> { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - let mut keys = Vec::new(); - for key in self.0.iter().keys() { - let key = key?; - let value = self.0.get(&key)?.unwrap(); - let value = u64::from_be_bytes(value.as_ref().try_into().unwrap()); - if timestamp - value > 60 * 60 * 24 * 30 { - keys.push(key); - } - } - for key in keys { - self.0.remove(key)?; - } - self.0.flush()?; - Ok(()) - } -} +use std::path::Path; + +use sled::Db; + +#[derive(Debug)] +pub struct DB(Db); + +impl DB { + pub fn open(path: impl AsRef) -> Self { + let db = sled::open(path).unwrap(); + DB(db) + } + + pub fn insert(&self, key: &str) -> sled::Result<()> { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + self.0.insert(key, ×tamp.to_be_bytes())?; + Ok(()) + } + + pub fn contains(&self, key: &str) -> sled::Result { + self.0.contains_key(key) + } + + pub fn auto_remove(&self) -> sled::Result<()> { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let mut keys = Vec::new(); + for key in self.0.iter().keys() { + let key = key?; + let value = self.0.get(&key)?.unwrap(); + let value = u64::from_be_bytes(value.as_ref().try_into().unwrap()); + if timestamp - value > 60 * 60 * 24 * 30 { + keys.push(key); + } + } + for key in keys { + self.0.remove(key)?; + } + self.0.flush()?; + Ok(()) + } +} diff --git a/plugins/yande_popular/src/lib.rs b/plugins/yande_popular/src/lib.rs index 3ea9e56..cfc182c 100644 --- a/plugins/yande_popular/src/lib.rs +++ b/plugins/yande_popular/src/lib.rs @@ -1,91 +1,91 @@ -use std::{collections::HashMap, path::Path}; - -use anyhow::Result; -use db::DB; -use matrix_bot_core::matrix::{client::Client, room::Room}; -use setting::RoomSetting; - -use crate::setting::Setting; - -mod db; -mod resize; -mod setting; -mod yande; - -pub async fn run(client: Client, plugin_folder: impl AsRef) -> Result<()> { - log::info!("start yande_popular"); - - let setting_hashmap = Setting::get_or_init(plugin_folder)? - .to_hashmap(&client) - .await?; - - loop { - log::info!("start scan"); - sync(&setting_hashmap).await.unwrap_or_else(|e| { - log::error!("scan failed: {}", e); - }); - tokio::time::sleep(tokio::time::Duration::from_secs(60 * 60)).await; - } -} - -pub async fn sync(setting_hashmap: &HashMap) -> Result<()> { - for (setting, (db, room)) in setting_hashmap.iter() { - log::info!("scan: {}", setting.room_id); - let mut image_list = Vec::new(); - - for url in setting.yande_url.iter() { - let list = yande::get_image_list(url).await?; - image_list.extend(list); - } - - let download_list = yande::get_download_list(&image_list, &db).await?; - - for (id, img_data) in download_list { - for (id, url) in img_data.url.iter() { - log::info!("prepare download: {}", id); - let path = match yande::download_img(*id, url, &setting.tmp_path).await { - Ok(path) => path, - Err(e) => { - log::error!("download failed: {}", e); - return Err(e.into()); - } - }; - - let path = if let Some(size) = setting.resize { - match resize::resize_and_compress(&path, size) { - Ok(path) => path, - Err(e) => { - log::error!("resize {id} failed: {}", e); - return Err(e.into()); - } - } - } else { - path - }; - - log::info!("upload: {}", id); - - match room.send_attachment(&path).await { - Ok(_) => { - log::info!("upload: {} done", id); - db.insert(&id.to_string())?; - } - Err(e) => { - log::error!("upload failed: {}", e); - return Err(e.into()); - } - } - - std::fs::remove_file(&path).unwrap_or_else(|e| { - log::error!("remove file failed: {}", e); - }); - } - let msg = - format!("来源:[https://yande.re/post/show/{id}](https://yande.re/post/show/{id})"); - room.send_msg(&msg, true).await?; - } - log::info!("scan: {} done", setting.room_id); - db.auto_remove()?; - } - Ok(()) -} +use std::{collections::HashMap, path::Path}; + +use anyhow::Result; +use db::DB; +use matrix_bot_core::matrix::{client::Client, room::Room}; +use setting::RoomSetting; + +use crate::setting::Setting; + +mod db; +mod resize; +mod setting; +mod yande; + +pub async fn run(client: Client, plugin_folder: impl AsRef) -> Result<()> { + log::info!("start yande_popular"); + + let setting_hashmap = Setting::get_or_init(plugin_folder)? + .to_hashmap(&client) + .await?; + + loop { + log::info!("start scan"); + sync(&setting_hashmap).await.unwrap_or_else(|e| { + log::error!("scan failed: {}", e); + }); + tokio::time::sleep(tokio::time::Duration::from_secs(60 * 60)).await; + } +} + +pub async fn sync(setting_hashmap: &HashMap) -> Result<()> { + for (setting, (db, room)) in setting_hashmap.iter() { + log::info!("scan: {}", setting.room_id); + let mut image_list = Vec::new(); + + for url in setting.yande_url.iter() { + let list = yande::get_image_list(url).await?; + image_list.extend(list); + } + + let download_list = yande::get_download_list(&image_list, &db).await?; + + for (id, img_data) in download_list { + for (id, url) in img_data.url.iter() { + log::info!("prepare download: {}", id); + let path = match yande::download_img(*id, url, &setting.tmp_path).await { + Ok(path) => path, + Err(e) => { + log::error!("download failed: {}", e); + return Err(e.into()); + } + }; + + let path = if let Some(size) = setting.resize { + match resize::resize_and_compress(&path, size) { + Ok(path) => path, + Err(e) => { + log::error!("resize {id} failed: {}", e); + return Err(e.into()); + } + } + } else { + path + }; + + log::info!("upload: {}", id); + + match room.send_attachment(&path).await { + Ok(_) => { + log::info!("upload: {} done", id); + db.insert(&id.to_string())?; + } + Err(e) => { + log::error!("upload failed: {}", e); + return Err(e.into()); + } + } + + std::fs::remove_file(&path).unwrap_or_else(|e| { + log::error!("remove file failed: {}", e); + }); + } + let msg = + format!("来源:[https://yande.re/post/show/{id}](https://yande.re/post/show/{id})"); + room.send_msg(&msg, true).await?; + } + log::info!("scan: {} done", setting.room_id); + db.auto_remove()?; + } + Ok(()) +} diff --git a/plugins/yande_popular/src/resize.rs b/plugins/yande_popular/src/resize.rs index bf6f959..34ede12 100644 --- a/plugins/yande_popular/src/resize.rs +++ b/plugins/yande_popular/src/resize.rs @@ -1,25 +1,25 @@ -use std::path::{Path, PathBuf}; - -use anyhow::Result; -use image_compressor::{ - compressor::{Compressor, ResizeType}, - Factor, -}; - -pub fn resize_and_compress(path: &Path, size: usize) -> Result { - let source = path; - let dest = path.parent().unwrap_or(Path::new(".")); - std::fs::create_dir_all(dest)?; - let mut comp = Compressor::new(source, &dest); - comp.set_factor(Factor::new_with_resize_type( - 85.0, - ResizeType::LongestSidePixels(size), - )); - comp.set_delete_source(false); - comp.set_overwrite_dest(true); - let path = comp.compress_to_jpg().unwrap_or_else(|e| { - log::error!("compress failed: {}", e); - source.to_path_buf() - }); - Ok(path) -} +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use image_compressor::{ + compressor::{Compressor, ResizeType}, + Factor, +}; + +pub fn resize_and_compress(path: &Path, size: usize) -> Result { + let source = path; + let dest = path.parent().unwrap_or(Path::new(".")); + std::fs::create_dir_all(dest)?; + let mut comp = Compressor::new(source, &dest); + comp.set_factor(Factor::new_with_resize_type( + 85.0, + ResizeType::LongestSidePixels(size), + )); + comp.set_delete_source(false); + comp.set_overwrite_dest(true); + let path = comp.compress_to_jpg().unwrap_or_else(|e| { + log::error!("compress failed: {}", e); + source.to_path_buf() + }); + Ok(path) +} diff --git a/plugins/yande_popular/src/setting.rs b/plugins/yande_popular/src/setting.rs index 16b42db..27d0d4a 100644 --- a/plugins/yande_popular/src/setting.rs +++ b/plugins/yande_popular/src/setting.rs @@ -1,70 +1,70 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -use anyhow::Result; -use matrix_bot_core::matrix::{client::Client, room::Room}; -use serde::{Deserialize, Serialize}; - -use crate::db::DB; - -#[derive(Debug, Deserialize, Serialize, Clone, Hash, Eq, PartialEq)] -pub struct RoomSetting { - pub tmp_path: PathBuf, - pub db_path: PathBuf, - pub room_id: String, - pub resize: Option, - pub yande_url: Vec, -} -#[derive(Debug, Deserialize, Serialize)] -pub struct Setting { - room: Vec, -} - -impl Setting { - pub async fn to_hashmap(self, client: &Client) -> Result> { - let mut hashmap = HashMap::new(); - for setting in self.room { - let db = DB::open(&setting.db_path); - std::fs::create_dir_all(Path::new(&setting.tmp_path)).unwrap_or_else(|e| { - log::error!("create tmp dir failed: {}", e); - }); - let room = Room::new(&client, &setting.room_id).await?; - hashmap.insert(setting.clone(), (db, room)); - } - Ok(hashmap) - } - - pub fn get_or_init(path: impl AsRef) -> Result { - let setting_path = path.as_ref().join("yande_popular.toml"); - // load setting, if not exists, create it and exit - let settings: Self = if !setting_path.exists() { - log::info!("create setting file: {}", setting_path.to_string_lossy()); - let settings = Setting { - room: vec![RoomSetting { - tmp_path: path.as_ref().join("yande_popular").join("tmp"), - db_path: path.as_ref().join("yande_popular").join("db"), - room_id: "".to_string(), - resize: Some(1920), - yande_url: vec!["https://yande.re/post/popular_recent".to_string()], - }], - }; - let toml = toml::to_string_pretty(&settings).unwrap(); - std::fs::write(&setting_path, toml)?; - log::error!( - "please edit setting file: {}", - setting_path.to_string_lossy() - ); - return Err(anyhow::anyhow!( - "please edit setting file: {}", - setting_path.to_string_lossy() - )); - } else { - log::info!("load setting file: {}", setting_path.to_string_lossy()); - let toml = std::fs::read_to_string(&setting_path)?; - toml::from_str(&toml)? - }; - Ok(settings) - } -} +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use matrix_bot_core::matrix::{client::Client, room::Room}; +use serde::{Deserialize, Serialize}; + +use crate::db::DB; + +#[derive(Debug, Deserialize, Serialize, Clone, Hash, Eq, PartialEq)] +pub struct RoomSetting { + pub tmp_path: PathBuf, + pub db_path: PathBuf, + pub room_id: String, + pub resize: Option, + pub yande_url: Vec, +} +#[derive(Debug, Deserialize, Serialize)] +pub struct Setting { + room: Vec, +} + +impl Setting { + pub async fn to_hashmap(self, client: &Client) -> Result> { + let mut hashmap = HashMap::new(); + for setting in self.room { + let db = DB::open(&setting.db_path); + std::fs::create_dir_all(Path::new(&setting.tmp_path)).unwrap_or_else(|e| { + log::error!("create tmp dir failed: {}", e); + }); + let room = Room::new(&client, &setting.room_id).await?; + hashmap.insert(setting.clone(), (db, room)); + } + Ok(hashmap) + } + + pub fn get_or_init(path: impl AsRef) -> Result { + let setting_path = path.as_ref().join("yande_popular.toml"); + // load setting, if not exists, create it and exit + let settings: Self = if !setting_path.exists() { + log::info!("create setting file: {}", setting_path.to_string_lossy()); + let settings = Setting { + room: vec![RoomSetting { + tmp_path: path.as_ref().join("yande_popular").join("tmp"), + db_path: path.as_ref().join("yande_popular").join("db"), + room_id: "".to_string(), + resize: Some(1920), + yande_url: vec!["https://yande.re/post/popular_recent".to_string()], + }], + }; + let toml = toml::to_string_pretty(&settings).unwrap(); + std::fs::write(&setting_path, toml)?; + log::error!( + "please edit setting file: {}", + setting_path.to_string_lossy() + ); + return Err(anyhow::anyhow!( + "please edit setting file: {}", + setting_path.to_string_lossy() + )); + } else { + log::info!("load setting file: {}", setting_path.to_string_lossy()); + let toml = std::fs::read_to_string(&setting_path)?; + toml::from_str(&toml)? + }; + Ok(settings) + } +} diff --git a/plugins/yande_popular/src/yande.rs b/plugins/yande_popular/src/yande.rs index dace1ee..c1c631b 100644 --- a/plugins/yande_popular/src/yande.rs +++ b/plugins/yande_popular/src/yande.rs @@ -1,235 +1,235 @@ -use std::{ - collections::{HashMap, VecDeque}, - io::Write, - path::{Path, PathBuf}, - sync::OnceLock, -}; - -use anyhow::Result; -use reqwest::{header, Client, ClientBuilder}; -use select::{ - document::{self, Document}, - predicate::{Attr, Class, Name}, -}; - -use crate::db::DB; - -pub static CLIENT: OnceLock = OnceLock::new(); - -type ImgInfo = HashMap; -#[derive(Debug, Clone)] -pub struct ImgData { - pub score: u64, - pub url: VecDeque<(i64, String)>, -} - -fn client_builder() -> Client { - let mut headers = header::HeaderMap::new(); - - headers.insert( - header::USER_AGENT, - header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.47"), - ); - - ClientBuilder::new() - .default_headers(headers) - .build() - .unwrap() -} - -async fn get(url: &str) -> Result { - let client = CLIENT.get_or_init(client_builder); - let resp = client.get(url).send().await?.text().await?; - Ok(resp) -} - -pub async fn get_image_list(url: &str) -> Result> { - let html = get(url).await?; - let document = document::Document::from(html.as_str()); - - let list = document - .find(Name("ul")) - .find(|node| node.attr("id") == Some("post-list-posts")) - .ok_or(anyhow::anyhow!("not found ul#post-list-posts"))?; - - let mut image_list = Vec::new(); - - for node in list.children().filter(|node| node.is(Name("li"))) { - let id = node.attr("id").ok_or(anyhow::anyhow!("not found id"))?; - - let id = id.trim().replace('p', "").parse::()?; - image_list.push(id); - } - - Ok(image_list) -} - -async fn find_parent(id: i64) -> Result<(i64, Document)> { - let url = format!("https://yande.re/post/show/{}", id); - let html = get(&url).await?; - - let document = document::Document::from(html.as_str()); - - let result = match document.find(Class("status-notice")).find(|node| { - node.children() - .any(|node| node.is(Name("a")) && node.text().contains("parent post")) - }) { - Some(node) => { - let id = node - .find(Name("a")) - .find(|node| { - node.attr("href") - .is_some_and(|href| href.starts_with("/post/show/")) - }) - .ok_or(anyhow::anyhow!("not found parent post"))? - .attr("href") - .ok_or(anyhow::anyhow!("not found href"))? - .trim() - .replace("/post/show/", "") - .parse::()?; - let url = format!("https://yande.re/post/show/{}", id); - let html = get(&url).await?; - - (id, document::Document::from(html.as_str())) - } - None => (id, document), - }; - - Ok(result) -} - -pub async fn get_image_info(id: i64) -> Result<(i64, ImgData)> { - let (id, document) = find_parent(id).await?; - - let mut download_link: VecDeque<(i64, String)> = VecDeque::new(); - - let mut score = find_score(id, &document)?; - - download_link.push_back((id, find_raw_url(&document)?)); - - let child = document.find(Class("status-notice")).find(|node| { - node.children() - .any(|node| node.is(Name("a")) && node.text().contains("child post")) - }); - - if let Some(node) = child { - for node in node.children().filter(|node| { - node.is(Name("a")) - && node - .attr("href") - .is_some_and(|href| href.starts_with("/post/show/")) - }) { - let id = node.text().trim().parse::()?; - - let html = get(&format!("https://yande.re/post/show/{}", id)).await?; - let document = document::Document::from(html.as_str()); - - let score_child = find_score(id, &document)?; - if score_child > score { - score = score_child; - } - - download_link.push_back((id, find_raw_url(&document)?)); - } - } - - Ok(( - id, - ImgData { - score, - url: download_link, - }, - )) -} - -fn find_score(id: i64, document: &Document) -> Result { - let score = document - .find(Attr("id", format!("post-score-{}", id).as_str())) - .find(|node| node.is(Name("span"))) - .ok_or(anyhow::anyhow!("not found span#post-score-{id}"))? - .text() - .trim() - .parse::()?; - Ok(score) -} - -fn find_raw_url(document: &Document) -> Result { - let url = document - .find(Name("a")) - .find(|node| node.attr("id") == Some("highres")) - .ok_or(anyhow::anyhow!("not found a#highres"))? - .attr("href") - .ok_or(anyhow::anyhow!("not found href"))?; - Ok(url.to_string()) -} - -pub async fn get_download_list(image_list: &[i64], db: &DB) -> Result { - let mut download_list = HashMap::new(); - - for img_id in image_list { - if db.contains(&img_id.to_string())? { - log::debug!("{} is exists,skip", img_id); - continue; - } - - let (id, img_data) = match get_image_info(*img_id).await { - Ok(img_data) => img_data, - Err(e) => { - log::error!("get image info failed: {}", e); - continue; - } - }; - log::debug!("get image info: {}", img_id); - if img_data.score < 50 || db.contains(&id.to_string())? { - log::debug!("{} score ,skip", img_id); - continue; - } - - download_list.insert(id, img_data.clone()); - } - - Ok(download_list) -} - -pub async fn download_img(id: i64, url: &str, path: &Path) -> Result { - let client = CLIENT.get_or_init(client_builder); - - let resp = client.get(url).send().await?; - - let ext = url.split('.').last().unwrap_or("jpg"); - let bytes = resp.bytes().await?; - - let path = path.join(format!("{}.{}", id, ext)); - - let mut file = std::fs::File::create(&path)?; - file.write_all(&bytes)?; - - Ok(path) -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[tokio::test] - async fn test_get() { - let resp = get("https://yande.re/post/popular_recent").await.unwrap(); - assert!(resp.contains("yande.re")); - } - - #[tokio::test] - async fn test_get_image_list() { - let resp = get("https://yande.re/post/popular_recent").await.unwrap(); - let image_list = get_image_list(&resp).await.unwrap(); - println!("{:#?}", image_list); - assert!(!image_list.is_empty()); - } - - #[tokio::test] - async fn test_get_image_info() { - let image_info = get_image_info(1124159).await.unwrap(); - println!("{:#?}", image_info); - assert!(!image_info.1.url.is_empty()); - } -} +use std::{ + collections::{HashMap, VecDeque}, + io::Write, + path::{Path, PathBuf}, + sync::OnceLock, +}; + +use anyhow::Result; +use reqwest::{header, Client, ClientBuilder}; +use select::{ + document::{self, Document}, + predicate::{Attr, Class, Name}, +}; + +use crate::db::DB; + +pub static CLIENT: OnceLock = OnceLock::new(); + +type ImgInfo = HashMap; +#[derive(Debug, Clone)] +pub struct ImgData { + pub score: u64, + pub url: VecDeque<(i64, String)>, +} + +fn client_builder() -> Client { + let mut headers = header::HeaderMap::new(); + + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.47"), + ); + + ClientBuilder::new() + .default_headers(headers) + .build() + .unwrap() +} + +async fn get(url: &str) -> Result { + let client = CLIENT.get_or_init(client_builder); + let resp = client.get(url).send().await?.text().await?; + Ok(resp) +} + +pub async fn get_image_list(url: &str) -> Result> { + let html = get(url).await?; + let document = document::Document::from(html.as_str()); + + let list = document + .find(Name("ul")) + .find(|node| node.attr("id") == Some("post-list-posts")) + .ok_or(anyhow::anyhow!("not found ul#post-list-posts"))?; + + let mut image_list = Vec::new(); + + for node in list.children().filter(|node| node.is(Name("li"))) { + let id = node.attr("id").ok_or(anyhow::anyhow!("not found id"))?; + + let id = id.trim().replace('p', "").parse::()?; + image_list.push(id); + } + + Ok(image_list) +} + +async fn find_parent(id: i64) -> Result<(i64, Document)> { + let url = format!("https://yande.re/post/show/{}", id); + let html = get(&url).await?; + + let document = document::Document::from(html.as_str()); + + let result = match document.find(Class("status-notice")).find(|node| { + node.children() + .any(|node| node.is(Name("a")) && node.text().contains("parent post")) + }) { + Some(node) => { + let id = node + .find(Name("a")) + .find(|node| { + node.attr("href") + .is_some_and(|href| href.starts_with("/post/show/")) + }) + .ok_or(anyhow::anyhow!("not found parent post"))? + .attr("href") + .ok_or(anyhow::anyhow!("not found href"))? + .trim() + .replace("/post/show/", "") + .parse::()?; + let url = format!("https://yande.re/post/show/{}", id); + let html = get(&url).await?; + + (id, document::Document::from(html.as_str())) + } + None => (id, document), + }; + + Ok(result) +} + +pub async fn get_image_info(id: i64) -> Result<(i64, ImgData)> { + let (id, document) = find_parent(id).await?; + + let mut download_link: VecDeque<(i64, String)> = VecDeque::new(); + + let mut score = find_score(id, &document)?; + + download_link.push_back((id, find_raw_url(&document)?)); + + let child = document.find(Class("status-notice")).find(|node| { + node.children() + .any(|node| node.is(Name("a")) && node.text().contains("child post")) + }); + + if let Some(node) = child { + for node in node.children().filter(|node| { + node.is(Name("a")) + && node + .attr("href") + .is_some_and(|href| href.starts_with("/post/show/")) + }) { + let id = node.text().trim().parse::()?; + + let html = get(&format!("https://yande.re/post/show/{}", id)).await?; + let document = document::Document::from(html.as_str()); + + let score_child = find_score(id, &document)?; + if score_child > score { + score = score_child; + } + + download_link.push_back((id, find_raw_url(&document)?)); + } + } + + Ok(( + id, + ImgData { + score, + url: download_link, + }, + )) +} + +fn find_score(id: i64, document: &Document) -> Result { + let score = document + .find(Attr("id", format!("post-score-{}", id).as_str())) + .find(|node| node.is(Name("span"))) + .ok_or(anyhow::anyhow!("not found span#post-score-{id}"))? + .text() + .trim() + .parse::()?; + Ok(score) +} + +fn find_raw_url(document: &Document) -> Result { + let url = document + .find(Name("a")) + .find(|node| node.attr("id") == Some("highres")) + .ok_or(anyhow::anyhow!("not found a#highres"))? + .attr("href") + .ok_or(anyhow::anyhow!("not found href"))?; + Ok(url.to_string()) +} + +pub async fn get_download_list(image_list: &[i64], db: &DB) -> Result { + let mut download_list = HashMap::new(); + + for img_id in image_list { + if db.contains(&img_id.to_string())? { + log::debug!("{} is exists,skip", img_id); + continue; + } + + let (id, img_data) = match get_image_info(*img_id).await { + Ok(img_data) => img_data, + Err(e) => { + log::error!("get image info failed: {}", e); + continue; + } + }; + log::debug!("get image info: {}", img_id); + if img_data.score < 50 || db.contains(&id.to_string())? { + log::debug!("{} score ,skip", img_id); + continue; + } + + download_list.insert(id, img_data.clone()); + } + + Ok(download_list) +} + +pub async fn download_img(id: i64, url: &str, path: &Path) -> Result { + let client = CLIENT.get_or_init(client_builder); + + let resp = client.get(url).send().await?; + + let ext = url.split('.').last().unwrap_or("jpg"); + let bytes = resp.bytes().await?; + + let path = path.join(format!("{}.{}", id, ext)); + + let mut file = std::fs::File::create(&path)?; + file.write_all(&bytes)?; + + Ok(path) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[tokio::test] + async fn test_get() { + let resp = get("https://yande.re/post/popular_recent").await.unwrap(); + assert!(resp.contains("yande.re")); + } + + #[tokio::test] + async fn test_get_image_list() { + let resp = get("https://yande.re/post/popular_recent").await.unwrap(); + let image_list = get_image_list(&resp).await.unwrap(); + println!("{:#?}", image_list); + assert!(!image_list.is_empty()); + } + + #[tokio::test] + async fn test_get_image_info() { + let image_info = get_image_info(1124159).await.unwrap(); + println!("{:#?}", image_info); + assert!(!image_info.1.url.is_empty()); + } +}