1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! # Token module.
//!
//! Functions to read/write token from/to the database. A token is any string associated with a key.
//!
//! Tokens are used in SecureJoin verification protocols.

use anyhow::Result;
use deltachat_derive::{FromSql, ToSql};

use crate::chat::ChatId;
use crate::context::Context;
use crate::tools::{create_id, time};

/// Token namespace
#[derive(
    Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[repr(u32)]
pub enum Namespace {
    #[default]
    Unknown = 0,
    Auth = 110,
    InviteNumber = 100,
}

/// Saves a token to the database.
pub async fn save(
    context: &Context,
    namespace: Namespace,
    foreign_id: Option<ChatId>,
    token: &str,
) -> Result<()> {
    match foreign_id {
        Some(foreign_id) => context
            .sql
            .execute(
                "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
                (namespace, foreign_id, token, time()),
            )
            .await?,
        None => {
            context
                .sql
                .execute(
                    "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);",
                    (namespace, token, time()),
                )
                .await?
        }
    };

    Ok(())
}

/// Lookup most recently created token for a namespace/chat combination.
///
/// As there may be more than one valid token for a chat-id,
/// (eg. when a qr code token is withdrawn, recreated and revived later),
/// use lookup() for qr-code creation only;
/// do not use lookup() to check for token validity.
///
/// To check if a given token is valid, use exists().
pub async fn lookup(
    context: &Context,
    namespace: Namespace,
    chat: Option<ChatId>,
) -> Result<Option<String>> {
    let token = match chat {
        Some(chat_id) => {
            context
                .sql
                .query_get_value(
                    "SELECT token FROM tokens WHERE namespc=? AND foreign_id=? ORDER BY timestamp DESC LIMIT 1;",
                    (namespace, chat_id),
                )
                .await?
        }
        // foreign_id is declared as `INTEGER DEFAULT 0` in the schema.
        None => {
            context
                .sql
                .query_get_value(
                    "SELECT token FROM tokens WHERE namespc=? AND foreign_id=0 ORDER BY timestamp DESC LIMIT 1;",
                    (namespace,),
                )
                .await?
        }
    };
    Ok(token)
}

pub async fn lookup_or_new(
    context: &Context,
    namespace: Namespace,
    foreign_id: Option<ChatId>,
) -> String {
    if let Ok(Some(token)) = lookup(context, namespace, foreign_id).await {
        return token;
    }

    let token = create_id();
    save(context, namespace, foreign_id, &token).await.ok();
    token
}

pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Result<bool> {
    let exists = context
        .sql
        .exists(
            "SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;",
            (namespace, token),
        )
        .await?;
    Ok(exists)
}

/// Looks up ChatId by auth token.
///
/// Returns None if auth token is not valid.
/// Returns zero/unset ChatId if the token corresponds to "setup contact" rather than group join.
pub async fn auth_chat_id(context: &Context, token: &str) -> Result<Option<ChatId>> {
    let chat_id: Option<ChatId> = context
        .sql
        .query_row_optional(
            "SELECT foreign_id FROM tokens WHERE namespc=? AND token=?",
            (Namespace::Auth, token),
            |row| {
                let chat_id: ChatId = row.get(0)?;
                Ok(chat_id)
            },
        )
        .await?;
    Ok(chat_id)
}

pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Result<()> {
    context
        .sql
        .execute(
            "DELETE FROM tokens WHERE namespc=? AND token=?;",
            (namespace, token),
        )
        .await?;
    Ok(())
}