mirror of
https://github.com/umutcamliyurt/Amnezichat.git
synced 2025-05-04 23:20:49 +01:00
Add voice messages
This commit is contained in:
parent
ec7027c694
commit
899f9cae6c
23
README.md
23
README.md
@ -5,7 +5,7 @@
|
||||
|
||||
# Amnezichat
|
||||
|
||||
<img src="banner.png" width="1000">
|
||||
<img src="banner.png" width="1200">
|
||||
|
||||
## Anti-forensic and secure messenger
|
||||
<!-- DESCRIPTION -->
|
||||
@ -49,7 +49,7 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en
|
||||
- EdDSA and Dilithium5 for authentication, ECDH and Kyber1024 for key exchange, encryption using ChaCha20-Poly1305
|
||||
|
||||
<!-- INSTALLATION -->
|
||||
## Basic server setup:
|
||||
## Server setup:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install curl build-essential git
|
||||
@ -59,16 +59,16 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
|
||||
## Onionsite setup with Docker:
|
||||
## Server setup with Docker:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install docker.io git
|
||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||
cd Amnezichat/
|
||||
sudo docker build -t amnezichat:latest .
|
||||
sudo docker run -p 8080:8080 amnezichat:latest
|
||||
cd Amnezichat/server/
|
||||
sudo docker build -t amnezichatserver:latest .
|
||||
sudo docker run -p 8080:8080 amnezichatserver:latest
|
||||
|
||||
## Client usage:
|
||||
## Client setup:
|
||||
|
||||
**For Web UI connect to http://localhost:8000**
|
||||
|
||||
@ -81,6 +81,15 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
|
||||
## Client setup with Docker:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install docker.io git
|
||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||
cd Amnezichat/client/
|
||||
sudo docker build -t amnezichat:latest .
|
||||
sudo docker run -p 8000:8000 amnezichat:latest
|
||||
|
||||
## Requirements:
|
||||
|
||||
- [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/)
|
||||
|
21
README_TR.md
21
README_TR.md
@ -5,7 +5,7 @@
|
||||
|
||||
# Amnezichat
|
||||
|
||||
<img src="banner.png" width="1000">
|
||||
<img src="banner.png" width="1200">
|
||||
|
||||
## İz bırakmayan güvenli mesajlaşma
|
||||
<!-- AÇIKLAMA -->
|
||||
@ -59,16 +59,16 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
|
||||
## Docker ile Onion sitesi kurulumu:
|
||||
## Docker ile sunucu kurulumu:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install docker.io git
|
||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||
cd Amnezichat/
|
||||
sudo docker build -t amnezichat:latest .
|
||||
sudo docker run -p 8080:8080 amnezichat:latest
|
||||
cd Amnezichat/server/
|
||||
sudo docker build -t amnezichatserver:latest .
|
||||
sudo docker run -p 8080:8080 amnezichatserver:latest
|
||||
|
||||
## İstemci kullanımı:
|
||||
## İstemci kurulumu:
|
||||
|
||||
**Web UI için http://localhost:8000 adresine bağlanın**
|
||||
|
||||
@ -81,6 +81,15 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
|
||||
## Docker ile istemci kurulumu:
|
||||
|
||||
sudo apt update
|
||||
sudo apt install docker.io git
|
||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||
cd Amnezichat/client/
|
||||
sudo docker build -t amnezichat:latest .
|
||||
sudo docker run -p 8000:8000 amnezichat:latest
|
||||
|
||||
## Gereksinimler:
|
||||
|
||||
- [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/)
|
||||
|
@ -21,4 +21,7 @@ rpassword = "7.3.1"
|
||||
sha3 = "0.10.8"
|
||||
x25519-dalek = "2.0.1"
|
||||
ed25519-dalek = "2.1.1"
|
||||
rocket = { version = "0.5", features = ["json"] }
|
||||
rocket = { version = "0.5", features = ["json"] }
|
||||
eframe = "0.26"
|
||||
egui = "0.26"
|
||||
rfd = "0.12"
|
26
client/Dockerfile
Normal file
26
client/Dockerfile
Normal file
@ -0,0 +1,26 @@
|
||||
FROM debian:12
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV RUSTUP_HOME=/usr/local/rustup \
|
||||
CARGO_HOME=/usr/local/cargo \
|
||||
PATH=/usr/local/cargo/bin:$PATH
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
build-essential \
|
||||
git \
|
||||
tor && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
RUN git clone https://github.com/umutcamliyurt/Amnezichat.git /opt/Amnezichat
|
||||
|
||||
WORKDIR /opt/Amnezichat/client
|
||||
RUN cargo build --release
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD tor & cargo run --release
|
@ -1,22 +0,0 @@
|
||||
173.245.48.0/20
|
||||
103.21.244.0/22
|
||||
103.22.200.0/22
|
||||
103.31.4.0/22
|
||||
141.101.64.0/18
|
||||
108.162.192.0/18
|
||||
190.93.240.0/20
|
||||
188.114.96.0/20
|
||||
197.234.240.0/22
|
||||
198.41.128.0/17
|
||||
162.158.0.0/15
|
||||
104.16.0.0/13
|
||||
104.24.0.0/14
|
||||
172.64.0.0/13
|
||||
131.0.72.0/22
|
||||
2400:cb00::/32
|
||||
2606:4700::/32
|
||||
2803:f800::/32
|
||||
2405:b500::/32
|
||||
2405:8100::/32
|
||||
2a06:98c0::/29
|
||||
2c0f:f248::/32
|
@ -34,29 +34,33 @@ use encryption::decrypt_data;
|
||||
use oqs::*;
|
||||
use oqs::sig::{Sig, PublicKey, SecretKey, Algorithm as SigAlgorithm};
|
||||
use rand::Rng;
|
||||
use reqwest::blocking::get;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use hex;
|
||||
use std::io::{self, Write};
|
||||
use rpassword::read_password;
|
||||
use std::result::Result;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
error::Error,
|
||||
};
|
||||
use ipnetwork::IpNetwork;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chacha20poly1305::aead::OsRng;
|
||||
use rand::RngCore;
|
||||
use sha3::{Sha3_512, Digest};
|
||||
use ed25519_dalek::VerifyingKey as Ed25519PublicKey;
|
||||
use eframe::egui;
|
||||
use rfd::MessageDialog;
|
||||
use rfd::MessageButtons;
|
||||
use rfd::MessageLevel;
|
||||
use rfd::MessageDialogResult;
|
||||
|
||||
fn get_raw_bytes_public_key(pk: &PublicKey) -> &[u8] {
|
||||
pk.as_ref()
|
||||
@ -95,7 +99,6 @@ fn request_user_confirmation(
|
||||
}
|
||||
|
||||
let path = "contact_fingerprints.enc";
|
||||
|
||||
let trusted_fingerprints = load_trusted_fingerprints(path, password)?;
|
||||
|
||||
if trusted_fingerprints.contains(fingerprint) {
|
||||
@ -103,34 +106,39 @@ fn request_user_confirmation(
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
println!("The fingerprint of the received public key is: {}", fingerprint);
|
||||
print!("Do you confirm this fingerprint? (yes/no): ");
|
||||
io::stdout().flush()?;
|
||||
let message = format!(
|
||||
"🔒 Fingerprint Verification\n\n\
|
||||
Your fingerprint:\n{}\n\n\
|
||||
Received fingerprint:\n{}\n\n\
|
||||
Do you want to trust the received fingerprint?",
|
||||
own_fingerprint, fingerprint
|
||||
);
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let response = input.trim().to_lowercase();
|
||||
let confirm = MessageDialog::new()
|
||||
.set_title("Trust New Fingerprint")
|
||||
.set_level(MessageLevel::Info)
|
||||
.set_description(&message)
|
||||
.set_buttons(MessageButtons::YesNo)
|
||||
.show();
|
||||
|
||||
match response.as_str() {
|
||||
"yes" => {
|
||||
print!("Would you like to remember this fingerprint for future sessions? (yes/no): ");
|
||||
io::stdout().flush()?;
|
||||
if confirm == MessageDialogResult::Yes {
|
||||
let remember = MessageDialog::new()
|
||||
.set_title("Remember Fingerprint?")
|
||||
.set_level(MessageLevel::Info)
|
||||
.set_description(
|
||||
"💾 Would you like to remember this fingerprint for future sessions?\n\
|
||||
This prevents asking again for the same contact."
|
||||
)
|
||||
.set_buttons(MessageButtons::YesNo)
|
||||
.show();
|
||||
|
||||
input.clear();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let remember_response = input.trim().to_lowercase();
|
||||
|
||||
if remember_response == "yes" {
|
||||
save_fingerprint(path, fingerprint, password)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
"no" => Ok(false),
|
||||
_ => {
|
||||
println!("Invalid input. Please enter 'yes' or 'no'.");
|
||||
request_user_confirmation(fingerprint, own_fingerprint, password)
|
||||
if remember == MessageDialogResult::Yes {
|
||||
save_fingerprint(path, fingerprint, password)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,57 +202,6 @@ fn generate_random_room_id() -> String {
|
||||
room_id
|
||||
}
|
||||
|
||||
fn load_blacklist(file_path: &str) -> HashSet<IpNetwork> {
|
||||
match fs::read_to_string(file_path) {
|
||||
Ok(contents) => contents
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim();
|
||||
IpNetwork::from_str(line).ok()
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_onion_site(url: &str) -> bool {
|
||||
url.contains(".onion")
|
||||
}
|
||||
|
||||
fn is_eepsite(url: &str) -> bool {
|
||||
url.contains(".i2p")
|
||||
}
|
||||
|
||||
fn resolve_dns(host: &str) -> Result<String, Box<dyn Error>> {
|
||||
let output = Command::new("dig")
|
||||
.args(["+short", host])
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let response = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if let Some(ip) = response
|
||||
.lines()
|
||||
.filter(|line| line.parse::<std::net::IpAddr>().is_ok())
|
||||
.next()
|
||||
{
|
||||
return Ok(ip.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Failed to resolve DNS to an IP address.".into())
|
||||
}
|
||||
|
||||
fn is_ip_blacklisted(ip: &str, blacklist: &HashSet<IpNetwork>) -> bool {
|
||||
|
||||
let ip: std::net::IpAddr = match ip.parse() {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
blacklist.iter().any(|range| range.contains(ip))
|
||||
}
|
||||
|
||||
fn pad_message(message: &str, max_length: usize) -> String {
|
||||
let current_length = message.len();
|
||||
|
||||
@ -262,176 +219,233 @@ fn pad_message(message: &str, max_length: usize) -> String {
|
||||
message.to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
choice: String,
|
||||
server_url: String,
|
||||
username: String,
|
||||
private_password: String,
|
||||
is_group_chat: bool,
|
||||
show_url_label: bool,
|
||||
room_id_input: String,
|
||||
room_password: String,
|
||||
error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
choice: "".into(),
|
||||
server_url: "".into(),
|
||||
username: "".into(),
|
||||
private_password: "".into(),
|
||||
is_group_chat: false,
|
||||
show_url_label: false,
|
||||
room_id_input: "".into(),
|
||||
room_password: "".into(),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{io::{self, Write}, thread, time::Duration};
|
||||
let mut options = eframe::NativeOptions::default();
|
||||
|
||||
options.viewport.resizable = Some(false);
|
||||
|
||||
options.viewport.inner_size = Some(egui::vec2(600.0, 900.0));
|
||||
|
||||
eframe::run_native("Messaging Setup", options, Box::new(|_cc| Box::new(SetupApp::default())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct SetupApp {
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
impl Default for SetupApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: AppState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for SetupApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(20.0);
|
||||
ui.heading(egui::RichText::new("Amnezichat").size(40.0));
|
||||
ui.add_space(30.0);
|
||||
|
||||
egui::Frame::group(ui.style()).inner_margin(egui::style::Margin::symmetric(20.0, 20.0)).show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("Choose an action:").size(24.0));
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
|
||||
ui.add_space(20.0);
|
||||
if ui.add(
|
||||
egui::Button::new(egui::RichText::new("➕ Create Room").size(24.0))
|
||||
.min_size(egui::vec2(200.0, 60.0))
|
||||
.fill(egui::Color32::from_rgb(50, 50, 50))
|
||||
).clicked() {
|
||||
self.state.choice = "create".into();
|
||||
self.state.room_id_input = generate_random_room_id();
|
||||
}
|
||||
ui.add_space(100.0);
|
||||
if ui.add(
|
||||
egui::Button::new(egui::RichText::new("🔗 Join Room").size(24.0))
|
||||
.min_size(egui::vec2(200.0, 60.0))
|
||||
.fill(egui::Color32::from_rgb(50, 50, 50))
|
||||
).clicked() {
|
||||
self.state.choice = "join".into();
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
||||
match self.state.choice.as_str() {
|
||||
"join" => {
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("🔑 Enter Room ID:").size(22.0));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.state.room_id_input)
|
||||
.font(egui::TextStyle::Heading)
|
||||
.desired_width(300.0)
|
||||
);
|
||||
}
|
||||
"create" => {
|
||||
if !self.state.room_id_input.is_empty() {
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("🆔 Generated Room ID:").size(22.0));
|
||||
ui.code(egui::RichText::new(&self.state.room_id_input).size(20.0));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(30.0);
|
||||
|
||||
egui::Frame::group(ui.style()).inner_margin(egui::style::Margin::symmetric(20.0, 20.0)).show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading(egui::RichText::new("🔧 Connection Details").size(36.0));
|
||||
ui.add_space(20.0);
|
||||
|
||||
egui::Grid::new("connection_details")
|
||||
.num_columns(2)
|
||||
.spacing([50.0, 16.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("Server URL:").size(22.0));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.state.server_url)
|
||||
.font(egui::TextStyle::Heading)
|
||||
.desired_width(300.0)
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label(egui::RichText::new("Username:").size(22.0));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.state.username)
|
||||
.font(egui::TextStyle::Heading)
|
||||
.desired_width(300.0)
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label(egui::RichText::new("Private Password:").size(22.0));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.state.private_password)
|
||||
.password(true)
|
||||
.font(egui::TextStyle::Heading)
|
||||
.desired_width(300.0)
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.add_space(20.0);
|
||||
ui.checkbox(&mut self.state.is_group_chat, egui::RichText::new("👥 Is Group Chat?").size(22.0));
|
||||
|
||||
if self.state.is_group_chat {
|
||||
ui.add_space(20.0);
|
||||
ui.label(egui::RichText::new("🔒 Room Password (min 8 chars):").size(22.0));
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.state.room_password)
|
||||
.font(egui::TextStyle::Heading)
|
||||
.desired_width(300.0)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(30.0);
|
||||
|
||||
if ui.add(
|
||||
egui::Button::new(egui::RichText::new("🚀 Start Messaging").size(28.0))
|
||||
.fill(egui::Color32::from_rgb(0, 100, 0))
|
||||
.min_size(egui::vec2(250.0, 60.0))
|
||||
).clicked() {
|
||||
if let Err(err) = validate_and_start(self.state.clone()) {
|
||||
self.state.error_message = Some(err.to_string());
|
||||
} else {
|
||||
|
||||
self.state.show_url_label = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = &self.state.error_message {
|
||||
ui.add_space(20.0);
|
||||
ui.colored_label(egui::Color32::RED, egui::RichText::new(format!("❗ {}", err)).size(22.0));
|
||||
}
|
||||
|
||||
if self.state.show_url_label {
|
||||
ui.add_space(20.0);
|
||||
ui.label(egui::RichText::new("Open http://127.0.0.1:8000 in your web browser").size(22.0));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_and_start(state: AppState) -> Result<(), Box<dyn Error>> {
|
||||
if state.server_url.is_empty() || state.username.is_empty() || state.private_password.is_empty() {
|
||||
return Err("Please fill in all fields.".into());
|
||||
}
|
||||
if state.is_group_chat && state.room_password.len() <= 8 {
|
||||
return Err("Room password must be longer than 8 characters.".into());
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = run_app_logic(state) {
|
||||
eprintln!("App error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app_logic(state: AppState) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let sigalg = sig::Sig::new(sig::Algorithm::Dilithium5)?;
|
||||
|
||||
println!("Would you like to create a new room or join an existing one?");
|
||||
println!("Type 'create' to create a new room or 'join' to join an existing one.");
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
let choice = choice.trim();
|
||||
let room_id = state.room_id_input.clone();
|
||||
let url = state.server_url.clone();
|
||||
let username = state.username.clone();
|
||||
let private_password = state.private_password.clone();
|
||||
|
||||
let room_id = match choice {
|
||||
"create" => {
|
||||
let new_room_id = generate_random_room_id();
|
||||
println!("Generated new room ID: {}", new_room_id);
|
||||
new_room_id
|
||||
}
|
||||
"join" => {
|
||||
println!("Enter the room ID to join:");
|
||||
let mut room_input = String::new();
|
||||
io::stdin().read_line(&mut room_input)?;
|
||||
room_input.trim().to_string()
|
||||
}
|
||||
_ => {
|
||||
println!("Invalid choice. Please restart the program and choose 'create' or 'join'.");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let blacklist_file = "cloudflare-ip-blacklist.txt";
|
||||
if !Path::new(blacklist_file).exists() {
|
||||
println!("File '{}' not found. Fetching from Codeberg...", blacklist_file);
|
||||
|
||||
let url = "https://codeberg.org/umutcamliyurt/Amnezichat/raw/branch/main/client/cloudflare-ip-blacklist.txt";
|
||||
let response = get(url)?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let content = response.text()?;
|
||||
|
||||
let mut file = File::create(blacklist_file)?;
|
||||
file.write_all(content.as_bytes())?;
|
||||
println!("File fetched and saved as '{}'.", blacklist_file);
|
||||
} else {
|
||||
println!("Failed to fetch the file from URL.");
|
||||
return Err("Failed to fetch blacklist.".into());
|
||||
}
|
||||
}
|
||||
|
||||
let blacklist = load_blacklist("cloudflare-ip-blacklist.txt");
|
||||
|
||||
let mut input = String::new();
|
||||
print!("Enter the server URL: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let url = input.trim().to_string();
|
||||
input.clear();
|
||||
|
||||
if is_onion_site(&url) {
|
||||
println!("This is an .onion site. Skipping IP check.");
|
||||
}
|
||||
else if is_eepsite(&url)
|
||||
{
|
||||
println!("This is an .i2p site. Skipping IP check.");
|
||||
}
|
||||
else {
|
||||
|
||||
let host = url
|
||||
.split('/')
|
||||
.nth(2)
|
||||
.unwrap_or(&url)
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or(&url);
|
||||
|
||||
match resolve_dns(host) {
|
||||
Ok(ip) => {
|
||||
|
||||
if is_ip_blacklisted(&ip, &blacklist) {
|
||||
println!("WARNING! The IP {} is in the blacklist.", ip);
|
||||
println!("The server you're trying to access is behind a Cloudflare reverse proxy.");
|
||||
println!("Proceed with caution as this setup may expose you to several potential risks:");
|
||||
println!();
|
||||
println!("Deanonymization attacks (including 0-click exploits)");
|
||||
println!("Metadata leaks");
|
||||
println!("Encryption vulnerabilities");
|
||||
println!("AI-based traffic analysis");
|
||||
println!("Connectivity issues");
|
||||
println!("Other undetected malicious behavior");
|
||||
println!();
|
||||
println!("What you can do:");
|
||||
println!("1. Choose a different server");
|
||||
println!("2. Self-host your own server");
|
||||
println!("3. Proceed anyway (Dangerous!)");
|
||||
println!();
|
||||
println!("For more info: https://git.calitabby.net/mirrors/deCloudflare");
|
||||
println!();
|
||||
println!("Do you want to proceed? (yes/no)");
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.expect("Failed to read input");
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
match input.as_str() {
|
||||
"yes" | "y" => {
|
||||
println!("Proceeding...");
|
||||
}
|
||||
"no" | "n" => {
|
||||
println!("Operation aborted!");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
println!("Invalid input. Please enter 'yes' or 'no'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to resolve IP for the server: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print!("Enter your username: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let username = input.trim().to_string();
|
||||
input.clear();
|
||||
|
||||
print!("Enter private key encryption password: ");
|
||||
io::stdout().flush()?;
|
||||
let private_password = read_password()?.to_string();
|
||||
|
||||
println!("Is this a group chat? (yes/no): ");
|
||||
let mut is_group_chat = String::new();
|
||||
io::stdin().read_line(&mut is_group_chat)?;
|
||||
let is_group_chat = is_group_chat.trim().to_lowercase() == "yes";
|
||||
|
||||
let room_password = if is_group_chat {
|
||||
|
||||
loop {
|
||||
print!("Enter room password (must be longer than 8 characters): ");
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let password_input = input.trim();
|
||||
if password_input.len() > 8 {
|
||||
break password_input.to_string();
|
||||
} else {
|
||||
println!("Error: Password must be longer than 8 characters. Please try again.");
|
||||
}
|
||||
}
|
||||
let room_password = if state.is_group_chat {
|
||||
let salt = derive_salt_from_password(&state.room_password);
|
||||
let key = derive_key(&state.room_password, &salt);
|
||||
hex::encode(key)
|
||||
} else {
|
||||
|
||||
String::new()
|
||||
};
|
||||
|
||||
let room_password = if is_group_chat {
|
||||
let salt = derive_salt_from_password(&room_password);
|
||||
let key = derive_key(&room_password, &salt);
|
||||
hex::encode(key)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if is_group_chat {
|
||||
if state.is_group_chat {
|
||||
println!("Skipping key exchange. Using room password as shared secret.");
|
||||
let hybrid_shared_secret = room_password.clone();
|
||||
let hybrid_shared_secret = room_password.clone();
|
||||
println!("Shared secret established.");
|
||||
println!("You can now start messaging!");
|
||||
|
||||
@ -482,7 +496,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
&room_id_locked,
|
||||
&shared_hybrid_secret,
|
||||
&url_locked,
|
||||
true,
|
||||
true,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("Error fetching messages: {}", e),
|
||||
@ -506,14 +520,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = random_data_thread.join() {
|
||||
eprintln!("Random data thread terminated with error: {:?}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = fetch_thread.join() {
|
||||
eprintln!("Fetch thread terminated with error: {:?}", e);
|
||||
}
|
||||
|
||||
random_data_thread.join().ok();
|
||||
fetch_thread.join().ok();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -412,7 +412,6 @@ pub fn send_encrypted_message(
|
||||
.send()?;
|
||||
|
||||
if res.status().is_success() {
|
||||
println!("Message sent successfully.");
|
||||
} else {
|
||||
eprintln!("Failed to send message: {}", res.status());
|
||||
}
|
||||
|
@ -13,37 +13,53 @@
|
||||
<div class="input-container">
|
||||
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off" />
|
||||
<button onclick="sendMessage()">Send</button>
|
||||
<button onclick="document.getElementById('mediaInput').click()">📷</button>
|
||||
<button onclick="startVoiceRecording()" id="micButton">🎤</button>
|
||||
<button onclick="toggleSettings()">Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recordingStatus" style="display:none; align-items:center; gap:10px; margin-top: 10px;">
|
||||
<span id="timer">00:00</span>
|
||||
<button onclick="cancelVoiceRecording()" style="background-color:red; color:white;">Cancel ❌</button>
|
||||
</div>
|
||||
|
||||
<div id="settings">
|
||||
<div class="settings-content">
|
||||
<h2>Settings</h2>
|
||||
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)" />
|
||||
<button onclick="document.getElementById('profilePicInput').click()">Choose Profile Picture</button>
|
||||
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)" />
|
||||
<button onclick="document.getElementById('mediaInput').click()">Send Media</button>
|
||||
<button onclick="toggleTheme()">Toggle Light/Dark Mode</button>
|
||||
<button onclick="closeSettings()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Modal -->
|
||||
<div id="mediaModal" onclick="closeMediaModal()">
|
||||
<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">×</span>
|
||||
</div>
|
||||
|
||||
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)" />
|
||||
|
||||
<script>
|
||||
let profilePicBase64 = localStorage.getItem("profilePic") || "";
|
||||
let lastNotifiedMessages = new Set();
|
||||
let notifiedMessageSet = new Set(JSON.parse(localStorage.getItem("notifiedMessages") || "[]"));
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
let recordingStartTime;
|
||||
let recordingTimerInterval;
|
||||
|
||||
function hashMessage(message) {
|
||||
return message.replace(/<[^>]*>/g, '').trim();
|
||||
}
|
||||
|
||||
function showNotification(message) {
|
||||
if (!lastNotifiedMessages.has(message) && Notification.permission === "granted") {
|
||||
const messageHash = hashMessage(message);
|
||||
if (!notifiedMessageSet.has(messageHash) && Notification.permission === "granted") {
|
||||
new Notification("New Message", {
|
||||
body: message.replace(/<strong>|<\/strong>/g, '')
|
||||
body: messageHash
|
||||
});
|
||||
lastNotifiedMessages.add(message);
|
||||
notifiedMessageSet.add(messageHash);
|
||||
localStorage.setItem("notifiedMessages", JSON.stringify(Array.from(notifiedMessageSet)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,15 +86,13 @@
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
const mediaBase64 = reader.result;
|
||||
const mediaBase64 = DOMPurify.sanitize(reader.result);
|
||||
let message = `<pfp>${profilePicBase64}</pfp><media>${mediaBase64}</media>`;
|
||||
|
||||
await fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
|
||||
fetchMessages();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
@ -92,51 +106,70 @@
|
||||
|
||||
const base64Img = /^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,/;
|
||||
const base64Vid = /^data:video\/(mp4|webm|ogg);base64,/;
|
||||
const base64Audio = /^data:audio\/(webm|ogg);base64,/;
|
||||
|
||||
messagesDiv.innerHTML = messages.map(msg => {
|
||||
let messagesHTML = messages.map(msg => {
|
||||
const pfpMatch = msg.match(/<pfp>(.*?)<\/pfp>/);
|
||||
const mediaMatch = msg.match(/<media>(.*?)<\/media>/);
|
||||
const messageText = msg.replace(/<pfp>.*?<\/pfp>/, '').replace(/<media>.*?<\/media>/, '').trim();
|
||||
const audioMatch = msg.match(/<audio>(.*?)<\/audio>/);
|
||||
const messageTextRaw = msg.replace(/<pfp>.*?<\/pfp>/, '').replace(/<media>.*?<\/media>/, '').replace(/<audio>.*?<\/audio>/, '').trim();
|
||||
const messageText = DOMPurify.sanitize(messageTextRaw);
|
||||
|
||||
const profilePicSrc = pfpMatch && base64Img.test(pfpMatch[1])
|
||||
? pfpMatch[1]
|
||||
? DOMPurify.sanitize(pfpMatch[1])
|
||||
: '/static/default_pfp.jpg';
|
||||
|
||||
const profilePic = `<img src="${profilePicSrc}" class="profile-pic" alt="Profile Picture">`;
|
||||
|
||||
let media = "";
|
||||
if (mediaMatch) {
|
||||
const src = mediaMatch[1];
|
||||
const src = DOMPurify.sanitize(mediaMatch[1]);
|
||||
if (base64Img.test(src)) {
|
||||
media = `<img src="${src}" class="media-img" alt="Media" onclick="openMediaModal('${src}', false)">`;
|
||||
} else if (base64Vid.test(src)) {
|
||||
media = `<video class="media-video" controls onclick="openMediaModal('${src}', true)"><source src="${src}" type="video/mp4">Your browser does not support video.</video>`;
|
||||
media = `<video class="media-video" controls onclick="openMediaModal('${src}', true)">
|
||||
<source src="${src}" type="video/mp4">Your browser does not support video.
|
||||
</video>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (audioMatch) {
|
||||
const src = DOMPurify.sanitize(audioMatch[1]);
|
||||
media = `<audio controls class="media-audio">
|
||||
<source src="${src}" type="audio/webm">Your browser does not support the audio element.
|
||||
</audio>`;
|
||||
}
|
||||
|
||||
showNotification(messageText || 'New media message');
|
||||
|
||||
return `
|
||||
<div class="message-row">
|
||||
${profilePic}
|
||||
<div class="message-bubble">
|
||||
${messageText ? `<p>${DOMPurify.sanitize(messageText)}</p>` : ''}
|
||||
${messageText ? `<p>${messageText}</p>` : ''}
|
||||
${media}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
messagesDiv.innerHTML = DOMPurify.sanitize(messagesHTML, {
|
||||
SAFE_FOR_JQUERY: true,
|
||||
ADD_ATTR: ['onclick']
|
||||
});
|
||||
|
||||
messagesDiv.scrollTo({ top: messagesDiv.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const msg = input.value.trim();
|
||||
|
||||
// Skip sending if message is truly empty
|
||||
if (!msg) return;
|
||||
|
||||
const sanitizedMsg = DOMPurify.sanitize(msg);
|
||||
const sanitizedMsg = DOMPurify.sanitize(msg, {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: []
|
||||
});
|
||||
|
||||
const message = `<pfp>${profilePicBase64}</pfp>${sanitizedMsg}`;
|
||||
|
||||
await fetch('/send', {
|
||||
@ -149,7 +182,6 @@
|
||||
fetchMessages();
|
||||
}
|
||||
|
||||
|
||||
function toggleSettings() {
|
||||
const modal = document.getElementById('settings');
|
||||
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||||
@ -200,6 +232,81 @@
|
||||
profilePicBase64 = savedPic;
|
||||
}
|
||||
});
|
||||
|
||||
function startVoiceRecording() {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
.then(stream => {
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
audioChunks = [];
|
||||
recordingStartTime = Date.now();
|
||||
|
||||
mediaRecorder.ondataavailable = event => {
|
||||
audioChunks.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
clearInterval(recordingTimerInterval);
|
||||
document.getElementById("recordingStatus").style.display = "none";
|
||||
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
if (!audioBlob || !audioChunks.length) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
const audioBase64 = DOMPurify.sanitize(reader.result);
|
||||
const message = `<pfp>${profilePicBase64}</pfp><audio>${audioBase64}</audio>`;
|
||||
await fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
fetchMessages();
|
||||
};
|
||||
reader.readAsDataURL(audioBlob);
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
startTimer();
|
||||
document.getElementById('micButton').innerText = '⏹️';
|
||||
document.getElementById('micButton').onclick = stopVoiceRecording;
|
||||
document.getElementById("recordingStatus").style.display = "flex";
|
||||
})
|
||||
.catch(err => {
|
||||
alert('Microphone access denied.');
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function stopVoiceRecording() {
|
||||
mediaRecorder.stop();
|
||||
document.getElementById('micButton').innerText = '🎤';
|
||||
document.getElementById('micButton').onclick = startVoiceRecording;
|
||||
}
|
||||
|
||||
function cancelVoiceRecording() {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
audioChunks = [];
|
||||
clearInterval(recordingTimerInterval);
|
||||
document.getElementById("recordingStatus").style.display = "none";
|
||||
document.getElementById('micButton').innerText = '🎤';
|
||||
document.getElementById('micButton').onclick = startVoiceRecording;
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
const timerElement = document.getElementById('timer');
|
||||
recordingTimerInterval = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
|
||||
const minutes = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
||||
const seconds = String(elapsed % 60).padStart(2, '0');
|
||||
timerElement.textContent = `${minutes}:${seconds}`;
|
||||
|
||||
if (elapsed >= 60) {
|
||||
cancelVoiceRecording();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -2,113 +2,129 @@ html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #1e1f22;
|
||||
background-color: #121212;
|
||||
color: #e1e1e1;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s, box-shadow 0.3s;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Messages area */
|
||||
#messages {
|
||||
flex-grow: 1;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #333b41;
|
||||
background-color: #2c2f36;
|
||||
border-radius: 10px;
|
||||
background: rgba(44, 47, 54, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
background-color: #444c56;
|
||||
padding: 10px 15px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(to bottom right, #3a3f4b, #2e333d);
|
||||
padding: 12px 18px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
word-break: break-word;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.message-bubble p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Media content */
|
||||
.media-img,
|
||||
.media-video {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* Input section */
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #444c56;
|
||||
background-color: #2c2f36;
|
||||
padding: 12px 16px;
|
||||
border-radius: 30px;
|
||||
border: 1px solid #3a3f4b;
|
||||
background-color: #1f2128;
|
||||
color: #e1e1e1;
|
||||
font-size: 15px;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #4c8bf5;
|
||||
box-shadow: 0 0 0 3px rgba(76, 139, 245, 0.25);
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
padding: 10px 22px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
background-color: #4c8bf5;
|
||||
border-radius: 30px;
|
||||
background: linear-gradient(135deg, #4c8bf5, #3b74d4);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #3978d1;
|
||||
background: linear-gradient(135deg, #3b74d4, #2f5eb8);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #2962a1;
|
||||
background: #2f5eb8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Settings overlay */
|
||||
@ -119,25 +135,28 @@ button:active {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(6px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Settings content */
|
||||
.settings-content {
|
||||
background-color: #2c2f36;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 300px;
|
||||
background-color: #1f2229;
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 14px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.settings-content h2 {
|
||||
margin-top: 0;
|
||||
color: #e1e1e1;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
@ -150,7 +169,7 @@ button:active {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
background-color: rgba(0,0,0,0.85);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
@ -160,7 +179,7 @@ button:active {
|
||||
#mediaModal video {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@ -168,28 +187,28 @@ button:active {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
font-size: 30px;
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* ============================= */
|
||||
/* Light Theme */
|
||||
/* ============================= */
|
||||
/* Light Theme Modernization */
|
||||
body.light-theme {
|
||||
background-color: #f9f9f9;
|
||||
background-color: #f1f3f5;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.light-theme #messages {
|
||||
background-color: #ffffff;
|
||||
border-color: #dcdcdc;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body.light-theme .message-bubble {
|
||||
background-color: #f1f3f5;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
body.light-theme input[type="text"] {
|
||||
@ -200,26 +219,18 @@ body.light-theme input[type="text"] {
|
||||
|
||||
body.light-theme input[type="text"]:focus {
|
||||
border-color: #4c8bf5;
|
||||
box-shadow: 0 0 0 2px rgba(76, 139, 245, 0.2);
|
||||
box-shadow: 0 0 0 3px rgba(76, 139, 245, 0.2);
|
||||
}
|
||||
|
||||
body.light-theme button {
|
||||
background-color: #4c8bf5;
|
||||
background: linear-gradient(135deg, #4c8bf5, #3b74d4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
body.light-theme button:hover {
|
||||
background-color: #3978d1;
|
||||
}
|
||||
|
||||
body.light-theme button:active {
|
||||
background-color: #2962a1;
|
||||
}
|
||||
|
||||
body.light-theme .settings-content {
|
||||
background-color: #ffffff;
|
||||
color: #1a1a1a;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.light-theme .settings-content h2 {
|
||||
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 333 KiB |
@ -1,7 +1,5 @@
|
||||
# Use Debian 12 as the base image
|
||||
FROM debian:12
|
||||
|
||||
# Install system dependencies, Tor, and Rust
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
build-essential \
|
||||
@ -10,30 +8,22 @@ RUN apt-get update && apt-get install -y \
|
||||
&& curl https://sh.rustup.rs -sSf | sh -s -- -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add Rust to PATH
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Clone the repository
|
||||
RUN git clone https://github.com/umutcamliyurt/Amnezichat.git .
|
||||
|
||||
# Navigate to the server directory
|
||||
WORKDIR /app/server
|
||||
|
||||
# Build the Rust project in release mode
|
||||
RUN cargo build --release
|
||||
|
||||
# Expose port 8080 for the application
|
||||
EXPOSE 8080
|
||||
|
||||
# Configure Tor hidden service
|
||||
RUN mkdir -p /var/lib/tor/hidden_service && \
|
||||
echo "HiddenServiceDir /var/lib/tor/hidden_service" >> /etc/tor/torrc && \
|
||||
echo "HiddenServicePort 80 127.0.0.1:8080" >> /etc/tor/torrc && \
|
||||
chown -R debian-tor:debian-tor /var/lib/tor/hidden_service && \
|
||||
chmod 700 /var/lib/tor/hidden_service
|
||||
|
||||
# Start Tor and the application
|
||||
CMD service tor start && sleep 5 && cat /var/lib/tor/hidden_service/hostname && cargo run --release
|
@ -63,6 +63,11 @@ cd Amnezichat/client/
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
</pre>
|
||||
|
||||
<!-- Download AppImage -->
|
||||
<h3>Or Download Prebuilt AppImage (Linux)</h3>
|
||||
<p>If you prefer a ready-to-use version, download the AppImage below:</p>
|
||||
<a href="/static/Amnezichat-x86_64.AppImage" class="download-button" download>Download AppImage</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user