Dev (#1)
* init * bot framework done * here and ready for orm * might use sea-orm * orm done * use teloxide * ready to go? * 需要完成命令部分 * 需要完成:list_handler() * 查询用户名应当以@开头 * use rustls to avoid segfault? * postgresql ready * inline query done * list_handler * flattern code * test needed * ready to build * some bugs * almost done * ready to take off Co-authored-by: senset <dummy@dummy.d>
This commit is contained in:
parent
fe53f1abd3
commit
56635e0e1b
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.vscode
|
||||||
|
target
|
||||||
|
.env
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.github
|
||||||
|
.DS_Store
|
66
.github/workflows/docker-publish.yml
vendored
Normal file
66
.github/workflows/docker-publish.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
# Publish semver tags as releases.
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Use docker.io for Docker Hub if empty
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
# github.repository as <account>/<repo>
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
# Login against a Docker registry except on PR
|
||||||
|
# https://github.com/docker/login-action
|
||||||
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Extract metadata (tags, labels) for Docker
|
||||||
|
# https://github.com/docker/metadata-action
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
|
# https://github.com/docker/build-push-action
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
|
.DS_Store
|
45
.vscode/launch.json
vendored
Normal file
45
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'saysthbot-reborn'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=saysthbot-reborn",
|
||||||
|
"--package=saysthbot-reborn"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "saysthbot-reborn",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'saysthbot-reborn'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=saysthbot-reborn",
|
||||||
|
"--package=saysthbot-reborn"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "saysthbot-reborn",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3209
Cargo.lock
generated
Normal file
3209
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "saysthbot-reborn"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "A telegram bot to record someone's message by forwarding"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wd_log = "0.1.5"
|
||||||
|
futures = "^0.3"
|
||||||
|
#lazy_static = "*"
|
||||||
|
strfmt = "^0.1.6"
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
version = "3.2.6"
|
||||||
|
features = ["derive", "env"]
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "^1.0"
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
|
[dependencies.teloxide]
|
||||||
|
version = "^0.9"
|
||||||
|
features = ["macros"]
|
||||||
|
|
||||||
|
[dependencies.sea-orm]
|
||||||
|
version = "^0.8.0"
|
||||||
|
features = ["macros", "sqlx-mysql", "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls"]
|
||||||
|
|
||||||
|
[dependencies.models]
|
||||||
|
path = "entity"
|
||||||
|
|
||||||
|
[dependencies.migration]
|
||||||
|
path = "migration"
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM rust as build
|
||||||
|
|
||||||
|
WORKDIR /usr/src/saysthbot
|
||||||
|
COPY . .
|
||||||
|
RUN rustup default nightly && cargo build --release
|
||||||
|
|
||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
RUN apt update && apt install -y proxychains4 ca-certificates && apt clean
|
||||||
|
ENV TGBOT_TOKEN="" DATABASE_URI="" WRAPPER=""
|
||||||
|
CMD ["-c", "${WRAPPER} ./saysthbot-reborn ${OPTIONS}"]
|
||||||
|
ENTRYPOINT [ "/bin/sh" ]
|
||||||
|
|
||||||
|
COPY --from=build /usr/src/saysthbot/target/release/saysthbot-reborn ./
|
42
README.md
42
README.md
@ -1 +1,43 @@
|
|||||||
# Say something bot - Reborn
|
# Say something bot - Reborn
|
||||||
|
|
||||||
|
A telegram bot to record someone's message by forwarding
|
||||||
|
|
||||||
|
```usage
|
||||||
|
saysthbot-reborn 0.1.0
|
||||||
|
A telegram bot to record someone's message by forwarding
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
saysthbot-reborn [OPTIONS] --tgbot-token <TGBOT_TOKEN>
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-d, --database-uri <DATABASE_URI>
|
||||||
|
Database URI [env: DATABASE_URI=] [default:
|
||||||
|
sqlite:///saysthbot.db]
|
||||||
|
|
||||||
|
-D, --debug
|
||||||
|
Enable debug mode
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Print help information
|
||||||
|
|
||||||
|
-t, --tgbot-token <TGBOT_TOKEN>
|
||||||
|
Telegram bot token [env: TGBOT_TOKEN=]
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Print version information
|
||||||
|
```
|
||||||
|
|
||||||
|
## build
|
||||||
|
|
||||||
|
You should use `nightly` build kit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup default nightly
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
Or simply use docker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bot .
|
||||||
|
```
|
||||||
|
12
entity/Cargo.toml
Normal file
12
entity/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "models"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "models"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.sea-orm]
|
||||||
|
version = "^0.8.0"
|
4
entity/src/entities/mod.rs
Normal file
4
entity/src/entities/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod record;
|
||||||
|
pub mod user;
|
8
entity/src/entities/prelude.rs
Normal file
8
entity/src/entities/prelude.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub use super::record::{
|
||||||
|
ActiveModel as RecordActiveModel, Column as RecordColumn, Entity as Record,
|
||||||
|
Model as RecordModel, PrimaryKey as RecordPrimaryKey, Relation as RecordRelation,
|
||||||
|
};
|
||||||
|
pub use super::user::{
|
||||||
|
ActiveModel as UserActiveModel, Column as UserColumn, Entity as User, Model as UserModel,
|
||||||
|
PrimaryKey as UserPrimaryKey, Relation as UserRelation,
|
||||||
|
};
|
35
entity/src/entities/record.rs
Normal file
35
entity/src/entities/record.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "records")]
|
||||||
|
pub struct Model {
|
||||||
|
/// internal ID
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// relation user id
|
||||||
|
#[sea_orm(indexed)]
|
||||||
|
pub user_id: i64,
|
||||||
|
|
||||||
|
/// records
|
||||||
|
#[sea_orm(indexed, column_type = "Text", unique)]
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::user::Entity",
|
||||||
|
from = "Column::UserId",
|
||||||
|
to = "super::user::Column::Id"
|
||||||
|
)]
|
||||||
|
User,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::User.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
35
entity/src/entities/user.rs
Normal file
35
entity/src/entities/user.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "users")]
|
||||||
|
pub struct Model {
|
||||||
|
/// internal ID
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// Telegram user ID
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub tg_uid: i64,
|
||||||
|
|
||||||
|
/// Telegram user name
|
||||||
|
#[sea_orm(nullable)]
|
||||||
|
pub username: Option<String>,
|
||||||
|
|
||||||
|
/// use notify
|
||||||
|
#[sea_orm(default_value = true)]
|
||||||
|
pub notify: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::record::Entity")]
|
||||||
|
Record,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::record::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Record.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
2
entity/src/lib.rs
Normal file
2
entity/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod entities;
|
||||||
|
pub use entities::*;
|
15
migration/Cargo.toml
Normal file
15
migration/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "migration"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "migration"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
models = { path = "../entity" }
|
||||||
|
|
||||||
|
[dependencies.sea-orm-migration]
|
||||||
|
version = "^0.8.0"
|
37
migration/README.md
Normal file
37
migration/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Running Migrator CLI
|
||||||
|
|
||||||
|
- Apply all pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
cargo run -- up
|
||||||
|
```
|
||||||
|
- Apply first 10 pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- up -n 10
|
||||||
|
```
|
||||||
|
- Rollback last applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down
|
||||||
|
```
|
||||||
|
- Rollback last 10 applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down -n 10
|
||||||
|
```
|
||||||
|
- Drop all tables from the database, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- fresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- refresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- reset
|
||||||
|
```
|
||||||
|
- Check the status of all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- status
|
||||||
|
```
|
16
migration/src/lib.rs
Normal file
16
migration/src/lib.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
mod m20220101_000001_create_table;
|
||||||
|
mod m20220625_222908_message_unique;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![
|
||||||
|
Box::new(m20220101_000001_create_table::Migration),
|
||||||
|
Box::new(m20220625_222908_message_unique::Migration),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
42
migration/src/m20220101_000001_create_table.rs
Normal file
42
migration/src/m20220101_000001_create_table.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use models::*;
|
||||||
|
use sea_orm_migration::{
|
||||||
|
prelude::*,
|
||||||
|
sea_orm::{ConnectionTrait, Schema},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20220101_000001_create_table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let db = manager.get_connection();
|
||||||
|
let builder = db.get_database_backend();
|
||||||
|
let schema = Schema::new(builder);
|
||||||
|
|
||||||
|
db.execute(builder.build(&schema.create_table_from_entity(user::Entity)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
db.execute(builder.build(&schema.create_table_from_entity(record::Entity)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(user::Entity).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(record::Entity).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
39
migration/src/m20220625_222908_message_unique.rs
Normal file
39
migration/src/m20220625_222908_message_unique.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use models::prelude::{Record, RecordColumn};
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
const RECORD_MESSAGE_UNIQUE: &str = "record_message_unique";
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20220625_222908_message_unique"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.table(Record)
|
||||||
|
.col(RecordColumn::Message)
|
||||||
|
.name(RECORD_MESSAGE_UNIQUE)
|
||||||
|
.unique()
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_index(
|
||||||
|
Index::drop()
|
||||||
|
.table(Record)
|
||||||
|
.name(RECORD_MESSAGE_UNIQUE)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run_cli(migration::Migrator).await;
|
||||||
|
}
|
21
src/callback_commands.rs
Normal file
21
src/callback_commands.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use teloxide::utils::command::BotCommands;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, BotCommands)]
|
||||||
|
#[command(rename = "lowercase", prefix = "!")]
|
||||||
|
pub enum CallbackCommands {
|
||||||
|
#[command(description = "internal command page", parse_with = "split")]
|
||||||
|
Page {
|
||||||
|
msg_id: i32,
|
||||||
|
username: String,
|
||||||
|
page: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(description = "default dummy command")]
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CallbackCommands {
|
||||||
|
fn default() -> Self {
|
||||||
|
CallbackCommands::Default
|
||||||
|
}
|
||||||
|
}
|
330
src/commands.rs
Normal file
330
src/commands.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use strfmt::Format;
|
||||||
|
use teloxide::{
|
||||||
|
prelude::*,
|
||||||
|
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||||
|
types::{InlineKeyboardButtonKind, ReplyMarkup},
|
||||||
|
utils::command::{BotCommands, ParseError},
|
||||||
|
};
|
||||||
|
use wd_log::log_debug_ln;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db_controller::PaginatedRecordData,
|
||||||
|
messages::{
|
||||||
|
BOT_ABOUT, BOT_BUTTON_END, BOT_BUTTON_HEAD, BOT_BUTTON_NEXT, BOT_BUTTON_PREV, BOT_HELP,
|
||||||
|
BOT_TEXT_DELETED, BOT_TEXT_LOADING, BOT_TEXT_MUTE_STATUS, BOT_TEXT_STATUS_OFF,
|
||||||
|
BOT_TEXT_STATUS_ON, BOT_TEXT_WELCOME,
|
||||||
|
},
|
||||||
|
telegram_bot::BotServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(BotCommands, PartialEq, Debug)]
|
||||||
|
#[command(rename = "lowercase")]
|
||||||
|
pub enum Commands {
|
||||||
|
#[command(description = "显示帮助信息")]
|
||||||
|
Help,
|
||||||
|
|
||||||
|
#[command(description = "关于本 Bot")]
|
||||||
|
About,
|
||||||
|
|
||||||
|
#[command(description = "关闭提醒")]
|
||||||
|
Mute,
|
||||||
|
|
||||||
|
#[command(description = "开启提醒")]
|
||||||
|
Unmute,
|
||||||
|
|
||||||
|
#[command(description = "列出已记录的内容", parse_with = "list_command_parser")]
|
||||||
|
List { username: String },
|
||||||
|
|
||||||
|
#[command(description = "删除记录")]
|
||||||
|
Del { id: i64 },
|
||||||
|
|
||||||
|
#[command(description = "注册")]
|
||||||
|
Start,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Commands {
|
||||||
|
fn default() -> Self {
|
||||||
|
Commands::Help
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_command_parser(input: String) -> Result<(String,), ParseError> {
|
||||||
|
log_debug_ln!(
|
||||||
|
"list_command_parse = \"{}\", is empty = {}",
|
||||||
|
input,
|
||||||
|
input.trim().is_empty()
|
||||||
|
);
|
||||||
|
|
||||||
|
let output: String;
|
||||||
|
|
||||||
|
if input.trim().is_empty() {
|
||||||
|
output = "me".to_string();
|
||||||
|
} else {
|
||||||
|
output = input
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((output,))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CommandHandler {}
|
||||||
|
|
||||||
|
impl CommandHandler {
|
||||||
|
pub async fn about_handler(bot_s: &BotServer, message: &Message) {
|
||||||
|
bot_s.send_text_reply(message, BOT_ABOUT).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn help_handler(bot_s: &BotServer, message: &Message) {
|
||||||
|
bot_s.send_text_reply(message, BOT_HELP).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn notify_handler(bot_s: &BotServer, message: &Message, enabled: bool) {
|
||||||
|
let user = match message.from() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.is_bot {
|
||||||
|
if let Err(error) = bot_s
|
||||||
|
.controller
|
||||||
|
.set_user_notify(&user.id.0.try_into().unwrap(), enabled)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
bot_s.controller.err_handler(error);
|
||||||
|
}
|
||||||
|
let mut vars = HashMap::new();
|
||||||
|
vars.insert(
|
||||||
|
"status".to_string(),
|
||||||
|
match enabled {
|
||||||
|
true => BOT_TEXT_STATUS_ON,
|
||||||
|
false => BOT_TEXT_STATUS_OFF,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bot_s
|
||||||
|
.send_text_reply(message, &BOT_TEXT_MUTE_STATUS.format(&vars).unwrap())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_handler(bot_s: &BotServer, message: &Message) {
|
||||||
|
let user = match message.from() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.is_bot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id: i64 = user.id.0.try_into().unwrap();
|
||||||
|
let username = match user.username.to_owned() {
|
||||||
|
Some(username) => format!("@{}", username),
|
||||||
|
None => user.first_name.to_owned(),
|
||||||
|
};
|
||||||
|
if let Err(error) = bot_s.controller.register_user(&user_id, &username).await {
|
||||||
|
bot_s.controller.err_handler(error);
|
||||||
|
}
|
||||||
|
bot_s.send_text_reply(message, BOT_TEXT_WELCOME).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn del_handler(bot_s: &BotServer, message: &Message, id: i64) {
|
||||||
|
let user = match message.from() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.is_bot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = bot_s
|
||||||
|
.controller
|
||||||
|
.del_record(id, user.id.0.try_into().unwrap())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
bot_s.controller.err_handler(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bot_s.send_text_reply(message, BOT_TEXT_DELETED).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_handler(bot_s: &BotServer, message: &Message, username: &str, page: usize) {
|
||||||
|
let user = match message.from() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.is_bot {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg_id = match bot_s.send_text_reply(message, BOT_TEXT_LOADING).await {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (msg, markup) = match Self::record_msg_genrator(bot_s, message, username, page).await {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
bot_s
|
||||||
|
.edit_text_reply_with_inline_key(message, msg_id, msg.as_str(), markup)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn record_msg_genrator(
|
||||||
|
bot_s: &BotServer,
|
||||||
|
message: &Message,
|
||||||
|
username: &str,
|
||||||
|
page: usize,
|
||||||
|
) -> Option<(String, ReplyMarkup)> {
|
||||||
|
let someone = match bot_s.controller.get_user_by_username(username).await {
|
||||||
|
Ok(someone) => someone,
|
||||||
|
Err(error) => {
|
||||||
|
bot_s.controller.err_handler(error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let someone = match someone {
|
||||||
|
Some(someone) => someone,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = match bot_s
|
||||||
|
.controller
|
||||||
|
.get_records_by_userid_with_pagination(someone.id, page)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(error) => {
|
||||||
|
bot_s.controller.err_handler(error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let paginated_record_data = match data {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((
|
||||||
|
Self::generate_text_record_msg(&paginated_record_data, page),
|
||||||
|
Self::generate_inline_keyboard(
|
||||||
|
page,
|
||||||
|
paginated_record_data.pages_count,
|
||||||
|
username,
|
||||||
|
message,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_inline_keyboard(
|
||||||
|
page: usize,
|
||||||
|
pages_count: usize,
|
||||||
|
username: &str,
|
||||||
|
message: &Message,
|
||||||
|
) -> ReplyMarkup {
|
||||||
|
let inline_keyboards = match page {
|
||||||
|
page if page == 0 && pages_count > 1 => vec![
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_NEXT.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
page + 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_END.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
pages_count - 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
page if page == 0 && pages_count <= 1 => vec![],
|
||||||
|
page if page >= pages_count - 1 => vec![
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_HEAD.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id, username, 0
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_PREV.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
page - 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_ => vec![
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_HEAD.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id, username, 0
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_PREV.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
page - 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_NEXT.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
page + 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
InlineKeyboardButton {
|
||||||
|
text: BOT_BUTTON_END.to_string(),
|
||||||
|
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||||
|
"!page {} {} {}",
|
||||||
|
message.id,
|
||||||
|
username,
|
||||||
|
pages_count - 1
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
|
||||||
|
inline_keyboard: vec![inline_keyboards],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_text_record_msg(
|
||||||
|
paginated_record_data: &PaginatedRecordData,
|
||||||
|
page: usize,
|
||||||
|
) -> String {
|
||||||
|
let mut msg = String::from("```");
|
||||||
|
for (message, _) in paginated_record_data.current_data.iter() {
|
||||||
|
msg = format!("{}\n{}\t\t\t\t{}", msg, message.id, message.message);
|
||||||
|
}
|
||||||
|
msg = format!(
|
||||||
|
"{}\n```\n{}/{}",
|
||||||
|
msg,
|
||||||
|
page + 1,
|
||||||
|
paginated_record_data.pages_count
|
||||||
|
);
|
||||||
|
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
19
src/config.rs
Normal file
19
src/config.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
const DEFAULT_DATABASE: &'static str = "sqlite:///saysthbot.db";
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Enable debug mode
|
||||||
|
#[clap(short = 'D', long, value_parser, default_value_t = false)]
|
||||||
|
pub debug: bool,
|
||||||
|
|
||||||
|
/// Telegram bot token
|
||||||
|
#[clap(short, long, value_parser, env = "TGBOT_TOKEN")]
|
||||||
|
pub tgbot_token: String,
|
||||||
|
|
||||||
|
/// Database URI
|
||||||
|
#[clap(short, long, value_parser, env = "DATABASE_URI", default_value=DEFAULT_DATABASE)]
|
||||||
|
pub database_uri: String,
|
||||||
|
}
|
190
src/db_controller.rs
Normal file
190
src/db_controller.rs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
use migration::{Migrator, MigratorTrait};
|
||||||
|
use models::prelude::*;
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, DatabaseTransaction, DbErr,
|
||||||
|
EntityTrait, PaginatorTrait, QueryFilter, Set, TransactionTrait,
|
||||||
|
};
|
||||||
|
use wd_log::{log_error_ln, log_info_ln, log_warn_ln};
|
||||||
|
|
||||||
|
const PAGE_SIZE: usize = 25;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Controller {
|
||||||
|
db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PaginatedRecordData {
|
||||||
|
pub items_count: usize,
|
||||||
|
pub pages_count: usize,
|
||||||
|
pub current_data: Vec<(RecordModel, Option<UserModel>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Controller {
|
||||||
|
/// Create controller
|
||||||
|
pub async fn new(config: String) -> Result<Self, DbErr> {
|
||||||
|
Ok(Self {
|
||||||
|
db: Database::connect(config).await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do migrate
|
||||||
|
pub async fn migrate(&self) -> Result<(), DbErr> {
|
||||||
|
if let Err(err) = Migrator::install(&self.db).await {
|
||||||
|
log_warn_ln!("{}", err)
|
||||||
|
}
|
||||||
|
if let Err(err) = Migrator::up(&self.db, None).await {
|
||||||
|
Err(err)
|
||||||
|
} else {
|
||||||
|
log_info_ln!("database initialized.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// register user when `/start` command called.
|
||||||
|
pub async fn register_user(&self, user_id: &i64, username: &String) -> Result<(), DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
self.setup_user(user_id, username, &transaction).await?;
|
||||||
|
transaction.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update user notify when `/mute` or `/unmute` command called.
|
||||||
|
pub async fn set_user_notify(&self, user_id: &i64, notify: bool) -> Result<(), DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
if let Some(user) = self.get_user(user_id, &transaction).await? {
|
||||||
|
let mut user_active: UserActiveModel = user.into();
|
||||||
|
user_active.notify = Set(notify);
|
||||||
|
user_active.save(&transaction).await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_notify(&self, user_id: &i64) -> Result<bool, DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||||
|
Ok(user.notify)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_user(
|
||||||
|
&self,
|
||||||
|
user_id: &i64,
|
||||||
|
username: &String,
|
||||||
|
transaction: &DatabaseTransaction,
|
||||||
|
) -> Result<UserActiveModel, DbErr> {
|
||||||
|
match self.get_user(user_id, &transaction).await? {
|
||||||
|
Some(user) => {
|
||||||
|
let mut user_active: UserActiveModel = user.into();
|
||||||
|
user_active.username = Set(Some(username.to_string()));
|
||||||
|
user_active.save(transaction).await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
UserActiveModel {
|
||||||
|
tg_uid: Set(user_id.to_owned()),
|
||||||
|
username: Set(Some(username.to_string())),
|
||||||
|
notify: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.save(transaction)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
&self,
|
||||||
|
user_id: &i64,
|
||||||
|
transaction: &DatabaseTransaction,
|
||||||
|
) -> Result<Option<UserModel>, DbErr> {
|
||||||
|
User::find()
|
||||||
|
.filter(UserColumn::Id.eq(user_id.to_owned()))
|
||||||
|
.one(transaction)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_username(&self, username: &str) -> Result<Option<UserModel>, DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
User::find()
|
||||||
|
.filter(UserColumn::Username.eq(username.to_owned()))
|
||||||
|
.one(&transaction)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get records when inline query called.
|
||||||
|
pub async fn get_records_by_keywords(
|
||||||
|
&self,
|
||||||
|
key_word: &String,
|
||||||
|
) -> Result<PaginatedRecordData, DbErr> {
|
||||||
|
let pagination = Record::find()
|
||||||
|
.find_also_related(User)
|
||||||
|
.filter(RecordColumn::Message.contains(key_word.as_str()))
|
||||||
|
.paginate(&self.db, PAGE_SIZE * 2); // 50 records seems ok.
|
||||||
|
Ok(PaginatedRecordData {
|
||||||
|
items_count: pagination.num_items().await?,
|
||||||
|
pages_count: pagination.num_pages().await?,
|
||||||
|
current_data: pagination.fetch().await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get records when `/list` command called or inline button request.
|
||||||
|
pub async fn get_records_by_userid_with_pagination(
|
||||||
|
&self,
|
||||||
|
user_id: i64,
|
||||||
|
page: usize,
|
||||||
|
) -> Result<Option<PaginatedRecordData>, DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||||
|
let pagination = Record::find()
|
||||||
|
.find_also_related(User)
|
||||||
|
.filter(RecordColumn::UserId.eq(user.id))
|
||||||
|
.paginate(&transaction, PAGE_SIZE);
|
||||||
|
Ok(Some(PaginatedRecordData {
|
||||||
|
current_data: pagination.fetch_page(page).await?,
|
||||||
|
items_count: pagination.num_items().await?,
|
||||||
|
pages_count: pagination.num_pages().await?,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
log_error_ln!("cannot find user tg_uid={}", user_id);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// add record forward a message to bot.
|
||||||
|
pub async fn add_record(
|
||||||
|
&self,
|
||||||
|
user_id: i64,
|
||||||
|
username: &String,
|
||||||
|
text: String,
|
||||||
|
) -> Result<(), DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
let user = self.setup_user(&user_id, &username, &transaction).await?;
|
||||||
|
RecordActiveModel {
|
||||||
|
message: Set(text),
|
||||||
|
user_id: user.id,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&transaction)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// del record when `/delete` command called.
|
||||||
|
pub async fn del_record(&self, id: i64, user_id: i64) -> Result<(), DbErr> {
|
||||||
|
let transaction = self.db.begin().await?;
|
||||||
|
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||||
|
RecordActiveModel {
|
||||||
|
id: Set(id),
|
||||||
|
user_id: Set(user.id),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.delete(&transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err_handler(&self, error: DbErr) {
|
||||||
|
log_error_ln!("{}", error);
|
||||||
|
}
|
||||||
|
}
|
36
src/main.rs
Normal file
36
src/main.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
mod callback_commands;
|
||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod db_controller;
|
||||||
|
mod messages;
|
||||||
|
mod telegram_bot;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use config::Args;
|
||||||
|
use telegram_bot::BotServer;
|
||||||
|
use wd_log::{log_debug_ln, log_panic, set_level, set_prefix, DEBUG, INFO};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
set_prefix("saysthbot");
|
||||||
|
|
||||||
|
if args.debug {
|
||||||
|
set_level(DEBUG);
|
||||||
|
log_debug_ln!("{:?}", args);
|
||||||
|
} else {
|
||||||
|
set_level(INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bot = match BotServer::new(args).await {
|
||||||
|
Ok(bot) => bot,
|
||||||
|
Err(err) => log_panic!("{}", err),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = bot.init().await {
|
||||||
|
log_panic!("{}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.run().await;
|
||||||
|
}
|
21
src/messages.rs
Normal file
21
src/messages.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
pub const BOT_TEXT_MESSAGE_ONLY: &'static str = "仅支持文本信息";
|
||||||
|
pub const BOT_TEXT_FORWARDED_ONLY: &'static str = "仅支持转发信息";
|
||||||
|
pub const BOT_TEXT_USER_ONLY: &'static str = "仅支持用户信息";
|
||||||
|
pub const BOT_TEXT_NO_BOT: &'static str = "不支持 bot 消息";
|
||||||
|
pub const BOT_TEXT_NOTED: &'static str = "✅ `{data}` 已记录";
|
||||||
|
pub const BOT_TEXT_NOTICE: &'static str = "[{username}](tg://user?id={user_id}) 转发了你的 `{data}`\n\n\t你可以使用 /list 命令查看自己或者他人被记录的信息\n\t你可以使用 /del 命令删除某条自己的信息\n\t你也可以使用 /mute 或者 /unmute 命令开启或者关闭提醒";
|
||||||
|
pub const BOT_TEXT_WELCOME: &'static str =
|
||||||
|
"✅ 注册成功!如果有别人记录了你的消息,这里会有提醒,可使用 /mute 命令关闭提醒";
|
||||||
|
pub const BOT_HELP: &'static str = "*帮助*\n\n\t/list `[@username]` 列出已记录的内容\n\t/del `id` 删除对应id的记录,只能删除自己的\n\t/mute 关闭提醒\n\t/unmute 开启提醒";
|
||||||
|
pub const BOT_ABOUT: &'static str =
|
||||||
|
"Say something bot \\- Reborn\n\n[Github](https://github.com/senseab/saysthbot-reborn) @ssthbot";
|
||||||
|
pub const BOT_TEXT_MUTE_STATUS: &'static str = "提醒状态:{status}";
|
||||||
|
pub const BOT_TEXT_STATUS_ON: &'static str = "✅ 开启";
|
||||||
|
pub const BOT_TEXT_STATUS_OFF: &'static str = "❎ 关闭";
|
||||||
|
pub const BOT_TEXT_DELETED: &'static str = "已删除";
|
||||||
|
pub const BOT_TEXT_SHOULD_START_WITH_AT: &'static str = "用户名应当以 `@` 开头";
|
||||||
|
pub const BOT_BUTTON_HEAD: &'static str = "⏮ 首页";
|
||||||
|
pub const BOT_BUTTON_END: &'static str = "末页 ⏭";
|
||||||
|
pub const BOT_BUTTON_PREV: &'static str = "⏪ 上一页";
|
||||||
|
pub const BOT_BUTTON_NEXT: &'static str = "下一页 ⏩";
|
||||||
|
pub const BOT_TEXT_LOADING: &'static str = "⌛️ 载入中……";
|
433
src/telegram_bot.rs
Normal file
433
src/telegram_bot.rs
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::callback_commands::CallbackCommands;
|
||||||
|
use crate::db_controller::Controller;
|
||||||
|
use crate::messages::*;
|
||||||
|
use crate::{commands::CommandHandler, commands::Commands, config::Args};
|
||||||
|
use migration::DbErr;
|
||||||
|
use strfmt::Format;
|
||||||
|
|
||||||
|
use teloxide::utils::command::BotCommands;
|
||||||
|
use teloxide::{
|
||||||
|
prelude::*, types::ForwardedFrom, types::InlineQueryResult, types::InlineQueryResultArticle,
|
||||||
|
types::InputMessageContent, types::InputMessageContentText, types::ParseMode,
|
||||||
|
types::ReplyMarkup, types::UpdateKind, RequestError,
|
||||||
|
};
|
||||||
|
use wd_log::{log_debug_ln, log_error_ln, log_info_ln, log_panic, log_warn_ln};
|
||||||
|
|
||||||
|
pub struct BotServer {
|
||||||
|
pub controller: Controller,
|
||||||
|
bot: Bot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotServer {
|
||||||
|
/// Create new bot
|
||||||
|
pub async fn new(config: Args) -> Result<Self, DbErr> {
|
||||||
|
Ok(Self {
|
||||||
|
bot: Bot::new(config.tgbot_token),
|
||||||
|
controller: Controller::new(config.database_uri).await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&self) -> Result<(), DbErr> {
|
||||||
|
self.controller.migrate().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the bot
|
||||||
|
pub async fn run(&self) {
|
||||||
|
match self.bot.get_me().send().await {
|
||||||
|
Ok(result) => log_info_ln!(
|
||||||
|
"connect succeed: id={}, botname=\"{}\"",
|
||||||
|
result.id,
|
||||||
|
result.username()
|
||||||
|
),
|
||||||
|
Err(error) => log_panic!("{}", error),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.register_commands().await;
|
||||||
|
|
||||||
|
let mut offset_id = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let updates = match self.bot.get_updates().offset(offset_id).send().await {
|
||||||
|
Ok(it) => it,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
for update in updates {
|
||||||
|
self.update_handler(&update).await;
|
||||||
|
offset_id = update.id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_commands(&self) {
|
||||||
|
if let Err(error) = self
|
||||||
|
.bot
|
||||||
|
.set_my_commands(Commands::bot_commands())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.default_error_handler(&error);
|
||||||
|
} else {
|
||||||
|
log_info_ln!("commands registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_handler(&self, update: &Update) {
|
||||||
|
match &update.kind {
|
||||||
|
UpdateKind::Message(ref message) => self.message_handler(message).await,
|
||||||
|
UpdateKind::InlineQuery(inline_query) => self.inline_query_hander(inline_query).await,
|
||||||
|
UpdateKind::CallbackQuery(callback) => self.callback_handler(callback).await,
|
||||||
|
kind => self.default_update_hander(&kind).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn default_update_hander(&self, update_kind: &UpdateKind) {
|
||||||
|
log_debug_ln!("non-supported kind {:?}", update_kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn callback_handler(&self, callback: &CallbackQuery) {
|
||||||
|
log_debug_ln!("callback={:#?}", callback);
|
||||||
|
|
||||||
|
let message = match &callback.message {
|
||||||
|
Some(msg) => msg,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match &callback.data {
|
||||||
|
Some(text) => text,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bot_username = match self.bot.get_me().send().await {
|
||||||
|
Ok(result) => result.username.to_owned(),
|
||||||
|
Err(error) => {
|
||||||
|
self.default_error_handler(&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bot_username = match bot_username {
|
||||||
|
Some(b) => b,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let commands = match CallbackCommands::parse(text, bot_username) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(error) => {
|
||||||
|
log_warn_ln!("{}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match commands {
|
||||||
|
CallbackCommands::Page {
|
||||||
|
msg_id: _,
|
||||||
|
username,
|
||||||
|
page,
|
||||||
|
} => {
|
||||||
|
let (msg, keyboard) = match CommandHandler::record_msg_genrator(
|
||||||
|
self,
|
||||||
|
message,
|
||||||
|
username.as_str(),
|
||||||
|
page,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(d) => d,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.edit_text_reply_with_inline_key(message, message.id, msg.as_str(), keyboard)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match self.bot.answer_callback_query(&callback.id).send().await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(error) => self.default_error_handler(&error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CallbackCommands::Default => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inline_query_hander(&self, inline_query: &InlineQuery) {
|
||||||
|
let results = match self
|
||||||
|
.controller
|
||||||
|
.get_records_by_keywords(&inline_query.query)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(results) => results,
|
||||||
|
Err(error) => {
|
||||||
|
self.controller.err_handler(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r: Vec<InlineQueryResult> = vec![];
|
||||||
|
for (record, o_user) in results.current_data.iter() {
|
||||||
|
let user = match o_user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = match &user.username {
|
||||||
|
Some(username) => username,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
r.push(InlineQueryResult::Article(InlineQueryResultArticle {
|
||||||
|
id: record.id.to_string(),
|
||||||
|
title: record.message.to_owned(),
|
||||||
|
input_message_content: InputMessageContent::Text(InputMessageContentText {
|
||||||
|
message_text: format!(
|
||||||
|
"*{}*: {}",
|
||||||
|
username.trim_start_matches("@"),
|
||||||
|
record.message
|
||||||
|
),
|
||||||
|
parse_mode: Some(ParseMode::MarkdownV2),
|
||||||
|
entities: None,
|
||||||
|
disable_web_page_preview: Some(true),
|
||||||
|
}),
|
||||||
|
reply_markup: None,
|
||||||
|
url: None,
|
||||||
|
hide_url: None,
|
||||||
|
description: Some(format!("By: {}", username)),
|
||||||
|
thumb_url: None,
|
||||||
|
thumb_width: None,
|
||||||
|
thumb_height: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = self
|
||||||
|
.bot
|
||||||
|
.answer_inline_query(&inline_query.id, r.into_iter())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
self.default_error_handler(&error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn message_handler(&self, message: &Message) {
|
||||||
|
if let Some(data) = &message.text() {
|
||||||
|
self.text_message_heandler(message, data).await
|
||||||
|
} else {
|
||||||
|
self.default_message_handler(message).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn text_message_heandler(&self, message: &Message, data: &str) {
|
||||||
|
let forward = match message.forward() {
|
||||||
|
Some(forward) => forward,
|
||||||
|
None => {
|
||||||
|
if data.starts_with("/") {
|
||||||
|
self.command_hanler(message).await;
|
||||||
|
} else {
|
||||||
|
self.send_text_reply(message, BOT_TEXT_FORWARDED_ONLY).await;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &forward.from {
|
||||||
|
ForwardedFrom::User(user) if !user.is_bot => {
|
||||||
|
let username = match &user.username {
|
||||||
|
Some(username) => format!("@{}", username),
|
||||||
|
None => user.first_name.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = self
|
||||||
|
.controller
|
||||||
|
.add_record(user.id.0.try_into().unwrap(), &username, data.to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log_error_ln!("{}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut vars = HashMap::new();
|
||||||
|
vars.insert("data".to_string(), data);
|
||||||
|
|
||||||
|
self.send_text_reply(message, &BOT_TEXT_NOTED.format(&vars).unwrap())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let from = match message.from() {
|
||||||
|
Some(from) => from,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if from.id == user.id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if match self
|
||||||
|
.controller
|
||||||
|
.get_user_notify(&user.id.0.try_into().unwrap())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(notify) => notify,
|
||||||
|
Err(error) => {
|
||||||
|
log_error_ln!("{}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
let mut vars = HashMap::new();
|
||||||
|
let user_id = user.id.to_string();
|
||||||
|
let data = data.to_string();
|
||||||
|
|
||||||
|
vars.insert("username".to_string(), &from.first_name);
|
||||||
|
vars.insert("user_id".to_string(), &user_id);
|
||||||
|
vars.insert("data".to_string(), &data);
|
||||||
|
|
||||||
|
match self
|
||||||
|
.bot
|
||||||
|
.send_message(user.id, &BOT_TEXT_NOTICE.format(&vars).unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => {
|
||||||
|
log_debug_ln!("message sent {:?}", result)
|
||||||
|
}
|
||||||
|
Err(err) => self.default_error_handler(&err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ForwardedFrom::User(_) => {
|
||||||
|
self.send_text_reply(message, BOT_TEXT_NO_BOT).await;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.send_text_message(message, BOT_TEXT_USER_ONLY).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command_hanler(&self, message: &Message) {
|
||||||
|
let msg = match message.text() {
|
||||||
|
Some(msg) => msg,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bot_username = match self.bot.get_me().send().await {
|
||||||
|
Ok(result) => result.username.to_owned(),
|
||||||
|
Err(error) => {
|
||||||
|
self.default_error_handler(&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bot_username = match bot_username {
|
||||||
|
Some(b) => b,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let commands = match Commands::parse(msg, bot_username) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(error) => {
|
||||||
|
log_warn_ln!("{}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match commands {
|
||||||
|
Commands::Help => CommandHandler::help_handler(&self, message).await,
|
||||||
|
Commands::About => CommandHandler::about_handler(&self, message).await,
|
||||||
|
Commands::Mute => CommandHandler::notify_handler(&self, message, true).await,
|
||||||
|
Commands::Unmute => CommandHandler::notify_handler(&self, message, false).await,
|
||||||
|
Commands::List { mut username } => {
|
||||||
|
if username == "me" {
|
||||||
|
if let Some(from) = message.from() {
|
||||||
|
if let Some(_username) = &from.username {
|
||||||
|
username = format!("@{}", _username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if username.starts_with("@") {
|
||||||
|
// always start from page=0
|
||||||
|
CommandHandler::list_handler(&self, message, &username, 0).await;
|
||||||
|
} else {
|
||||||
|
self.send_text_reply(message, BOT_TEXT_SHOULD_START_WITH_AT)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::Del { id } => CommandHandler::del_handler(&self, message, id).await,
|
||||||
|
Commands::Start => CommandHandler::setup_handler(&self, message).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_error_handler(&self, error: &RequestError) {
|
||||||
|
log_error_ln!("{:?}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn default_message_handler(&self, message: &Message) {
|
||||||
|
log_debug_ln!(
|
||||||
|
"non-spported message {:?} from `{:?}`",
|
||||||
|
message.kind,
|
||||||
|
message.from()
|
||||||
|
);
|
||||||
|
self.send_text_reply(message, BOT_TEXT_MESSAGE_ONLY).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_text_message(&self, message: &Message, text: &str) -> Option<i32> {
|
||||||
|
match &self
|
||||||
|
.bot
|
||||||
|
.send_message(message.chat.id, text)
|
||||||
|
.parse_mode(ParseMode::MarkdownV2)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => {
|
||||||
|
log_debug_ln!("message sent {:?}", result);
|
||||||
|
Some(result.id)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.default_error_handler(error);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_text_reply(&self, message: &Message, text: &str) -> Option<i32> {
|
||||||
|
match &self
|
||||||
|
.bot
|
||||||
|
.send_message(message.chat.id, text)
|
||||||
|
.reply_to_message_id(message.id)
|
||||||
|
.parse_mode(ParseMode::MarkdownV2)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => {
|
||||||
|
log_debug_ln!("reply sent {:?}", result);
|
||||||
|
Some(result.id)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.default_error_handler(error);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit_text_reply_with_inline_key(
|
||||||
|
&self,
|
||||||
|
message: &Message,
|
||||||
|
msg_id: i32,
|
||||||
|
text: &str,
|
||||||
|
keyboard: ReplyMarkup,
|
||||||
|
) {
|
||||||
|
let keyboard = match keyboard {
|
||||||
|
ReplyMarkup::InlineKeyboard(keyboard) => keyboard,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &self
|
||||||
|
.bot
|
||||||
|
.edit_message_text(message.chat.id, msg_id, text)
|
||||||
|
.reply_markup(keyboard)
|
||||||
|
.parse_mode(ParseMode::MarkdownV2)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => log_debug_ln!("reply sent {:?}", result),
|
||||||
|
Err(error) => self.default_error_handler(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user