Skip to content

Commit

Permalink
Announcements
Browse files Browse the repository at this point in the history
  • Loading branch information
Leonid Kozarin committed Sep 19, 2024
1 parent 95e6760 commit cd083df
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ GROWTH_DOD_BONUS_MAX=5
NEWCOMERS_GRACE_DAYS=7
TOP_LIMIT=10

# Perks
HELP_PUSSIES_COEF=0.01
LOAN_PAYOUT_COEF=0.1

Expand All @@ -43,5 +44,10 @@ LOAN_PAYOUT_COEF=0.1
DOD_SELECTION_MODE=EXCLUSION
DOD_RICH_EXCLUSION_RATIO=0.1

# Announcements are displayed at the end of the Dick of the Day message, not more than a specified amount of times.
ANNOUNCEMENT_MAX_SHOWS=5
#ANNOUNCEMENT_EN=
#ANNOUNCEMENT_RU=

# to enable Webhook Mode, set to a correct URL, proxied by a reverse proxy server
#WEBHOOK_URL=https://your.domain/DickGrowerBot/webhook
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ derive_more = { version = "1.0.0-beta.6", features = ["display", "error", "const
num-traits = "0.2.18"
downcast-rs = "1.2.0"
flurry = "0.5.1"
sha2 = "0.10.8"

[dev-dependencies]
testcontainers = "0.15.0"
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ ARG HELP_PUSSIES_COEF
ARG LOAN_PAYOUT_COEF
ARG DOD_SELECTION_MODE
ARG DOD_RICH_EXCLUSION_RATIO
ARG ANNOUNCEMENT_MAX_SHOWS
ARG ANNOUNCEMENT_EN
ARG ANNOUNCEMENT_RU
ENTRYPOINT [ "/usr/local/bin/dickGrowerBot" ]

LABEL org.opencontainers.image.source=https://github.com/kozalosev/DickGrowerBot
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ services:
- LOAN_PAYOUT_COEF
- DOD_SELECTION_MODE
- DOD_RICH_EXCLUSION_RATIO
- ANNOUNCEMENT_MAX_SHOWS
- ANNOUNCEMENT_EN
- ANNOUNCEMENT_RU
expose:
- 8080
networks:
Expand Down
20 changes: 20 additions & 0 deletions migrations/19_create-announcements-table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
DO $$ BEGIN
CREATE TYPE language_code AS ENUM (
'en',
'ru'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

CREATE TABLE IF NOT EXISTS Announcements (
chat_id bigint REFERENCES Chats(id),
language language_code,
hash bytea NOT NULL,
times_shown smallint NOT NULL CHECK ( times_shown >= 0 ),

PRIMARY KEY (chat_id, language)
);

COMMENT ON TABLE Announcements IS 'A table to keep track on the amount of times some announcement was shown in each group chat';
COMMENT ON COLUMN Announcements.hash IS 'The SHA-256 hash of an announcement string set by application properties';
53 changes: 52 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Display;
use std::ops::Not;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use anyhow::anyhow;
use reqwest::Url;
use sha2::{Digest, Sha256};
use sha2::digest::core_api::CoreWrapper;
use teloxide::types::Me;
use crate::domain::Ratio;
use crate::domain::{LanguageCode, Ratio, SupportedLanguage};
use crate::domain::SupportedLanguage::{EN, RU};
use crate::handlers::perks::HelpPussiesPerk;
use crate::handlers::utils::Incrementor;
use crate::help;
Expand All @@ -20,6 +24,7 @@ pub struct AppConfig {
pub top_limit: u16,
pub loan_payout_ratio: f32,
pub dod_rich_exclusion_ratio: Option<Ratio>,
pub announcements: AnnouncementsConfig,
pub command_toggles: CachedEnvToggles,
}

Expand Down Expand Up @@ -77,6 +82,9 @@ impl AppConfig {
let callback_locks = get_env_value_or_default("PVP_CALLBACK_LOCKS_ENABLED", true);
let show_stats = get_env_value_or_default("PVP_STATS_SHOW", true);
let show_stats_notice = get_env_value_or_default("PVP_STATS_SHOW_NOTICE", true);
let announcement_max_shows = get_optional_env_value("ANNOUNCEMENT_MAX_SHOWS");
let announcement_en = get_optional_env_value("ANNOUNCEMENT_EN");
let announcement_ru = get_optional_env_value("ANNOUNCEMENT_RU");
Self {
features: FeatureToggles {
chats_merging,
Expand All @@ -92,6 +100,16 @@ impl AppConfig {
top_limit,
loan_payout_ratio,
dod_rich_exclusion_ratio,
announcements: AnnouncementsConfig {
max_shows: announcement_max_shows,
announcements: [
(EN, announcement_en),
(RU, announcement_ru),
].map(|(lc, text)| (lc, Announcement::new(text)))
.into_iter()
.filter_map(|(lc, mb_ann)| mb_ann.map(|ann| (lc, ann)))
.collect()
},
command_toggles: Default::default(),
}
}
Expand Down Expand Up @@ -130,6 +148,33 @@ impl CachedEnvToggles {
}
}

#[derive(Clone, Default)]
pub struct AnnouncementsConfig {
pub max_shows: usize,
pub announcements: HashMap<SupportedLanguage, Announcement>,
}

impl AnnouncementsConfig {
pub fn get(&self, lang_code: &LanguageCode) -> Option<&Announcement> {
self.announcements.get(&lang_code.to_supported_language())
}
}

#[derive(Clone)]
pub struct Announcement {
pub text: Arc<String>,
pub hash: Arc<Vec<u8>>,
}

impl Announcement {
fn new(text: String) -> Option<Self> {
text.is_empty().not().then(|| Self {
hash: Arc::new(hash(&text)),
text: Arc::new(text),
})
}
}

pub fn build_context_for_help_messages(me: Me, incr: &Incrementor, competitor_bots: &[&str]) -> anyhow::Result<help::Context> {
let other_bots = competitor_bots
.iter()
Expand Down Expand Up @@ -204,6 +249,12 @@ fn ensure_starts_with_at_sign(s: String) -> String {
}
}

fn hash(s: &str) -> Vec<u8> {
let mut hasher = Sha256::new();
CoreWrapper::update(&mut hasher, s.as_bytes());
(*hasher.finalize()).to_vec()
}

#[cfg(test)]
mod test {
use super::ensure_starts_with_at_sign;
Expand Down
6 changes: 4 additions & 2 deletions src/domain/langcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ static DEFAULT: Lazy<LanguageCode> = Lazy::new(|| LanguageCode("en".to_string())
#[derive(Clone, Constructor, From)]
pub struct LanguageCode(String);

#[derive(Hash, Copy, Clone, Eq, PartialEq, sqlx::Type)]
#[sqlx(type_name = "language_code", rename_all = "lowercase")]
pub enum SupportedLanguage {
EN,
RU,
Expand All @@ -28,9 +30,9 @@ impl LanguageCode {
pub fn as_str(&self) -> &str {
self.0.as_str()
}

pub fn to_supported_language(&self) -> SupportedLanguage {
match self.0.as_str() {
match self.as_str() {
"ru" => SupportedLanguage::RU,
_ => SupportedLanguage::EN
}
Expand Down
5 changes: 4 additions & 1 deletion src/handlers/dod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ pub(crate) async fn dick_of_day_impl(cfg: config::AppConfig, repos: &repo::Repos
}
};
let time_left_part = utils::date::get_time_till_next_day_string(&lang_code);
format!("{main_part}{time_left_part}")
let announcement_part = repos.announcements.get_new(&chat_id.kind(), &lang_code).await?
.map(|announcement| format!("\n\n<i>{announcement}</i>"))
.unwrap_or_default();
format!("{main_part}{time_left_part}{announcement_part}")
},
None => t!("commands.dod.no_candidates", locale = &lang_code)
};
Expand Down
108 changes: 108 additions & 0 deletions src/repo/announcements.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use derive_more::Constructor;
use sqlx::{Pool, Postgres};
use crate::repo::{ensure_only_one_row_updated, ChatIdInternal, ChatIdKind};
use crate::config;
use crate::domain::{LanguageCode, SupportedLanguage};

Check warning on line 5 in src/repo/announcements.rs

View workflow job for this annotation

GitHub Actions / Run-tests

unused import: `SupportedLanguage`

#[derive(sqlx::FromRow)]
struct AnnouncementEntity {
chat_id: i64,
hash: Vec<u8>,
times_shown: isize,
}

struct Announcement {
chat_id: ChatIdInternal,
hash: Vec<u8>,
times_shown: usize,
}

impl From<AnnouncementEntity> for Announcement {
fn from(value: AnnouncementEntity) -> Self {
Self {
chat_id: ChatIdInternal(value.chat_id),
hash: value.hash,
times_shown: value.times_shown as usize
}
}
}

#[derive(Clone, Constructor)]
pub struct Announcements {
pool: Pool<Postgres>,
announcements: config::AnnouncementsConfig,
}

impl Announcements {

pub async fn get_new(&self, chat_id: &ChatIdKind, lang_code: &LanguageCode) -> anyhow::Result<Option<String>> {
let maybe_announcement = match self.announcements.get(lang_code) {
Some(announcement) if self.check_conditions(chat_id, announcement, lang_code).await? => Some((*announcement.text).clone()),
Some(_) | None => None
};
Ok(maybe_announcement)
}

async fn check_conditions(&self, chat_id_kind: &ChatIdKind, announcement: &config::Announcement, lang_code: &LanguageCode) -> anyhow::Result<bool> {
let res = match self.get(&chat_id_kind, lang_code).await? {
_ if self.announcements.max_shows == 0 => false,
Some(entity) if entity.hash[..] != announcement.hash[..] => {
self.update(entity.chat_id, lang_code, &announcement.hash).await?;
true
}
Some(entity) if entity.times_shown >= self.announcements.max_shows =>
false,
Some(entity) => {
self.increment_times_shown(entity.chat_id, lang_code).await?;
true
}
None => {
self.create(&chat_id_kind, lang_code, &announcement.hash).await?;
true
}
};
Ok(res)
}

async fn get(&self, chat_id_kind: &ChatIdKind, lang_code: &LanguageCode) -> anyhow::Result<Option<Announcement>> {
sqlx::query_as!(AnnouncementEntity,

Check failure on line 68 in src/repo/announcements.rs

View workflow job for this annotation

GitHub Actions / Run-tests

`DATABASE_URL` must be set, or `cargo sqlx prepare` must have been run and .sqlx must exist, to use query macros
"SELECT chat_id, hash, times_shown FROM Announcements
WHERE chat_id = (SELECT id FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text)
AND language = $2",
chat_id_kind.value() as String, lang_code.to_supported_language() as SupportedLanguage)
.fetch_optional(&self.pool)
.await
.map(|opt| opt.map(Into::into))
.map_err(Into::into)
}

async fn create(&self, chat_id_kind: &ChatIdKind, lang_code: &LanguageCode, hash: &[u8]) -> anyhow::Result<()> {
sqlx::query!(

Check failure on line 80 in src/repo/announcements.rs

View workflow job for this annotation

GitHub Actions / Run-tests

`DATABASE_URL` must be set, or `cargo sqlx prepare` must have been run and .sqlx must exist, to use query macros
"INSERT INTO Announcements (chat_id, language, hash, times_shown) VALUES (
(SELECT id FROM Chats WHERE chat_id = $1::bigint OR chat_instance = $1::text),
$2, $3, 1)",
chat_id_kind.value() as String, lang_code.to_supported_language() as SupportedLanguage, hash)
.execute(&self.pool)
.await
.map_err(Into::into)
.and_then(ensure_only_one_row_updated)
}

async fn increment_times_shown(&self, chat_id: ChatIdInternal, lang_code: &LanguageCode) -> anyhow::Result<()> {
sqlx::query!("UPDATE Announcements SET times_shown = times_shown + 1 WHERE chat_id = $1 AND language::text = $2",

Check failure on line 92 in src/repo/announcements.rs

View workflow job for this annotation

GitHub Actions / Run-tests

`DATABASE_URL` must be set, or `cargo sqlx prepare` must have been run and .sqlx must exist, to use query macros
chat_id.0, lang_code.as_str())
.execute(&self.pool)
.await
.map_err(Into::into)
.and_then(ensure_only_one_row_updated)
}

async fn update(&self, chat_id: ChatIdInternal, lang_code: &LanguageCode, hash: &[u8]) -> anyhow::Result<()> {
sqlx::query!("UPDATE Announcements SET hash = $3, times_shown = 0 WHERE chat_id = $1 AND language = $2",

Check failure on line 101 in src/repo/announcements.rs

View workflow job for this annotation

GitHub Actions / Run-tests

`DATABASE_URL` must be set, or `cargo sqlx prepare` must have been run and .sqlx must exist, to use query macros
chat_id.0, lang_code.to_supported_language() as SupportedLanguage, hash)
.execute(&self.pool)
.await
.map_err(Into::into)
.and_then(ensure_only_one_row_updated)
}
}
3 changes: 1 addition & 2 deletions src/repo/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ impl Loans {
.execute(&self.pool)
.await
.map_err(|e| anyhow!(e))
.and_then(ensure_only_one_row_updated)?;
Ok(())
.and_then(ensure_only_one_row_updated)
}
}
14 changes: 11 additions & 3 deletions src/repo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ mod import;
mod promo;
mod loans;
mod pvpstats;
mod stats;
mod announcements;

#[cfg(test)]
pub(crate) mod test;
mod stats;

use anyhow::anyhow;
use sqlx::{Pool, Postgres};
Expand All @@ -22,6 +23,7 @@ pub use promo::*;
pub use loans::*;
pub use pvpstats::*;
pub use stats::*;
pub use announcements::*;
use crate::config;
use crate::config::DatabaseConfig;

Expand All @@ -33,6 +35,7 @@ pub struct Repositories {
pub import: Import,
pub promo: Promo,
pub loans: Loans,
pub announcements: Announcements,
pub pvp_stats: BattleStatsRepo,
pub personal_stats: PersonalStatsRepo,
}
Expand All @@ -46,6 +49,7 @@ impl Repositories {
import: Import::new(db_conn.clone()),
promo: Promo::new(db_conn.clone()),
loans: Loans::new(db_conn.clone(), config),
announcements: Announcements::new(db_conn.clone(), config.announcements.clone()),
pvp_stats: BattleStatsRepo::new(db_conn.clone(), config.features),
personal_stats: PersonalStatsRepo::new(db_conn.clone()),
}
Expand Down Expand Up @@ -154,6 +158,10 @@ impl From<&ChatIdKind> for ChatIdType {
}
}

#[derive(Debug, Copy, Clone, sqlx::Type)]
#[sqlx(transparent)]
pub struct ChatIdInternal(i64);

#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, derive_more::From)]
pub struct UID(i64);
Expand Down Expand Up @@ -233,9 +241,9 @@ macro_rules! repository {
};
}

fn ensure_only_one_row_updated(res: PgQueryResult) -> Result<PgQueryResult, anyhow::Error> {
fn ensure_only_one_row_updated(res: PgQueryResult) -> Result<(), anyhow::Error> {
match res.rows_affected() {
1 => Ok(res),
x => Err(anyhow!("not only one row was updated but {x}"))
}
}.map(|_| ())
}
Loading

0 comments on commit cd083df

Please sign in to comment.