Compare commits

...

7 commits

Author SHA1 Message Date
dca70ba2cc
Bump version to 0.2.2, add inline command for listing unviewed videos
Some checks failed
Docker Push / docker (push) Has been cancelled
2025-02-09 18:58:24 +03:00
fa125b47fb
Add TELEGRAM_API_URL support, improve error handling in video metadata retrieval and fixing migration rollback
Some checks are pending
Docker Push / docker (push) Waiting to run
2025-02-08 15:18:22 +03:00
655e195c91
f*cking i am! f*cking i32!
Some checks failed
Docker Push / docker (push) Has been cancelled
2025-01-28 21:46:03 +03:00
3132ebd036
fix i32 uid in check_rights 2025-01-28 21:30:25 +03:00
c84385bc68
Added environments to README
Some checks failed
Docker Push / docker (push) Has been cancelled
2025-01-20 05:21:31 +03:00
591020f04b
Auto Migrations + Docker
Some checks are pending
Docker Push / docker (push) Waiting to run
2025-01-19 18:49:15 +03:00
5bcfff99a1
i hate README 2025-01-08 18:31:55 +03:00
18 changed files with 604 additions and 290 deletions

42
.github/workflows/docker-push.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Docker Push
on:
push:
branches: [ "master" ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
# - name: Checkout code
# uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# - name: Login to Docker Hub
# uses: docker/login-action@v3
# with:
# username: ${{ vars.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get short SHA
id: short_sha
run: echo "sha=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
# context: .
tags: ghcr.io/${{ github.repository_owner }}/doggy-watch:${{ steps.short_sha.outputs.sha }},ghcr.io/${{ github.repository_owner }}/doggy-watch:latest
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/doggy-watch:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/doggy-watch:buildcache,mode=max

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/act /act
.env .env
note.txt note.txt
docker-compose.yaml

496
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,17 @@
[package] [package]
name = "doggy-watch" name = "doggy-watch"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"] authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.2.0" version = "0.2.2"
edition = "2021" edition = "2021"
publish = false publish = false
[workspace] [workspace]
members = [ "database", "youtube", "database/migration" ] members = [ "database", "youtube", "migration" ]
[dependencies] [dependencies]
database = { path = "database" } database = { path = "database" }
youtube = { path = "youtube" } youtube = { path = "youtube" }
migration = { path = "migration"}
anyhow = "1.0" anyhow = "1.0"
dotenvy = "0.15" dotenvy = "0.15"
@ -25,6 +26,7 @@ tracing-panic = "0.1"
lazy_static = "1.5" lazy_static = "1.5"
indexmap = "2.7" indexmap = "2.7"
dashmap = "6.1" dashmap = "6.1"
url = "2.5"
# https://github.com/teloxide/teloxide/issues/1154 # https://github.com/teloxide/teloxide/issues/1154
# [profile.dev] # [profile.dev]

39
Dockerfile Normal file
View file

@ -0,0 +1,39 @@
## Chef
# FROM clux/muslrust:stable AS chef
FROM rust:alpine AS chef
USER root
RUN apk add --no-cache musl-dev libressl-dev
RUN cargo install cargo-chef
WORKDIR /build
## Planner
FROM chef AS planner
COPY Cargo.toml Cargo.lock ./
COPY ./migration ./migration
COPY ./database ./database
COPY ./youtube ./youtube
COPY src src
RUN cargo chef prepare --recipe-path recipe.json
## Builder
FROM chef AS builder
COPY --from=planner /build/recipe.json recipe.json
# Build dependencies - this is the caching Docker layer!
RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json
# Build application
COPY Cargo.toml Cargo.lock ./
COPY ./migration ./migration
COPY ./database ./database
COPY ./youtube ./youtube
COPY src src
RUN cargo build --release --target x86_64-unknown-linux-musl --bin doggy-watch
## Runtime
FROM alpine:3.20.0 AS runtime
WORKDIR /app
COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/doggy-watch /app/doggy-watch
RUN apk add --no-cache tzdata
ENV TZ=Etc/UTC
ENTRYPOINT [ "./doggy-watch" ]

View file

@ -3,14 +3,58 @@
Telegram бот для предложения YouTube видео на стрим. Telegram бот для предложения YouTube видео на стрим.
Сделан специально для [Doggy Dox](https://www.twitch.tv/doggy_dox). Сделан специально для [Doggy Dox](https://www.twitch.tv/doggy_dox).
## Переменные
`DATABASE_URL=postgres://<username>:<password>@<address>/<database>`
Параметры базы данных
`TOKEN=<bot_token>`
Токен бота. Можно получить у [@botfather](tg://resolve?domain=botfather)
`ADMINISTRATORS=<user_id>[,<user_id>,...]`
ID администраторов, разделяется запятой.
Можно получить у [@getmyid_bot](tg://resolve?domain=getmyid_bot)
`CHANNEL=<chat_id>`
ID канала для проверки подписки.
Можно получить у [@getmyid_bot](tg://resolve?domain=getmyid_bot) переслав ему сообщения из канала.
`CHANNEL_INVITE_HASH=<hash>`
Хэш для инвайт ссылки (необязательно). Хэш можно извлечь из ссылки-приглашения после плюса.
Пример: `https://t.me/+<hash>`
`RUST_LOG=<level>[,target=level,...]`
Журналирование (необязательно).
Типы:
`trace, debug, info, warn, error`
Также можно указать отдельный уровень логирования для отдельных целей.
`TELEGRAM_API_URL=<url>`
Сторонний Telegram Bot API сервер (необязательно).
### Только для Docker
`TZ=<TZ_identifier>`
Необязательно, но рекомендуется, т.к. данные в БД хранятся без часового пояса.
Можно взять из [таблицы с Википедии](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
## Contributing ## Contributing
![Спроси меня о чём угодно!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg) ![Спроси меня о чём угодно!](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)
в в
[![Telegram](https://badgen.net/static/icon/Telegram?icon=telegram&color=cyan&label)](https://t.me/shiroyashik) [![Telegram](https://badgen.net/static/icon/Telegram?icon=telegram&color=cyan&label)](https://t.me/shiroyashik)
или или
![Discord](https://badgen.net/badge/icon/Discord?icon=discord&label) ![Discord](https://badgen.net/badge/icon/Discord?icon=discord&label)
Если у вас есть идем, нашли баг или хотите предложить улучшения Если у вас есть идеи, нашли баг или хотите предложить улучшения:
создавайте [issue](https://github.com/shiroyashik/doggy-watch/issues) создавайте [issue](https://github.com/shiroyashik/doggy-watch/issues)
или свяжитесь со мной напрямую через Discord/Telegram (**@shiroyashik**). или свяжитесь со мной напрямую через Discord/Telegram (**@shiroyashik**).

View file

@ -108,22 +108,22 @@ impl MigrationTrait for Migration {
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Videos // Actions
manager manager
.drop_table(Table::drop().table(Videos::Table).to_owned()) .drop_table(Table::drop().table(Actions::Table).to_owned())
.await?; .await?;
// Requests // Requests
manager manager
.drop_table(Table::drop().table(Requests::Table).to_owned()) .drop_table(Table::drop().table(Requests::Table).to_owned())
.await?; .await?;
// Actions
manager
.drop_table(Table::drop().table(Actions::Table).to_owned())
.await?;
// Archived // Archived
manager manager
.drop_table(Table::drop().table(Archived::Table).to_owned()) .drop_table(Table::drop().table(Archived::Table).to_owned())
.await?; .await?;
// Videos
manager
.drop_table(Table::drop().table(Videos::Table).to_owned())
.await?;
// Moderators // Moderators
manager manager
.drop_table(Table::drop().table(Moderators::Table).to_owned()) .drop_table(Table::drop().table(Moderators::Table).to_owned())

View file

@ -10,10 +10,17 @@ use crate::{check_subscription, markup, notify, AppState, DialogueState, MyDialo
pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> { pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> {
use youtube::*; use youtube::*;
if let Some(text) = msg.clone().text() { if let Some(text) = msg.clone().text() {
if let Some(user) = check_subscription(&bot, &msg.from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await { if let Some(user) = check_subscription(&bot, &msg.clone().from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await {
// Get ready! // Get ready!
if let Some(ytid) = extract_youtube_video_id(text) { if let Some(ytid) = extract_youtube_video_id(text) {
let meta = get_video_metadata(&ytid).await?; let meta = match get_video_metadata(&ytid).await {
Ok(meta) => meta,
Err(err) => {
tracing::error!("Caused an exception in get_video_metadata due: {err:?}");
bot.send_message(msg.chat.id, "Ошибка при получении метаданных видео!").await?;
return Ok(());
},
};
// Post // Post
bot.send_message(msg.chat.id, format!( bot.send_message(msg.chat.id, format!(
"Вы уверены что хотите добавить <b>{}</b>", "Вы уверены что хотите добавить <b>{}</b>",
@ -21,6 +28,7 @@ pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Re
)).parse_mode(ParseMode::Html).reply_markup(markup::inline_yes_or_no()).await?; )).parse_mode(ParseMode::Html).reply_markup(markup::inline_yes_or_no()).await?;
dialogue.update(DialogueState::AcceptVideo { ytid, uid: user.id.0, title: meta.title }).await?; dialogue.update(DialogueState::AcceptVideo { ytid, uid: user.id.0, title: meta.title }).await?;
} else { } else {
tracing::debug!("Not a YouTube video: {:?}", msg);
bot.send_message(msg.chat.id, "Это не похоже на YouTube видео... Долбоёб").await?; bot.send_message(msg.chat.id, "Это не похоже на YouTube видео... Долбоёб").await?;
} }
} else { } else {

View file

@ -1,34 +1,115 @@
use std::sync::Arc; use std::sync::Arc;
use indexmap::IndexMap; use indexmap::IndexMap;
use teloxide::{prelude::*, types::{LinkPreviewOptions, ParseMode}}; use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions, ParseMode}};
use sea_orm::{prelude::*, Order, QueryOrder}; use sea_orm::{prelude::*, Order, QueryOrder};
use database::*; use database::*;
use crate::AppState; use crate::AppState;
pub async fn command(bot: Bot, msg: Message, state: Arc<AppState>) -> anyhow::Result<()> { struct Video {
struct Video {
id: i32, id: i32,
title: String, title: String,
url: String, url: String,
contributors: u64, contributors: u64,
status: String, status: String,
} }
pub async fn command(bot: Bot, msg: Message, state: Arc<AppState>) -> anyhow::Result<()> {
let videos: Vec<(requests::Model, Option<videos::Model>)> = requests::Entity::find() let videos: Vec<(requests::Model, Option<videos::Model>)> = requests::Entity::find()
.find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).all(&state.db).await?; .find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).all(&state.db).await?;
// let videos_len = videos.len();
if !videos.is_empty() { let result = generate_list(videos, &state).await;
match result {
Ok(list) => {
let result = if let Some(list) = list {
list
} else {
"Нет видео для просмотра :(".to_string()
};
let keyboard: Vec<Vec<InlineKeyboardButton>> = vec![
vec![InlineKeyboardButton::callback("Непросмотренные", "list_unviewed")],
];
bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html)
.link_preview_options(LinkPreviewOptions {
is_disabled: true,
url: None,
prefer_small_media: false,
prefer_large_media: false,
show_above_text: false
}).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?;
},
Err(e) => {
tracing::error!("{:?}", e);
bot.send_message(msg.chat.id, "Произошла ошибка!").await?;
},
}
Ok(())
}
pub async fn inline(state: Arc<AppState>, bot: Bot, q: CallbackQuery) -> anyhow::Result<()> {
bot.answer_callback_query(&q.id).await?;
let videos: Vec<(requests::Model, Option<videos::Model>)> = requests::Entity::find()
.find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).filter(requests::Column::ViewedAt.is_null()).all(&state.db).await?;
let result = generate_list(videos, &state).await;
match result {
Ok(list) => {
let result = if let Some(list) = list {
list
} else {
"Нет видео для просмотра :(".to_string()
};
let keyboard: Vec<Vec<InlineKeyboardButton>> = vec![
vec![InlineKeyboardButton::callback("Обновить", "list_unviewed")],
];
if let Some(message) = q.regular_message() {
bot.edit_message_text(message.chat.id, message.id, result).parse_mode(ParseMode::Html)
.link_preview_options(LinkPreviewOptions {
is_disabled: true,
url: None,
prefer_small_media: false,
prefer_large_media: false,
show_above_text: false
}).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?;
} else if let Some(message_id) = q.inline_message_id {
bot.edit_message_text_inline(&message_id, result)
.parse_mode(ParseMode::Html).disable_web_page_preview(true).reply_markup(InlineKeyboardMarkup::new(keyboard)).await?;
} else {
bot.send_message(q.from.id, result).parse_mode(ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(keyboard))
.link_preview_options(LinkPreviewOptions {
is_disabled: true,
url: None,
prefer_small_media: false,
prefer_large_media: false,
show_above_text: false
}).await?;
}
},
Err(e) => {
tracing::error!("{:?}", e);
bot.send_message(q.from.id, "Произошла ошибка!").await?;
},
}
Ok(())
}
async fn generate_list(videos: Vec<(requests::Model, Option<videos::Model>)>, state: &AppState) -> anyhow::Result<Option<String>> {
if videos.is_empty() {
return Ok(None);
}
let mut by_date: IndexMap<Date, Vec<Video>> = IndexMap::new(); let mut by_date: IndexMap<Date, Vec<Video>> = IndexMap::new();
for (request, video) in videos { for (request, video) in videos {
let video = video.unwrap(); let video = video.unwrap();
let creator = if let Some(c) = request.find_related(actions::Entity).order_by(actions::Column::Id, Order::Asc).one(&state.db).await? { let creator = if let Some(c) = request.find_related(actions::Entity).order_by(actions::Column::Id, Order::Asc).one(&state.db).await? {
c c
} else { } else {
let data = format!("Can't find creator for {request:?}"); anyhow::bail!("Can't find creator for {request:?}");
bot.send_message(msg.chat.id, data.clone()).await?;
anyhow::bail!(data);
}; };
let contributors = request.find_related(actions::Entity).count(&state.db).await?; let contributors = request.find_related(actions::Entity).count(&state.db).await?;
@ -76,9 +157,5 @@ pub async fn command(bot: Bot, msg: Message, state: Arc<AppState>) -> anyhow::Re
} }
} }
// result.push_str(&format!("\nВсего: {}", videos_len)); // result.push_str(&format!("\nВсего: {}", videos_len));
bot.send_message(msg.chat.id, result).parse_mode(ParseMode::Html).link_preview_options(LinkPreviewOptions { is_disabled: true, url: None, prefer_small_media: false, prefer_large_media: false, show_above_text: false }).await?; Ok(Some(result))
} else {
bot.send_message(msg.chat.id, "Нет видео для просмотра :(").await?;
}
Ok(())
} }

View file

@ -19,6 +19,7 @@ pub fn schema() -> UpdateHandler<anyhow::Error> {
use dptree::case; use dptree::case;
let moderator_commands = dptree::entry() let moderator_commands = dptree::entry()
.branch(case![Command::Start].endpoint(start::command_mod)) .branch(case![Command::Start].endpoint(start::command_mod))
.branch(case![Command::Help].endpoint(start::command_mod))
.branch(case![Command::List].endpoint(list::command)) .branch(case![Command::List].endpoint(list::command))
.branch(case![Command::Archive].endpoint(archive::command)) .branch(case![Command::Archive].endpoint(archive::command))
.branch(case![Command::Mods].endpoint(moderator::list::command)) .branch(case![Command::Mods].endpoint(moderator::list::command))
@ -73,6 +74,7 @@ pub fn schema() -> UpdateHandler<anyhow::Error> {
InlineCommand::parse(&q.data?) InlineCommand::parse(&q.data?)
})) }))
.branch(case![InlineCommand::Cancel].endpoint(cancel)) .branch(case![InlineCommand::Cancel].endpoint(cancel))
.branch(case![InlineCommand::ListUnviewed].endpoint(list::inline))
.branch(filter(|com: InlineCommand| { .branch(filter(|com: InlineCommand| {
matches!(com, InlineCommand::ArchiveAll | InlineCommand::ArchiveViewed) matches!(com, InlineCommand::ArchiveAll | InlineCommand::ArchiveViewed)
}).endpoint(archive::inline)) }).endpoint(archive::inline))

View file

@ -29,7 +29,7 @@ pub async fn inline(bot: Bot, q: CallbackQuery, state: Arc<AppState>, uid: Strin
if let Some(data) = q.clone().data { if let Some(data) = q.clone().data {
let text= if &data == "yes" { let text= if &data == "yes" {
if let Ok(uid) = uid.parse::<u64>() { if let Ok(uid) = uid.parse::<u64>() {
if Entity::delete_by_id(uid as i32).exec(&state.db).await?.rows_affected != 0 { if Entity::delete_by_id(uid as i64).exec(&state.db).await?.rows_affected != 0 {
"Модератор удалён!" "Модератор удалён!"
} else { } else {
"Произошла ошибка!\nПо всей видимости такого модератора не существует." "Произошла ошибка!\nПо всей видимости такого модератора не существует."

View file

@ -8,7 +8,7 @@ use crate::AppState;
/// Invert notify status for moderator /// Invert notify status for moderator
pub async fn command(bot: Bot, msg: Message, uid: UserId, state: Arc<AppState>) -> anyhow::Result<()> { pub async fn command(bot: Bot, msg: Message, uid: UserId, state: Arc<AppState>) -> anyhow::Result<()> {
let text = if let Some(moder) = moderators::Entity::find_by_id(uid.0 as i32).one(&state.db).await? { let text = if let Some(moder) = moderators::Entity::find_by_id(uid.0 as i64).one(&state.db).await? {
let moder = match moder.notify { let moder = match moder.notify {
true => { true => {
let mut moder = moder.into_active_model(); let mut moder = moder.into_active_model();

View file

@ -6,6 +6,7 @@ pub enum InlineCommand {
Unview(i32), Unview(i32),
ArchiveViewed, ArchiveViewed,
ArchiveAll, ArchiveAll,
ListUnviewed,
Cancel, Cancel,
} }
@ -19,6 +20,7 @@ impl InlineCommand {
"unview" => Self::Unview(parts.next()?.parse().ok()?), "unview" => Self::Unview(parts.next()?.parse().ok()?),
"archive_viewed" => Self::ArchiveViewed, "archive_viewed" => Self::ArchiveViewed,
"archive_all" => Self::ArchiveAll, "archive_all" => Self::ArchiveAll,
"list_unviewed" => Self::ListUnviewed,
"cancel" => Self::Cancel, "cancel" => Self::Cancel,
_ => return None, _ => return None,
}) })

View file

@ -2,6 +2,7 @@ use std::{env::var, sync::Arc, time::Duration};
use dashmap::DashMap; use dashmap::DashMap;
use database::moderators; use database::moderators;
use migration::{Migrator, MigratorTrait};
use sea_orm::{prelude::*, sea_query::OnConflict, ConnectOptions, Database, Set}; use sea_orm::{prelude::*, sea_query::OnConflict, ConnectOptions, Database, Set};
use teloxide::{ use teloxide::{
dispatching::dialogue::InMemStorage, dispatching::dialogue::InMemStorage,
@ -16,6 +17,7 @@ mod markup;
mod inline; mod inline;
pub use inline::InlineCommand; pub use inline::InlineCommand;
use url::Url;
pub const COOLDOWN_DURATION: Duration = Duration::from_secs(10); pub const COOLDOWN_DURATION: Duration = Duration::from_secs(10);
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -27,6 +29,12 @@ lazy_static! {
pub static ref TOKEN: String = { pub static ref TOKEN: String = {
var("TOKEN").expect("TOKEN env not set.") var("TOKEN").expect("TOKEN env not set.")
}; };
pub static ref TELEGRAM_API_URL: Url = {
match var("TELEGRAM_API_URL") {
Ok(url) => url.parse().expect("Can't parse TELEGRAM_API_URL"),
Err(_) => teloxide::net::TELEGRAM_API_URL.parse().expect("Failed to parse default Telegram bot API url")
}
};
pub static ref DATABASE_URL: String = { pub static ref DATABASE_URL: String = {
var("DATABASE_URL").expect("DATABASE_URL env not set.") var("DATABASE_URL").expect("DATABASE_URL env not set.")
}; };
@ -59,13 +67,16 @@ async fn main() -> anyhow::Result<()> {
})); }));
tracing::info!("Doggy-Watch v{VERSION}"); tracing::info!("Doggy-Watch v{VERSION}");
tracing::info!("{:?}", *ADMINISTRATORS); tracing::info!("admins: {:?} tg api: {}", *ADMINISTRATORS, TELEGRAM_API_URL.as_str());
let bot = Bot::new(&*TOKEN); let bot = Bot::new(&*TOKEN).set_api_url(TELEGRAM_API_URL.clone());
let mut opt = ConnectOptions::new(&*DATABASE_URL); let mut opt = ConnectOptions::new(&*DATABASE_URL);
opt.sqlx_logging_level(tracing::log::LevelFilter::Trace); opt.sqlx_logging_level(tracing::log::LevelFilter::Trace);
let db: DatabaseConnection = Database::connect(opt).await?; let db: DatabaseConnection = Database::connect(opt).await?;
// applying migrations
Migrator::up(&db, None).await?;
// add administrators to db // add administrators to db
{ {
let admins: Vec<moderators::ActiveModel> = ADMINISTRATORS.iter().map(|&x| { let admins: Vec<moderators::ActiveModel> = ADMINISTRATORS.iter().map(|&x| {
@ -89,7 +100,7 @@ async fn main() -> anyhow::Result<()> {
// Pass the shared state to the handler as a dependency. // Pass the shared state to the handler as a dependency.
.dependencies(dptree::deps![state, InMemStorage::<DialogueState>::new()]) .dependencies(dptree::deps![state, InMemStorage::<DialogueState>::new()])
.default_handler(|upd| async move { .default_handler(|upd| async move {
tracing::warn!("Unhandled update: {:?}", upd); tracing::debug!("Unhandled update: {:?}", upd);
}) })
.enable_ctrlc_handler() .enable_ctrlc_handler()
.build() .build()
@ -116,6 +127,8 @@ pub enum DialogueState {
enum Command { enum Command {
#[command(description = "запустить бота и/или вывести этот текст.")] #[command(description = "запустить бота и/или вывести этот текст.")]
Start, Start,
#[command(description = "вывести этот текст.")]
Help,
#[command(description = "вывести список.")] #[command(description = "вывести список.")]
List, List,
#[command(description = "действия с архивом.")] #[command(description = "действия с архивом.")]
@ -180,7 +193,7 @@ impl AppState {
async fn check_rights(&self, uid: &UserId) -> anyhow::Result<Rights> { async fn check_rights(&self, uid: &UserId) -> anyhow::Result<Rights> {
use database::moderators::Entity as Moderators; use database::moderators::Entity as Moderators;
Ok(if let Some(moder) = Moderators::find_by_id(uid.0 as i32).one(&self.db).await? { Ok(if let Some(moder) = Moderators::find_by_id(uid.0 as i64).one(&self.db).await? {
Rights::Moderator { can_add_mods: moder.can_add_mods } Rights::Moderator { can_add_mods: moder.can_add_mods }
} else { } else {
Rights::None Rights::None