Compare commits

..

No commits in common. "master" and "v0.2.1" have entirely different histories.

12 changed files with 282 additions and 468 deletions

493
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "doggy-watch"
authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
version = "0.2.2"
version = "0.2.0"
edition = "2021"
publish = false
@ -26,7 +26,6 @@ tracing-panic = "0.1"
lazy_static = "1.5"
indexmap = "2.7"
dashmap = "6.1"
url = "2.5"
# https://github.com/teloxide/teloxide/issues/1154
# [profile.dev]

View file

@ -1,6 +1,6 @@
## Chef
# FROM clux/muslrust:stable AS chef
FROM rust:alpine AS chef
FROM rust:1.84.0-alpine3.20 AS chef
USER root
RUN apk add --no-cache musl-dev libressl-dev
RUN cargo install cargo-chef

View file

@ -35,10 +35,6 @@ ID канала для проверки подписки.
`trace, debug, info, warn, error`
Также можно указать отдельный уровень логирования для отдельных целей.
`TELEGRAM_API_URL=<url>`
Сторонний Telegram Bot API сервер (необязательно).
### Только для Docker
`TZ=<TZ_identifier>`

View file

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

View file

@ -10,17 +10,10 @@ use crate::{check_subscription, markup, notify, AppState, DialogueState, MyDialo
pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Result<()> {
use youtube::*;
if let Some(text) = msg.clone().text() {
if let Some(user) = check_subscription(&bot, &msg.clone().from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await {
if let Some(user) = check_subscription(&bot, &msg.from.ok_or(anyhow::anyhow!("Message not from user!"))?.id).await {
// Get ready!
if let Some(ytid) = extract_youtube_video_id(text) {
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(());
},
};
let meta = get_video_metadata(&ytid).await?;
// Post
bot.send_message(msg.chat.id, format!(
"Вы уверены что хотите добавить <b>{}</b>",
@ -28,7 +21,6 @@ pub async fn message(bot: Bot, msg: Message, dialogue: MyDialogue) -> anyhow::Re
)).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?;
} else {
tracing::debug!("Not a YouTube video: {:?}", msg);
bot.send_message(msg.chat.id, "Это не похоже на YouTube видео... Долбоёб").await?;
}
} else {

View file

@ -1,13 +1,14 @@
use std::sync::Arc;
use indexmap::IndexMap;
use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions, ParseMode}};
use teloxide::{prelude::*, types::{LinkPreviewOptions, ParseMode}};
use sea_orm::{prelude::*, Order, QueryOrder};
use database::*;
use crate::AppState;
pub async fn command(bot: Bot, msg: Message, state: Arc<AppState>) -> anyhow::Result<()> {
struct Video {
id: i32,
title: String,
@ -15,101 +16,19 @@ struct Video {
contributors: u64,
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()
.find_also_related(videos::Entity).filter(videos::Column::Banned.eq(false)).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")],
];
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 videos_len = videos.len();
if !videos.is_empty() {
let mut by_date: IndexMap<Date, Vec<Video>> = IndexMap::new();
for (request, video) in videos {
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? {
c
} else {
anyhow::bail!("Can't find creator for {request:?}");
let data = format!("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?;
@ -157,5 +76,9 @@ async fn generate_list(videos: Vec<(requests::Model, Option<videos::Model>)>, st
}
}
// result.push_str(&format!("\nВсего: {}", videos_len));
Ok(Some(result))
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?;
} else {
bot.send_message(msg.chat.id, "Нет видео для просмотра :(").await?;
}
Ok(())
}

View file

@ -19,7 +19,6 @@ pub fn schema() -> UpdateHandler<anyhow::Error> {
use dptree::case;
let moderator_commands = dptree::entry()
.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::Archive].endpoint(archive::command))
.branch(case![Command::Mods].endpoint(moderator::list::command))
@ -74,7 +73,6 @@ pub fn schema() -> UpdateHandler<anyhow::Error> {
InlineCommand::parse(&q.data?)
}))
.branch(case![InlineCommand::Cancel].endpoint(cancel))
.branch(case![InlineCommand::ListUnviewed].endpoint(list::inline))
.branch(filter(|com: InlineCommand| {
matches!(com, InlineCommand::ArchiveAll | InlineCommand::ArchiveViewed)
}).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 {
let text= if &data == "yes" {
if let Ok(uid) = uid.parse::<u64>() {
if Entity::delete_by_id(uid as i64).exec(&state.db).await?.rows_affected != 0 {
if Entity::delete_by_id(uid as i32).exec(&state.db).await?.rows_affected != 0 {
"Модератор удалён!"
} else {
"Произошла ошибка!\nПо всей видимости такого модератора не существует."

View file

@ -8,7 +8,7 @@ use crate::AppState;
/// Invert notify status for moderator
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 i64).one(&state.db).await? {
let text = if let Some(moder) = moderators::Entity::find_by_id(uid.0 as i32).one(&state.db).await? {
let moder = match moder.notify {
true => {
let mut moder = moder.into_active_model();

View file

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

View file

@ -17,7 +17,6 @@ mod markup;
mod inline;
pub use inline::InlineCommand;
use url::Url;
pub const COOLDOWN_DURATION: Duration = Duration::from_secs(10);
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -29,12 +28,6 @@ lazy_static! {
pub static ref TOKEN: String = {
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 = {
var("DATABASE_URL").expect("DATABASE_URL env not set.")
};
@ -67,8 +60,8 @@ async fn main() -> anyhow::Result<()> {
}));
tracing::info!("Doggy-Watch v{VERSION}");
tracing::info!("admins: {:?} tg api: {}", *ADMINISTRATORS, TELEGRAM_API_URL.as_str());
let bot = Bot::new(&*TOKEN).set_api_url(TELEGRAM_API_URL.clone());
tracing::info!("{:?}", *ADMINISTRATORS);
let bot = Bot::new(&*TOKEN);
let mut opt = ConnectOptions::new(&*DATABASE_URL);
opt.sqlx_logging_level(tracing::log::LevelFilter::Trace);
@ -100,7 +93,7 @@ async fn main() -> anyhow::Result<()> {
// Pass the shared state to the handler as a dependency.
.dependencies(dptree::deps![state, InMemStorage::<DialogueState>::new()])
.default_handler(|upd| async move {
tracing::debug!("Unhandled update: {:?}", upd);
tracing::warn!("Unhandled update: {:?}", upd);
})
.enable_ctrlc_handler()
.build()
@ -127,8 +120,6 @@ pub enum DialogueState {
enum Command {
#[command(description = "запустить бота и/или вывести этот текст.")]
Start,
#[command(description = "вывести этот текст.")]
Help,
#[command(description = "вывести список.")]
List,
#[command(description = "действия с архивом.")]
@ -193,7 +184,7 @@ impl AppState {
async fn check_rights(&self, uid: &UserId) -> anyhow::Result<Rights> {
use database::moderators::Entity as Moderators;
Ok(if let Some(moder) = Moderators::find_by_id(uid.0 as i64).one(&self.db).await? {
Ok(if let Some(moder) = Moderators::find_by_id(uid.0 as i32).one(&self.db).await? {
Rights::Moderator { can_add_mods: moder.can_add_mods }
} else {
Rights::None