Skip to content

Commit

Permalink
refactor(#22) - Created internal ReAPI Library
Browse files Browse the repository at this point in the history
* started work on ReAPI

* more work

* added users to ReAPI

* login works

* donkey

* Fixed ReAPI impl in sb

* moved login in base of ReAPI

* added debug to login

* something

* something

* Beginnings of SB logic

* fix images

* remove login from root

* More progress

* got images to work

* Adding ReAPI rooms function

* everything

* everything

* fix merge conflict

* fix test

* it works

* this doesn't work

* Batching Messages

* reimpl exporting

* Adding other export formats; fixing timestamp; image saving only impl. on .txt

* Now with 100% more export choises (that actually work)

* Fixing shit

* Prepare for  v1.0.0 release

* Fix edition typo

* Remove unessesary crates

* Removing OneElectrons implicit panic from the test...

---------

Co-authored-by: Electron <one.electron109@protonmail.com>
  • Loading branch information
MPult and oneElectron authored Apr 30, 2023
1 parent 8cdd87c commit be06e6d
Show file tree
Hide file tree
Showing 19 changed files with 737 additions and 595 deletions.
7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rexit"
version = "0.1.1"
version = "1.0.0"
edition = "2021"
description = "Export your Reddit Chats"
readme = "README.md"
Expand All @@ -9,7 +9,7 @@ license = "GPL-3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.24"
chrono = { version = "0.4.24", features = ["serde"] }
reqwest = {version = "0.11.16", features = ["blocking", "multipart", "cookies", "gzip"]}
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0"
Expand All @@ -20,5 +20,4 @@ log = "0.4.0"
pretty_env_logger = "0.4.0"
inquire = "0.6.1"
cached = "0.43.0"
console = { version = "0.15.5", features = ["windows-console-colors"] }
url = { version = "2.3.1", features = ["serde"] }
console = { version = "0.15.5", features = ["windows-console-colors"] }
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ Options:
Currently, you need to specify the formats, and it will ask for the username and password (or bearer token with that auth flow).

```bash
$ rexit --formats csv,json,txt
$ rexit --formats csv,json,txt --images
> Your Reddit Username: <USERNAME>
> Your Reddit Password: <PASSWORD>
```
It will save the files to the current directory. For CSV and TXT it is split by room; for JSON it's combined into one file. If an image (.jpg, .gif, .png, etc.) was sent the matrix URL (`mxc://<serverName>/<ID>`) will be displayed as the message content.
It will save the files to the current directory. For CSV and TXT it is split by room. If an image (.jpg, .gif, .png, etc.) was sent the filename will be displayed as the message content, along with the prefix `FILE`.

## Installation
You can use the files provided in the releases' page of this repository, or install via cargo.

### Manual Install

1. Download the build for your system (Windows or M1 MacOS)
2. Use the terminal run Rexit with the arguments you want. (See Usage for details)

### Cargo Install
```BASH
$ cargo install rexit
```
Expand Down
5 changes: 4 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ test:
cargo test

test-creds:
cargo test --include-ignored
cargo test -- --include-ignored

doc:
cargo doc --no-deps --open
129 changes: 129 additions & 0 deletions src/ReAPI/images.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use super::Client;
use crate::exit;
use cached::SizedCache;
use console::style;
use serde::Serialize;
use std::path::PathBuf;

#[derive(std::hash::Hash, Clone, Debug, Serialize)]
pub struct Image {
pub extension: String,
pub id: String,
pub data: Vec<u8>,
}

impl Image {
pub fn export_to(&self, path: PathBuf) {
let mut path = path;
path.push(self.id.clone());

std::fs::write(
path.with_extension(self.extension.clone()),
self.data.clone(),
)
.unwrap();
}

pub fn from(id: String, extension: String, data: Vec<u8>) -> Image {
Image {
extension,
id,
data,
}
}
}

/// Gets images from a mxc:// URL as per [SPEC](https://spec.matrix.org/v1.6/client-server-api/#get_matrixmediav3downloadservernamemediaid)
#[cached::proc_macro::cached(
type = "SizedCache<String, Image>",
create = "{ SizedCache::with_size(10_000) }",
convert = r#"{ format!("{}", url) }"#
)]
pub fn get_image(client: &Client, url: String) -> Image {
info!(target: "get_image", "Getting image: {}", url);
let (url, id) = parse_matrix_image_url(url.as_str());

let data = client.reqwest_client.get(url).send().unwrap();

Image {
extension: get_image_extension(&data.headers()),
id,
data: data.bytes().unwrap().to_vec(),
}
}

fn parse_matrix_image_url(url: &str) -> (String, String) {
let url = reqwest::Url::parse(url).unwrap(); // I assume that all urls given to this function are valid

let output_url =
reqwest::Url::parse("https://matrix.redditspace.com/_matrix/media/r0/download/reddit.com/")
.unwrap();

let id = url.path_segments().unwrap().next().unwrap();

let output_url = output_url.join(id).unwrap();

(output_url.to_string(), id.to_string())
}

fn get_image_extension(headers: &reqwest::header::HeaderMap) -> String {
let mut extension: Option<String> = None;

// Iterate over headers to find content-type
for (header_name, header_value) in headers {
if header_name.as_str() != "content-type" {
continue;
}
let file_type = header_value.to_str().unwrap().to_string();

let mut file_type = file_type.split("/");

extension = match file_type.nth(1).unwrap() {
"jpeg" => Some("jpeg".to_string()),
"png" => Some("png".to_string()),
"gif" => Some("gif".to_string()),
_ => {
println!("{}", style("Failed to read image type").red().bold());
exit!(0);
}
};
}

if extension.is_none() {
println!(
"{}",
style("Error: Something failed reading the image type")
.red()
.bold()
);
error!("Something failed reading the image type");
exit!(0);
}

return extension.unwrap();
}

#[cfg(test)]
mod tests {
#[test]
fn get_image() {
let image = super::get_image(
&super::super::new_client(true),
"mxc://reddit.com/dwdprq7pxbva1/".to_string(),
);

image.export_to(std::path::PathBuf::from(
"./test_resources/test_cases/ReAPI/images/get_images/",
));

assert!(std::path::PathBuf::from(
"./test_resources/test_cases/ReAPI/images/get_images/dwdprq7pxbva1.gif"
)
.exists());

std::fs::remove_file(
"./test_resources/test_cases/ReAPI/images/get_images/dwdprq7pxbva1.gif",
)
.expect("Could not remove downloaded file");
}
}
162 changes: 162 additions & 0 deletions src/ReAPI/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use console::style;
use regex::Regex;

impl super::Client {
pub fn logged_in(&self) -> bool {
self.bearer.is_some()
}

pub fn bearer_token(&self) -> String {
if let Some(token) = self.bearer.clone() {
return token.clone();
}

println!("{}", style("You are not logged in").red().bold());
crate::exit!(0);
}

pub fn login_with_token(&mut self, bearer: String) {
self.bearer = Some(bearer);
}

/// Log into Reddit returning the Bearer
pub fn login(&mut self, username: String, password: String) {
// URL encode the password & username
let encoded_password: String;
let username = urlencoding::encode(&username);

// Reddit is doing a weird thing where * is not urlencoded. Sorry for everyone that has * and %2A in their password
if password.contains("*") {
debug!("Password has *; URL-encode was rewritten");
encoded_password = password.replace("%2A", "*");
} else {
encoded_password = urlencoding::encode(&password).into_owned();
}

// Send an HTTP GET request to get the CSRF token
let resp = self
.reqwest_client
.get("https://www.reddit.com/login/")
.send()
.expect("Failed to send HTTP request; to obtain CSRF token");

debug!("CSRF Request Response: {:?}", resp);
let body = resp.text();
let body = body.expect("Failed to read response body");

// Regex to find the CSRF token in the body of the HTML
let csrf =
Regex::new(r#"<input\s+type="hidden"\s+name="csrf_token"\s+value="([^"]*)""#).unwrap();

// For the love of god do not touch this code ever; i made a deal with the devil to make this work
let mut csrf_token: String = String::default();
for i in csrf.captures_iter(body.as_str()) {
for i in i.get(1).iter() {
csrf_token = String::from(i.as_str().clone());
debug!("CSRF Token: {}", csrf_token);
}
}

// Form data for actual login
let form_data = format!(
"csrf_token={}&otp=&password={}&dest=https%3A%2F%2Fwww.reddit.com&username={}",
csrf_token, encoded_password, username
);

// Perform the actual login post request
let _x = self.reqwest_client
.post("https://www.reddit.com/login")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Sec-Ch-Ua", "\"Not:A-Brand\";v=\"99\", \"Chromium\";v=\"112\"")
.header("Sec-Ch-Ua-Platform", "Windows")
.header("Sec-Ch-Ua-Mobile", "?0")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.121 Safari/537.36")
.header("Origin", "https://www.reddit.com")
.header("Sec-Fetch-Site", "same-origin")
.header("Sec-Fetch-Mode", "cors")
.header("Sec-Fetch-Dest", "empty")
.header("Referrer","https://www.reddit.com/login/")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
.body(form_data)
.send()
.expect("Failed to send HTTP request; to obtain session token");


// Request / to get the bearer token
let response = self.reqwest_client
.get("https://www.reddit.com/")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.121 Safari/537.36")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
.header("Referrer","https://www.reddit.com/login/")
.header("Sec-Fetch-Dest", "document")
.header("Sec-Fetch-Mode", "navigate")
.header("Sec-Fetch-Site", "same-origin")
.header("Sec-Fetch-User", "?1")
.header("Te", "trailers")
.send()
.expect("Error getting bearer token");

// Extract the Bearer Token from the JSON response
let bearer_regex = Regex::new(r#"accessToken":"([^"]+)"#).unwrap();

let mut bearer_token: String = String::default();
for i in bearer_regex.captures_iter(&response.text().unwrap()) {
for i in i.get(1).iter() {
bearer_token = String::from(i.as_str().clone());
debug!("Bearer Token: {}", bearer_token.trim());
}
}

// Login to matrix.reddit.com using the bearer for reddit.com
let data = format!(
"{{\"type\":\"com.reddit.token\",\"token\":\"{bearer_token}\",\"initial_device_display_name\":\"Reddit Web Client\"}}"
);

debug!("Matrix request body: {:#?}", data);

let response = self.reqwest_client
.post("https://matrix.redditspace.com/_matrix/client/r0/login")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.121 Safari/537.36")
.header("Accept", "application/json")
.header("Origin", "https://chat.reddit.com")
.header("Sec-Fetch-Site", "cross-site")
.header("Sec-Fetch-Mode", "cors")
.header("Sec-Fetch-Dest", "empty")
.header("Accept-Encoding", "gzip, deflate")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Te", "trailers")
.body(data)
.send()
.expect("Failed to send HTTP request; to login to matrix");

debug!("Matrix login response: {:#?}", response);
if !response.status().is_success() {
println!("{}", style("Login failed").red().bold());
crate::exit!(0, "Login exited with failure");
}

self.bearer = Some(bearer_token);
}
}

#[cfg(test)]
mod tests {
#[test]
#[ignore = "creds"]
fn login() {
let mut client = super::super::new_client(true);
let (username, password) = get_login();

client.login(username, password);
}

fn get_login() -> (String, String) {
let username = std::env::var("REXIT_USERNAME").expect("Could not find username in env");
let password = std::env::var("REXIT_PASSWORD").expect("Could not find password in env");

(username, password)
}
}
Loading

0 comments on commit be06e6d

Please sign in to comment.