users can set their authentication through a TSaR bot now! Can't be used for anything yet, though
parent
c1e02b04c6
commit
41e80ca508
@ -1,2 +1,6 @@
|
||||
/target
|
||||
ttc_test*
|
||||
eid
|
||||
|
||||
# DO NOT COMMIT, CONTAINS SENSITIVE INFORMATION
|
||||
login
|
||||
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT pronouns \n FROM pronouns \n WHERE username == ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "pronouns",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a49c11570aff702c2e583a7e37a94fcb98c3a76db228002b01a93e74a747c44a"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
-- allow pronouns to be null
|
||||
ALTER TABLE pronouns ADD COLUMN pronouns_nullable_tmp TEXT;
|
||||
UPDATE pronouns SET pronouns_nullable_tmp = pronouns;
|
||||
ALTER TABLE pronouns DROP COLUMN pronouns;
|
||||
ALTER TABLE pronouns RENAME COLUMN pronouns_nullable_tmp TO pronouns;
|
||||
|
||||
-- change table name
|
||||
ALTER TABLE pronouns RENAME TO users;
|
||||
|
||||
-- add authentication field
|
||||
ALTER TABLE users ADD COLUMN auth BLOB;
|
@ -0,0 +1,300 @@
|
||||
use std::{error::Error, fs::File, time::Duration, io::{Read, Write}, cmp::max};
|
||||
use log::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use base64::prelude::*;
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct LoginInfo {
|
||||
#[serde(rename = "user")]
|
||||
username: String,
|
||||
auth: String,
|
||||
uid: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "fn")]
|
||||
enum StitchFunction {
|
||||
#[serde(rename = "banter.list")]
|
||||
GetMessages {
|
||||
folder: String,
|
||||
start: i64,
|
||||
results: i64,
|
||||
},
|
||||
|
||||
#[serde(rename = "chat.action")]
|
||||
SendMessage {
|
||||
sid: i64,
|
||||
text: String,
|
||||
action: String, // should ALWAYS be "SENDSESS"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct StitchCall {
|
||||
username: String,
|
||||
auth: String,
|
||||
#[serde(rename = "expectUserChange")]
|
||||
expect_user_change: bool,
|
||||
#[serde(rename = "requests")]
|
||||
functions: Vec<StitchFunction>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct MessageMemberDescription {
|
||||
#[serde(default)]
|
||||
uid: Option<i64>,
|
||||
#[serde(default)]
|
||||
you: bool,
|
||||
mid: i64,
|
||||
#[serde(rename = "readThru")]
|
||||
last_eid_read: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct MessageEvent {
|
||||
eid: i64,
|
||||
mid: i64,
|
||||
time: i64,
|
||||
#[serde(rename = "type")]
|
||||
event_type: String,
|
||||
data: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct MessageThreadDescription {
|
||||
sid: i64,
|
||||
title: String,
|
||||
preview: String,
|
||||
uids: Vec<i64>,
|
||||
#[serde(rename = "areYouAnon")]
|
||||
are_you_anon: bool,
|
||||
#[serde(rename = "lastEid")]
|
||||
last_eid: i64,
|
||||
#[serde(rename = "type")]
|
||||
thread_type: String,
|
||||
members: Vec<MessageMemberDescription>,
|
||||
events: Vec<MessageEvent>,
|
||||
#[serde(rename = "readToEid", default)]
|
||||
read_to_eid: i64,
|
||||
#[serde(rename = "groupId", default)]
|
||||
group_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
enum StitchFunctionResponse {
|
||||
GetMessages {
|
||||
folder: String,
|
||||
items: Vec<MessageThreadDescription>,
|
||||
#[serde(rename = "endReached")]
|
||||
end_reached: bool,
|
||||
ok: bool,
|
||||
tag: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct UserDescription {
|
||||
id: i64,
|
||||
name: String,
|
||||
key: String,
|
||||
seen: i64,
|
||||
#[serde(rename = "avatarId")]
|
||||
avatar_id: i64,
|
||||
#[serde(rename = "avatar")]
|
||||
avatar_path: String,
|
||||
#[serde(rename = "posts")]
|
||||
post_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct StitchResponse {
|
||||
ok: bool,
|
||||
responses: Vec<StitchFunctionResponse>,
|
||||
users: Vec<UserDescription>,
|
||||
auth: String,
|
||||
v: String,
|
||||
}
|
||||
|
||||
impl StitchCall {
|
||||
fn from_login_info(login_info: &LoginInfo, expect_user_change: bool, functions: Vec<StitchFunction>) -> Self {
|
||||
Self {
|
||||
username: login_info.username.clone(),
|
||||
auth: login_info.auth.clone(),
|
||||
expect_user_change,
|
||||
functions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StitchFunction {
|
||||
fn send_message(sid: i64, text: String) -> Self {
|
||||
Self::SendMessage {
|
||||
sid,
|
||||
text,
|
||||
action: "SENDSESS".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn send_message(client: &Client, login: &LoginInfo, sid: i64, text: String) -> Result<(), ()> {
|
||||
let call = StitchCall::from_login_info(login, false, vec![StitchFunction::send_message(sid, text)]);
|
||||
client.post("https://twocansandstring.com/stitchservices")
|
||||
.json(&call)
|
||||
.send()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| error!("Failed to send message witht error {}", e))
|
||||
}
|
||||
|
||||
|
||||
async fn handle_event(pool: &SqlitePool, login: &LoginInfo, thread: &MessageThreadDescription, res: &StitchResponse, client: &Client) -> Result<(), ()> {
|
||||
let event = &thread.events[0];
|
||||
let text = event.data.split_at(2).1.to_lowercase();
|
||||
let sid = thread.sid;
|
||||
let mid = event.mid;
|
||||
let Some(uid) = thread.members.iter().find(|member| member.mid == mid).and_then(|member| member.uid) else { return Err(()) };
|
||||
let Some(username) = res.users.iter().find(|user| user.id == uid).map(|user| &user.name) else { return Err(()) };
|
||||
|
||||
debug!("handling '{text}' from {username} in thread {sid}");
|
||||
|
||||
if text.starts_with("help") {
|
||||
send_message(client, login, sid,
|
||||
r#"To generate a new authentication code, send a message with the text "get_authentication" (without quotes).
|
||||
Note: this will remove your previous authentication code, if you had one."#.into()
|
||||
).await?;
|
||||
} else if text.starts_with("get_authentication") {
|
||||
let key_data: [u8; 16] = thread_rng().gen();
|
||||
let b64_key = BASE64_URL_SAFE.encode(key_data);
|
||||
let hashed_key = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(key_data);
|
||||
hasher.update("get salted on lmao");
|
||||
hasher.finalize()
|
||||
};
|
||||
let hashed_key_slice = hashed_key.as_slice();
|
||||
|
||||
|
||||
let user_in_db = sqlx::query!("SELECT username FROM users WHERE username == ?;", username)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| error!("failed to search database for username: {e}"))?
|
||||
.is_some();
|
||||
|
||||
if user_in_db {
|
||||
sqlx::query!("UPDATE users
|
||||
SET auth = ?
|
||||
WHERE username == ?;
|
||||
", hashed_key_slice, username)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| error!("failed to update authentication key: {e}"))?;
|
||||
} else {
|
||||
sqlx::query!("INSERT INTO users (username, auth)
|
||||
VALUES (?, ?);
|
||||
", username, hashed_key_slice)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| error!("failed to create user for authentication: {e}"))?;
|
||||
}
|
||||
|
||||
send_message(client, login, sid,
|
||||
format!("A new authentication code has been generated. If you had a previous code, it is now invalid.
|
||||
!!! NOTE: THIS IS A PASSWORD; DO NOT SHARE IT WITH OTHERS !!!
|
||||
Your authentication code is displayed below:
|
||||
|
||||
{b64_key}
|
||||
")
|
||||
).await?;
|
||||
send_message(client, login, sid,
|
||||
"In order to use this code for The Third Can, open the extension popup and navigate to the authentication section. It will have a text box where you can enter your code.".into()
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
async fn poll_tsar(pool: &SqlitePool, client: &Client, messages_request_body: &StitchCall, last_eid_handled: &mut i64, login: &LoginInfo) -> Result<(), Box<dyn Error>> {
|
||||
println!("Querying messages");
|
||||
let recent_messages = client.post("https://twocansandstring.com/stitchservices")
|
||||
.json(&messages_request_body)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let mut res: StitchResponse = serde_json::from_str(&recent_messages)?;
|
||||
let threads = match res.responses.pop().unwrap() {
|
||||
StitchFunctionResponse::GetMessages { items, .. } => items,
|
||||
};
|
||||
|
||||
let mut new_last_eid_handled = 0i64;
|
||||
for thread in threads.into_iter()
|
||||
.filter(|thread| thread.last_eid > *last_eid_handled)
|
||||
.filter(|thread| {
|
||||
thread.members.iter()
|
||||
.find(|m| m.you)
|
||||
.is_some_and(|you| thread.events[0].mid != you.mid)
|
||||
})
|
||||
{
|
||||
let success = handle_event(pool, &login, &thread, &res, &client).await;
|
||||
|
||||
if success.is_ok() {
|
||||
new_last_eid_handled = max(thread.events[0].eid, new_last_eid_handled);
|
||||
}
|
||||
}
|
||||
|
||||
*last_eid_handled = max(*last_eid_handled, new_last_eid_handled);
|
||||
{
|
||||
let mut eid_file = File::create("eid")?;
|
||||
eid_file.write_all(&last_eid_handled.to_le_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub async fn run_tsar(pool: &SqlitePool) {
|
||||
println!("Initializing TSaR");
|
||||
|
||||
let login_info: LoginInfo = {
|
||||
let login_file = File::open("login").expect("login file must exist");
|
||||
serde_json::from_reader(login_file).expect("login file must be readable")
|
||||
};
|
||||
|
||||
let mut last_eid_handled: i64 = {
|
||||
let mut out = [0u8;8];
|
||||
File::open("eid")
|
||||
.ok()
|
||||
.and_then(|mut file| file.read(&mut out).ok());
|
||||
i64::from_le_bytes(out)
|
||||
};
|
||||
|
||||
|
||||
let client = Client::new();
|
||||
let request_period = Duration::from_secs(10);
|
||||
let messages_request_body = StitchCall::from_login_info(
|
||||
&login_info,
|
||||
false,
|
||||
vec![StitchFunction::GetMessages {
|
||||
folder: "inbox".into(),
|
||||
start: 0,
|
||||
results: 20,
|
||||
}]
|
||||
);
|
||||
|
||||
|
||||
loop {
|
||||
poll_tsar(pool, &client, &messages_request_body, &mut last_eid_handled, &login_info)
|
||||
.await
|
||||
.unwrap_or_else( |err| error!("{}", err.to_string()) );
|
||||
|
||||
tokio::time::sleep(request_period).await;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue