diff --git a/Cargo.lock b/Cargo.lock
index ecf2071..940476d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,8 @@ dependencies = [
"dotenvy",
"log",
"pretty_env_logger",
+ "rand",
+ "regex",
"teloxide",
"tokio",
]
@@ -113,6 +115,12 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
[[package]]
name = "bytes"
version = "1.8.0"
@@ -1113,6 +1121,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
[[package]]
name = "pretty_env_logger"
version = "0.5.0"
@@ -1165,6 +1182,36 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
[[package]]
name = "rc-box"
version = "1.2.0"
@@ -2147,6 +2194,27 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
[[package]]
name = "zerofrom"
version = "0.1.4"
diff --git a/Cargo.toml b/Cargo.toml
index 4a54903..96e1273 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,3 +11,10 @@ tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
diesel = { version = "2.2.4", features = ["sqlite"] }
chrono = "0.4.38"
dotenvy = "0.15.7"
+rand = "0.8.5"
+regex = "1.11.1"
+
+[profile.release]
+debug = false
+lto = true
+codegen-units = 1
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8f8d1ab
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# ApollousaBot
+
+A Telegram bot to remind members in Posthuman-Community to take breaks and exercise.
\ No newline at end of file
diff --git a/db_helper.py b/db_helper.py
new file mode 100644
index 0000000..932b7ea
--- /dev/null
+++ b/db_helper.py
@@ -0,0 +1,100 @@
+import sqlite3
+
+conn = sqlite3.connect("database.db")
+cursor = conn.cursor()
+
+cursor.execute("""
+CREATE TABLE IF NOT EXISTS quotes (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT,
+ msg TEXT NOT NULL
+)
+""")
+
+messages = [
+ "自然が呼んでいる、外に出て歩こう!🌿",
+ "椅子は動けないけど、あなたは動ける!立ってストレッチ!🚀",
+ "宠猫喊你起身活动,来吧喵 🐱",
+ "Your pet cat says it’s time to move, meow! 🐱",
+ "ネコが「動く時間だよ」、ニャー!🐱",
+ "锻炼身体,心情也会变好哦! 🌞",
+ "Exercise your body, uplift your mood! 🌈",
+ "体を動かして、気分もスッキリ!🌞",
+ "别只是动动手指,起来扭扭腰吧! 💃",
+ "Don't only move your fingers, get up and wiggle your waist! 💃",
+ "指だけ動かさないで、腰を振ろう!💃",
+ "起身运动一下,犒劳自己一片小饼干! 🍪",
+ "Move around and reward yourself with a cookie! 🍪",
+ "動いたら、クッキーを自分にご褒美!🍪",
+ "再多努力五分钟,然后起来活动一下吧! ✊",
+ "Work hard for another five minutes, then get up and move! ✊",
+ "あと 5 分頑張って、そして動こう!✊",
+ "用挤牙膏的劲去运动,身体会感谢你! 🏋️",
+ "Squeeze in some exercise like you squeeze toothpaste, your body will thank you! 🏋️",
+ "歯磨き粉を絞るように運動して、体が感謝してくれるよ!🏋️",
+ "会健身的椅子才是好椅子!让你的动起来! 🪑",
+ "A good chair gets some exercise too! Let's move yours! 🪑",
+ "健康な椅子は運動する椅子だ!動かそう!🪑",
+ "保持健康,今天走一万步啦! 💪",
+ "Keep fit, hit 10,000 steps today! 💪",
+ "健康を保つために、今日は 1 万歩歩こう!👟",
+ "左脚,右脚,一步两步,动起来! 🚶",
+ "Left foot, right foot, one two step, let's move! 🚶♀️",
+ "左右の足、一歩二歩、動こう!🚶",
+ "玩个小游戏,站起来跳十次! 🎮",
+ "Play a little game, jump up ten times! 🎮",
+ "ゲームしよう、10 回ジャンプ!🎮",
+ "起来动一动,心情棒棒哒! 🌟",
+ "Get up and move, feel super awesome! 🌟",
+ "立ち上がって動くと、気分が最高!✨",
+ "给自己来个 “动感时刻” 吧! 🕺",
+ "自分に「動の時間」を贈ろう!🕺",
+ "收拾收拾,活动一下筋骨! 🚶",
+ "Tidy up and stretch your muscles! 🚶♂️",
+ "身の回りを整理して、筋肉を伸ばそう!🧹",
+ "休息片刻,能量满满! ✨",
+ "Take a break, feel energized! ✨",
+ "休憩して、エネルギーを補充しよう!🔋",
+ "运动一下,神清气爽! 🌿",
+ "A quick workout, clear mind! 🌿",
+ "少し運動して、頭をリフレッシュ!🍃",
+ "每天一小步,健康一大步! 🤸",
+ "A little step each day, a big leap to health! 🤸",
+ "毎日少し動いて、大きな健康を得よう!🏃♂️",
+ "起身喝口水,再散步五分钟! 🚶",
+ "Stand up, have a sip of water, and take a five-minute walk! 🚶♀️",
+ "立ち上がって水を飲んで、5 分歩こう!🚶♂️",
+ "时间到!该起来动动啦! ⏰",
+ "Time's up! Get up and move around! ⏰",
+ "時間だよ!立ち上がって動こう!⏳",
+ "只需几步,就能解锁健康! 🚶",
+ "Just a few steps to unlock health! 🚶♀️",
+ "健康へのカギは、数歩歩くだけ!🔑",
+ "活动一下,用笑容迎接每一天! 😊",
+ "Move around, greet each day with a smile! 😊",
+ "動いて、毎日を笑顔で迎えよう!😊",
+ "别让自己 “长在” 椅子上,起来动动吧! 🌟",
+ "椅子に「根を張らない」ように、立ち上がって動こう!🌿",
+ "让你的小腿跑一跑,充电完毕!⚡",
+ "Let your legs run a bit, recharge complete! ⚡",
+ "少し足を動かして、充電完了!⚡",
+ "记得活动哦!短暂休息,长久健康! 🚶",
+ "Don't forget to move! Short breaks, long health! 🚶♂️",
+ "動くのを忘れないで!短い休憩、長い健康!🚶♀️",
+ "起身摇摆,工作更自在! 💃",
+ "Stand up and sway, work feels like play! 💃",
+ "立ち上がって揺れよう、仕事が楽しくなるよ!💃 ",
+ "久坐不好,起来活动活动吧! 🚶",
+ "记得起身活动活动,松松背拉拉筋。😊",
+]
+
+
+for idx, msg in enumerate(messages, start=1):
+ cursor.execute(
+ """
+ INSERT INTO quotes (ID, msg) VALUES (?, ?)
+ """,
+ (idx, msg),
+ )
+
+conn.commit()
+conn.close()
diff --git a/src/bot/handler.rs b/src/bot/handler.rs
index 4a9775a..c11cc49 100644
--- a/src/bot/handler.rs
+++ b/src/bot/handler.rs
@@ -2,6 +2,7 @@ use crate::{
bot::commands::Command,
db::action::{clear_reminder_time, set_reminder_time, set_user_timezone},
};
+use regex::Regex;
use teloxide::{adaptors::DefaultParseMode, prelude::*, utils::html};
use chrono::naive::NaiveTime;
@@ -20,7 +21,7 @@ pub async fn reply(bot: Bot, msg: Message, command: Command) -> ResponseResult<(
Command::Help => {
bot.send_message(
msg.chat.id,
- format!("Hi, {mentioned_user}, Welcome to the Exercise Reminder Bot! 😉\n\nPlease use /settime HH:MM to set the reminder time.\n\nAnd use /settimezone +01:00 (according to your location) to accurate the reminder.\n\nIf you are not sure your timezone, please check this page.",
+ format!("Hi, {mentioned_user}, Welcome to the Exercise Reminder Bot! 😉\n\nPlease use /settime HH:MM to set the reminder time. And use /settimezone +01:00 (according to your location) to accurate the reminder.\n\nIf you are not sure your timezone, please check this page.\n\nPlease settime first before settimezone.",
),
)
.await?;
@@ -54,14 +55,28 @@ pub async fn reply(bot: Bot, msg: Message, command: Command) -> ResponseResult<(
}
}
Command::SetTimezone(timezone) => {
- set_user_timezone(conn, user_id, msg.chat.id, timezone.as_str());
- bot.send_message(
- msg.chat.id,
- format!(
- "Hi, {mentioned_user}, your timezone is set to UTC{timezone}."
- ),
- )
- .await?;
+ let tz_pattern =
+ Regex::new(r"^(\+|-)\d{2}:\d{2}$").expect("Failed to initialize regex pattern");
+
+ match tz_pattern.is_match(&timezone) {
+ true => {
+ set_user_timezone(conn, user_id, msg.chat.id, &username, timezone.as_str());
+ bot.send_message(
+ msg.chat.id,
+ format!(
+ "Hi, {mentioned_user}, your timezone is set to UTC{timezone}."
+ ),
+ )
+ .await?;
+ }
+ false => {
+ bot.send_message(
+ msg.chat.id,
+ "Invalid timezone format. Please use the format ±HH:MM.",
+ )
+ .await?;
+ }
+ }
}
Command::Stop => {
clear_reminder_time(conn, msg.chat.id, user_id);
diff --git a/src/db/action.rs b/src/db/action.rs
index 483990f..359ac2a 100644
--- a/src/db/action.rs
+++ b/src/db/action.rs
@@ -1,3 +1,4 @@
+use crate::db::schema::quotes::dsl::*;
use crate::db::schema::users::dsl::*;
use diesel::prelude::*;
use teloxide::types::{ChatId, UserId};
@@ -11,6 +12,10 @@ pub fn set_reminder_time(
_username: &str,
time: &str,
) {
+ println!(
+ "set_reminder_time -> UserId: {}, ChatId: {}, Username: {}, Time: {}",
+ _user_id.0, _chat_id.0, _username, time
+ );
diesel::insert_into(users)
.values((
chat_id.eq(_chat_id.0),
@@ -18,7 +23,7 @@ pub fn set_reminder_time(
username.eq(_username),
reminder_time.eq(time),
))
- .on_conflict(chat_id)
+ .on_conflict((chat_id, user_id))
.do_update()
.set(reminder_time.eq(time))
.execute(conn)
@@ -29,22 +34,46 @@ pub fn set_user_timezone(
conn: &mut SqliteConnection,
_user_id: UserId,
_chat_id: ChatId,
+ _username: &str,
_user_timezone: &str,
) {
println!(
- "UserId: {}, ChatId: {}, UserTimezone: {}",
- _user_id.0, _chat_id.0, _user_timezone
+ "set_user_timezone -> UserId: {}, ChatId: {}, Username: {}, UserTimezone: {}",
+ _user_id.0, _chat_id.0, _username, _user_timezone
);
- diesel::update(
- users.filter(
+ let user_exists = users
+ .filter(
chat_id
.eq(_chat_id.0)
.and(user_id.eq(i64::try_from(_user_id.0).unwrap())),
- ),
- )
- .set(tz_offset.eq(_user_timezone))
- .execute(conn)
- .expect("Error update user timezone");
+ )
+ .select((chat_id, user_id))
+ .first::<(i64, i64)>(conn)
+ .optional()
+ .expect("Error checking if user exists");
+
+ if user_exists.is_some() {
+ diesel::update(
+ users.filter(
+ chat_id
+ .eq(_chat_id.0)
+ .and(user_id.eq(i64::try_from(_user_id.0).unwrap())),
+ ),
+ )
+ .set(tz_offset.eq(_user_timezone))
+ .execute(conn)
+ .expect("Error updating user timezone");
+ } else {
+ diesel::insert_into(users)
+ .values((
+ chat_id.eq(_chat_id.0),
+ user_id.eq(i64::try_from(_user_id.0).unwrap()),
+ username.eq(_username),
+ tz_offset.eq(_user_timezone),
+ ))
+ .execute(conn)
+ .expect("Error inserting new user with timezone");
+ }
}
pub fn clear_reminder_time(conn: &mut SqliteConnection, _chat_id: ChatId, _user_id: UserId) {
@@ -60,5 +89,17 @@ pub fn clear_reminder_time(conn: &mut SqliteConnection, _chat_id: ChatId, _user_
}
pub fn get_user_reminders(conn: &mut SqliteConnection) -> Vec {
- users.load::(conn).expect("Error loading user")
+ // Only work for group members
+ users
+ .select((chat_id, user_id, username, reminder_time, tz_offset))
+ .filter(chat_id.ne(user_id))
+ .load::(conn)
+ .expect("Error loading user")
+}
+
+pub fn get_quotes(conn: &mut SqliteConnection) -> Vec {
+ quotes
+ .select(msg)
+ .load::(conn)
+ .expect("Error loading Quotes")
}
diff --git a/src/db/model.rs b/src/db/model.rs
index 4efc291..9a87d0e 100644
--- a/src/db/model.rs
+++ b/src/db/model.rs
@@ -1,13 +1,23 @@
-use crate::db::schema::users;
+use crate::db::schema::{quotes, users};
use diesel::prelude::*;
-#[derive(Queryable, Selectable)]
+#[derive(Debug, Queryable, Selectable, QueryableByName, Insertable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Users {
pub chat_id: i64,
pub user_id: i64,
pub username: String,
- pub reminder_time: String,
+ pub reminder_time: Option,
pub tz_offset: Option,
}
+
+#[derive(Queryable, Selectable)]
+#[diesel(table_name = quotes)]
+#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
+pub struct Quotes {
+ #[allow(dead_code)]
+ pub id: i32,
+ #[allow(dead_code)]
+ pub msg: String,
+}
diff --git a/src/db/schema.rs b/src/db/schema.rs
index f703e6f..4e044ad 100644
--- a/src/db/schema.rs
+++ b/src/db/schema.rs
@@ -1,11 +1,23 @@
// @generated automatically by Diesel CLI.
diesel::table! {
- users (chat_id) {
+ quotes (id) {
+ id -> Integer,
+ msg -> Text,
+ }
+}
+
+diesel::table! {
+ users (chat_id, user_id) {
chat_id -> BigInt,
user_id -> BigInt,
username -> Text,
- reminder_time -> Text,
tz_offset -> Nullable,
+ reminder_time -> Nullable,
}
}
+
+diesel::allow_tables_to_appear_in_same_query!(
+ quotes,
+ users,
+);
diff --git a/src/scheduler/reminder.rs b/src/scheduler/reminder.rs
index 2d65c0b..88e5242 100644
--- a/src/scheduler/reminder.rs
+++ b/src/scheduler/reminder.rs
@@ -1,7 +1,8 @@
use std::sync::Arc;
use crate::db::action as operator;
-use crate::utils::TimezoneOffest;
+use crate::db::model::Users;
+use crate::utils::{get_random_quote, TimezoneOffest};
use chrono::{FixedOffset, Utc};
use teloxide::adaptors::DefaultParseMode;
use teloxide::prelude::*;
@@ -12,32 +13,52 @@ use crate::db::establish_connection;
type Bot = DefaultParseMode;
+async fn handle_user_reminder(
+ user: &Users,
+ bot: &Arc,
+ selected_quote: &str,
+) -> Result<(), Box> {
+ let tzoffset = user.tz_offset.as_ref().ok_or(format!(
+ "User {} does not have a timezone offset set.",
+ user.username
+ ))?;
+ let user_timezone = tzoffset
+ .parse::()
+ .map_err(|e| format!("Failed to parse timezone offset: {}", e))?;
+ let duration_secs = user_timezone.to_duration();
+ let user_utc_time = FixedOffset::east_opt(duration_secs).map_or(
+ "Invalid timezone offset".to_string(),
+ |offset| {
+ Utc::now()
+ .with_timezone(&offset)
+ .format("%H:%M")
+ .to_string()
+ },
+ );
+
+ if let Some(reminder) = &user.reminder_time {
+ if user_utc_time == *reminder {
+ let _user_id = UserId(u64::try_from(user.user_id)?);
+ let mentioned_user = html::user_mention(_user_id, &user.username);
+ let notification = format!("{mentioned_user} {selected_quote}");
+ bot.send_message(ChatId(user.chat_id), notification).await?;
+ }
+ }
+
+ Ok(())
+}
+
pub async fn schedule_reminders(bot: &Arc) {
let conn = &mut establish_connection();
let users_to_remind = operator::get_user_reminders(conn);
+ let all_quotes = operator::get_quotes(conn);
+ let selected_quote =
+ get_random_quote(&all_quotes).unwrap_or("记得起身活动活动,松松背拉拉筋。");
+
for user in users_to_remind {
- if let Some(tzoffset) = user.tz_offset.as_ref() {
- let user_timezone: TimezoneOffest = tzoffset.parse().unwrap();
-
- let duration_secs = user_timezone.to_duration();
- let user_utc_time = Utc::now()
- .with_timezone(&FixedOffset::east_opt(duration_secs).unwrap())
- .format("%H:%M")
- .to_string();
-
- // println!("User UTC time: {}", user_utc_time);
-
- if user_utc_time == user.reminder_time {
- // println!("{}: {}", user.reminder_time, user.username);
- let _user_id = UserId(u64::try_from(user.user_id).unwrap());
- let _username = user.username;
- let mentioned_user = html::user_mention(_user_id, &_username);
- let notification = format!("{mentioned_user},记得起身活动活动,松松背拉拉筋。");
- bot.send_message(ChatId(user.chat_id), notification)
- .await
- .unwrap();
- }
+ if let Err(e) = handle_user_reminder(&user, bot, selected_quote).await {
+ eprintln!("Error handling reminder for user {}: {}", user.username, e);
}
}
}
diff --git a/src/utils.rs b/src/utils.rs
index 70c203e..dc2182d 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,3 +1,5 @@
+use rand::Rng;
+
#[derive(Debug, PartialEq)]
pub struct TimezoneOffest {
offset_hours: i32,
@@ -47,6 +49,19 @@ impl TimezoneOffest {
}
}
+pub fn get_random_quote(quotes: &[String]) -> Option<&str> {
+ if !quotes.is_empty() {
+ let length = quotes.len();
+ let mut rng = rand::thread_rng();
+ let random_index = rng.gen_range(0..length);
+ let random_quote = "es[random_index];
+
+ Some(random_quote)
+ } else {
+ None
+ }
+}
+
#[cfg(test)]
mod tests {
@@ -87,4 +102,25 @@ mod tests {
let ew_offset = Utc::now().with_timezone(&FixedOffset::east_opt(duration).unwrap());
println!("UTC+8:00 -> {}", ew_offset);
}
+
+ #[test]
+ fn test_get_random_quote() {
+ let quotes = vec![
+ String::from("wwww"),
+ String::from("vvvv"),
+ String::from("xxxx"),
+ ];
+
+ let random_quote = get_random_quote("es);
+ assert!(random_quote.is_some());
+
+ if let Some(quote) = random_quote {
+ println!("{quote}");
+ }
+
+ let empty_quotes: Vec = vec![];
+
+ let random_quote = get_random_quote(&empty_quotes);
+ assert!(random_quote.is_none());
+ }
}