Skip to main content

telegram_raf/telegram/
contests.rs

1// Copyright 2021 Paolo Galeone <nessuno@nerdz.eu>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use chrono::{DateTime, Utc};
16use log::error;
17use rusqlite::params;
18use telexide_fork::{api::types::GetChatMember, prelude::*};
19
20use crate::persistence::types::{Contest, DBKey, Rank};
21use crate::telegram::users;
22
23use std::string::ToString;
24
25/// Returns the `Contest` with the specified `id`, if exists.
26///
27/// # Arguments
28/// * `ctx` - Telexide context
29/// * `id` - The ID (`RaF` generated) of the contest to search.
30///
31/// # Panics
32/// Panics if the connection to the DB fails, or if the returned data is corrupt.
33#[must_use]
34pub fn get(ctx: &Context, id: i64) -> Option<Contest> {
35    let guard = ctx.data.read();
36    let map = guard.get::<DBKey>().expect("db");
37    let conn = map.get().unwrap();
38    let mut stmt = conn
39        .prepare("SELECT name, prize, end, started_at, chan, stopped FROM contests WHERE id = ?")
40        .unwrap();
41    let mut iter = stmt
42        .query_map(params![id], |row| {
43            Ok(Contest {
44                id,
45                name: row.get(0)?,
46                prize: row.get(1)?,
47                end: row.get(2)?,
48                started_at: row.get(3)?,
49                chan: row.get(4)?,
50                stopped: row.get(5)?,
51            })
52        })
53        .unwrap();
54    let c = iter.next().unwrap();
55    if let Ok(c) = c {
56        return Some(c);
57    }
58    None
59}
60
61/// Returns all the `Contest` created for the channel with ID `id`.
62///
63/// # Arguments
64/// * `ctx` - Telexide context
65/// * `chan` - The ID (Telegram generated) of the Channel.
66///
67/// # Panics
68/// Panics if the connection to the DB fails, or if the returned data is corrupt.
69#[must_use]
70pub fn get_all(ctx: &Context, chan: i64) -> Vec<Contest> {
71    let guard = ctx.data.read();
72    let map = guard.get::<DBKey>().expect("db");
73    let conn = map.get().unwrap();
74    let mut stmt = conn
75        .prepare(
76            "SELECT id, name, prize, end, started_at, stopped FROM contests WHERE chan = ? ORDER BY end DESC",
77        )
78        .unwrap();
79
80    let contests = stmt
81        .query_map(params![chan], |row| {
82            Ok(Contest {
83                id: row.get(0)?,
84                name: row.get(1)?,
85                prize: row.get(2)?,
86                end: row.get(3)?,
87                started_at: row.get(4)?,
88                stopped: row.get(5)?,
89                chan,
90            })
91        })
92        .unwrap()
93        .map(std::result::Result::unwrap)
94        .collect();
95    contests
96}
97
98/// Returns rank for the `contest`, already oredered by number of invites accepted in descending
99/// order.
100///
101/// # Arguments
102/// * `ctx` - Telexide context
103/// * `contest` - The `Contest` under examination
104///
105/// # Panics
106/// Panics if the connection to the DB fails, or if the returned data is corrupt.
107#[must_use]
108pub fn ranking(ctx: &Context, contest: &Contest) -> Vec<Rank> {
109    let guard = ctx.data.read();
110    let map = guard.get::<DBKey>().expect("db");
111    let conn = map.get().unwrap();
112    // NOTE: the ordering ALSO via t.source is required to give a meaningful order (depending on
113    // the id, hence jsut to have them different) in case of equal rank
114    let mut stmt = conn
115            .prepare(
116                "SELECT ROW_NUMBER() OVER (ORDER BY t.c, t.source DESC) AS r, t.c, t.source
117                FROM (SELECT COUNT(*) AS c, source FROM invitations WHERE contest = ? GROUP BY source) AS t",
118            )
119            .unwrap();
120    stmt.query_map(params![contest.id], |row| {
121        Ok(Rank {
122            rank: row.get(0)?,
123            invites: row.get(1)?,
124            user: users::get(ctx, row.get(2)?).unwrap(),
125        })
126    })
127    .unwrap()
128    .map(std::result::Result::unwrap)
129    .collect::<Vec<Rank>>()
130}
131
132/// Possible errors while creating a Contest
133#[derive(Debug, Clone)]
134pub enum Error {
135    /// Error while parsing the user inserted date
136    ParseError(chrono::format::ParseError),
137    /// Generic error we want to report to the user as a string
138    GenericError(String),
139}
140
141impl From<chrono::format::ParseError> for Error {
142    /// Returns `Error::ParseError`
143    fn from(error: chrono::format::ParseError) -> Error {
144        Error::ParseError(error)
145    }
146}
147
148impl From<String> for Error {
149    /// Returns `Error::GenericError`
150    fn from(error: String) -> Error {
151        Error::GenericError(error)
152    }
153}
154
155impl std::fmt::Display for Error {
156    /// Format all the possible errors
157    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
158        match self {
159            Error::ParseError(error) => write!(f, "DateTime parse {error}"),
160            Error::GenericError(error) => write!(f, "{error}"),
161        }
162    }
163}
164
165/// Parse the input `text` and creates a valid `Contest` associated to the chan.
166///
167/// # Arguments
168///
169/// * `text` - A string slice holding the user inserted text
170/// * `chan` - The channel to associate with the Contest in case of success
171///
172/// # Errors
173/// If the parsing from text fails for whatever reason, it returns an `Error`
174/// that contains a detail. In case of failed parsing, it's a `Error::ParseError(e)`
175/// otherwise is a `Error::GenericError(s)` with a string containing the reason
176/// of the failure.
177pub fn from_text(text: &str, chan: i64) -> Result<Contest, Error> {
178    let rows = text
179        .split('\n')
180        .skip_while(|r| r.is_empty())
181        .collect::<Vec<&str>>();
182    if rows.len() != 3 {
183        return Err(format!("failed because row.len() != 3. Got: {}", rows.len()).into());
184    }
185    let id = -1;
186    let name = rows[0].to_string();
187    let prize = rows[2].to_string();
188    // user input: YYYY-MM-DD hh:mm TZ, needs to become
189    // YYYY-MM-DD hh:mm:ss TZ to get enough data to create a datetime object
190    let add_seconds = |row: &str| -> String {
191        let mut elements = row
192            .split_whitespace()
193            .map(ToString::to_string)
194            .collect::<Vec<String>>();
195        if elements.len() != 3 {
196            return row.to_string();
197        }
198        // 0: YYYY-MM-DD
199        // 1: hh:mm
200        // 2: TZ
201        elements[1] += ":00";
202        elements.join(" ")
203    };
204    let now = Utc::now();
205    let end: DateTime<Utc> =
206        DateTime::parse_from_str(&add_seconds(rows[1]), "%Y-%m-%d %H:%M:%S %#z")?.into();
207    if end < now {
208        return Err("End date can't be in the past".to_string().into());
209    }
210    Ok(Contest {
211        id,
212        end,
213        name,
214        prize,
215        chan,
216        stopped: false,
217        started_at: None,
218    })
219}
220
221/// Count the users that participated to the `contest`
222///
223/// # Arguments
224///
225/// * `ctx`: The telexide ctx, used to get the db
226/// * `contest`: The Contest under examination
227///
228/// # Panics
229/// Panics if the connection to the DB fails, or if the returned data is corrupt.
230#[must_use]
231pub fn count_users(ctx: &Context, contest: &Contest) -> i64 {
232    struct Counter {
233        value: i64,
234    }
235    let guard = ctx.data.read();
236    let map = guard.get::<DBKey>().expect("db");
237    let conn = map.get().unwrap();
238    let mut stmt = conn
239        .prepare("SELECT COUNT(id) FROM invitations WHERE contest = ?")
240        .unwrap();
241    let vals = stmt
242        .query_map(params![contest.id], |row| {
243            Ok(Counter { value: row.get(0)? })
244        })
245        .unwrap()
246        .map(|count| count.unwrap_or(Counter { value: -1 }).value)
247        .collect::<Vec<i64>>();
248    if vals.is_empty() {
249        return 0;
250    }
251    vals[0]
252}
253
254/// Function to call to verify that the joined users are still in the channel.
255/// NOTE: this function is async because it uses the async `ctx.api.get_chat_member`
256/// function to check if the user is still inside the channel referenced by the `contest`.
257///
258/// # Arguments
259/// * `ctx`: The Telexide context, used to get the db
260/// * `contest`: The Contest under examination
261///
262/// # Panics
263/// Panics if the connection to the DB fails, or if the returned data is corrupt.
264pub async fn validate_users(ctx: &Context, contest: &Contest) {
265    struct InnerUser {
266        id: i64,
267    }
268    let users = {
269        let guard = ctx.data.read();
270        let map = guard.get::<DBKey>().expect("db");
271        let conn = map.get().unwrap();
272        let mut stmt = conn
273            .prepare("SELECT dest FROM invitations WHERE contest = ?")
274            .unwrap();
275        stmt.query_map(params![contest.id], |row| Ok(InnerUser { id: row.get(0)? }))
276            .unwrap()
277            .map(|user| user.unwrap().id)
278            .collect::<Vec<i64>>()
279    };
280
281    for user in users {
282        let member = ctx
283            .api
284            .get_chat_member(GetChatMember {
285                chat_id: contest.chan,
286                user_id: user,
287            })
288            .await;
289
290        let in_channel = member.is_ok();
291        if !in_channel {
292            let res = {
293                let guard = ctx.data.read();
294                let map = guard.get::<DBKey>().expect("db");
295                let conn = map.get().unwrap();
296                let mut stmt = conn
297                    .prepare("DELETE FROM invitations WHERE dest = ? and contest = ?")
298                    .unwrap();
299                stmt.execute(params![user, contest.id])
300            };
301            if res.is_err() {
302                error!("[users validation] {}", res.err().unwrap());
303            }
304        }
305    }
306}