mirror of
https://github.com/umutcamliyurt/Amnezichat.git
synced 2025-05-05 08:00: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
|
# Amnezichat
|
||||||
|
|
||||||
<img src="banner.png" width="1000">
|
<img src="banner.png" width="1200">
|
||||||
|
|
||||||
## Anti-forensic and secure messenger
|
## Anti-forensic and secure messenger
|
||||||
<!-- DESCRIPTION -->
|
<!-- 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
|
- EdDSA and Dilithium5 for authentication, ECDH and Kyber1024 for key exchange, encryption using ChaCha20-Poly1305
|
||||||
|
|
||||||
<!-- INSTALLATION -->
|
<!-- INSTALLATION -->
|
||||||
## Basic server setup:
|
## Server setup:
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install curl build-essential git
|
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 build --release
|
||||||
cargo run --release
|
cargo run --release
|
||||||
|
|
||||||
## Onionsite setup with Docker:
|
## Server setup with Docker:
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install docker.io git
|
sudo apt install docker.io git
|
||||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||||
cd Amnezichat/
|
cd Amnezichat/server/
|
||||||
sudo docker build -t amnezichat:latest .
|
sudo docker build -t amnezichatserver:latest .
|
||||||
sudo docker run -p 8080:8080 amnezichat:latest
|
sudo docker run -p 8080:8080 amnezichatserver:latest
|
||||||
|
|
||||||
## Client usage:
|
## Client setup:
|
||||||
|
|
||||||
**For Web UI connect to http://localhost:8000**
|
**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 build --release
|
||||||
cargo run --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:
|
## Requirements:
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/)
|
- [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
|
# Amnezichat
|
||||||
|
|
||||||
<img src="banner.png" width="1000">
|
<img src="banner.png" width="1200">
|
||||||
|
|
||||||
## İz bırakmayan güvenli mesajlaşma
|
## İz bırakmayan güvenli mesajlaşma
|
||||||
<!-- AÇIKLAMA -->
|
<!-- 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 build --release
|
||||||
cargo run --release
|
cargo run --release
|
||||||
|
|
||||||
## Docker ile Onion sitesi kurulumu:
|
## Docker ile sunucu kurulumu:
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install docker.io git
|
sudo apt install docker.io git
|
||||||
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
git clone https://github.com/umutcamliyurt/Amnezichat.git
|
||||||
cd Amnezichat/
|
cd Amnezichat/server/
|
||||||
sudo docker build -t amnezichat:latest .
|
sudo docker build -t amnezichatserver:latest .
|
||||||
sudo docker run -p 8080:8080 amnezichat: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**
|
**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 build --release
|
||||||
cargo run --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:
|
## Gereksinimler:
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/)
|
- [Rust](https://www.rust-lang.org), [Tor](https://gitlab.torproject.org/tpo/core/tor), [I2P](https://i2pd.website/)
|
||||||
|
@ -22,3 +22,6 @@ sha3 = "0.10.8"
|
|||||||
x25519-dalek = "2.0.1"
|
x25519-dalek = "2.0.1"
|
||||||
ed25519-dalek = "2.1.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::*;
|
||||||
use oqs::sig::{Sig, PublicKey, SecretKey, Algorithm as SigAlgorithm};
|
use oqs::sig::{Sig, PublicKey, SecretKey, Algorithm as SigAlgorithm};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use reqwest::blocking::get;
|
|
||||||
use std::fs;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
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 hex;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use rpassword::read_password;
|
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
error::Error,
|
error::Error,
|
||||||
};
|
};
|
||||||
use ipnetwork::IpNetwork;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use chacha20poly1305::aead::OsRng;
|
use chacha20poly1305::aead::OsRng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use sha3::{Sha3_512, Digest};
|
use sha3::{Sha3_512, Digest};
|
||||||
use ed25519_dalek::VerifyingKey as Ed25519PublicKey;
|
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] {
|
fn get_raw_bytes_public_key(pk: &PublicKey) -> &[u8] {
|
||||||
pk.as_ref()
|
pk.as_ref()
|
||||||
@ -95,7 +99,6 @@ fn request_user_confirmation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let path = "contact_fingerprints.enc";
|
let path = "contact_fingerprints.enc";
|
||||||
|
|
||||||
let trusted_fingerprints = load_trusted_fingerprints(path, password)?;
|
let trusted_fingerprints = load_trusted_fingerprints(path, password)?;
|
||||||
|
|
||||||
if trusted_fingerprints.contains(fingerprint) {
|
if trusted_fingerprints.contains(fingerprint) {
|
||||||
@ -103,34 +106,39 @@ fn request_user_confirmation(
|
|||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("The fingerprint of the received public key is: {}", fingerprint);
|
let message = format!(
|
||||||
print!("Do you confirm this fingerprint? (yes/no): ");
|
"🔒 Fingerprint Verification\n\n\
|
||||||
io::stdout().flush()?;
|
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();
|
let confirm = MessageDialog::new()
|
||||||
io::stdin().read_line(&mut input)?;
|
.set_title("Trust New Fingerprint")
|
||||||
let response = input.trim().to_lowercase();
|
.set_level(MessageLevel::Info)
|
||||||
|
.set_description(&message)
|
||||||
|
.set_buttons(MessageButtons::YesNo)
|
||||||
|
.show();
|
||||||
|
|
||||||
match response.as_str() {
|
if confirm == MessageDialogResult::Yes {
|
||||||
"yes" => {
|
let remember = MessageDialog::new()
|
||||||
print!("Would you like to remember this fingerprint for future sessions? (yes/no): ");
|
.set_title("Remember Fingerprint?")
|
||||||
io::stdout().flush()?;
|
.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();
|
if remember == MessageDialogResult::Yes {
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
let remember_response = input.trim().to_lowercase();
|
|
||||||
|
|
||||||
if remember_response == "yes" {
|
|
||||||
save_fingerprint(path, fingerprint, password)?;
|
save_fingerprint(path, fingerprint, password)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
} else {
|
||||||
"no" => Ok(false),
|
Ok(false)
|
||||||
_ => {
|
|
||||||
println!("Invalid input. Please enter 'yes' or 'no'.");
|
|
||||||
request_user_confirmation(fingerprint, own_fingerprint, password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,57 +202,6 @@ fn generate_random_room_id() -> String {
|
|||||||
room_id
|
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 {
|
fn pad_message(message: &str, max_length: usize) -> String {
|
||||||
let current_length = message.len();
|
let current_length = message.len();
|
||||||
|
|
||||||
@ -262,174 +219,231 @@ fn pad_message(message: &str, max_length: usize) -> String {
|
|||||||
message.to_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>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
use std::sync::{Arc, Mutex};
|
let mut options = eframe::NativeOptions::default();
|
||||||
use std::{io::{self, Write}, thread, time::Duration};
|
|
||||||
|
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)?;
|
let sigalg = sig::Sig::new(sig::Algorithm::Dilithium5)?;
|
||||||
|
|
||||||
println!("Would you like to create a new room or join an existing one?");
|
let room_id = state.room_id_input.clone();
|
||||||
println!("Type 'create' to create a new room or 'join' to join an existing one.");
|
let url = state.server_url.clone();
|
||||||
let mut choice = String::new();
|
let username = state.username.clone();
|
||||||
io::stdin().read_line(&mut choice)?;
|
let private_password = state.private_password.clone();
|
||||||
let choice = choice.trim();
|
|
||||||
|
|
||||||
let room_id = match choice {
|
let room_password = if state.is_group_chat {
|
||||||
"create" => {
|
let salt = derive_salt_from_password(&state.room_password);
|
||||||
let new_room_id = generate_random_room_id();
|
let key = derive_key(&state.room_password, &salt);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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)
|
hex::encode(key)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_group_chat {
|
if state.is_group_chat {
|
||||||
println!("Skipping key exchange. Using room password as shared secret.");
|
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!("Shared secret established.");
|
||||||
@ -506,14 +520,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = random_data_thread.join() {
|
random_data_thread.join().ok();
|
||||||
eprintln!("Random data thread terminated with error: {:?}", e);
|
fetch_thread.join().ok();
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = fetch_thread.join() {
|
|
||||||
eprintln!("Fetch thread terminated with error: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,7 +412,6 @@ pub fn send_encrypted_message(
|
|||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
if res.status().is_success() {
|
if res.status().is_success() {
|
||||||
println!("Message sent successfully.");
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to send message: {}", res.status());
|
eprintln!("Failed to send message: {}", res.status());
|
||||||
}
|
}
|
||||||
|
@ -13,37 +13,53 @@
|
|||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off" />
|
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off" />
|
||||||
<button onclick="sendMessage()">Send</button>
|
<button onclick="sendMessage()">Send</button>
|
||||||
|
<button onclick="document.getElementById('mediaInput').click()">📷</button>
|
||||||
|
<button onclick="startVoiceRecording()" id="micButton">🎤</button>
|
||||||
<button onclick="toggleSettings()">Settings</button>
|
<button onclick="toggleSettings()">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
</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 id="settings">
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)" />
|
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)" />
|
||||||
<button onclick="document.getElementById('profilePicInput').click()">Choose Profile Picture</button>
|
<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="toggleTheme()">Toggle Light/Dark Mode</button>
|
||||||
<button onclick="closeSettings()">Close</button>
|
<button onclick="closeSettings()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Modal -->
|
|
||||||
<div id="mediaModal" onclick="closeMediaModal()">
|
<div id="mediaModal" onclick="closeMediaModal()">
|
||||||
<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">×</span>
|
<span class="close-btn" onclick="closeMediaModal(); event.stopPropagation();">×</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let profilePicBase64 = localStorage.getItem("profilePic") || "";
|
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) {
|
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", {
|
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) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = async () => {
|
reader.onloadend = async () => {
|
||||||
const mediaBase64 = reader.result;
|
const mediaBase64 = DOMPurify.sanitize(reader.result);
|
||||||
let message = `<pfp>${profilePicBase64}</pfp><media>${mediaBase64}</media>`;
|
let message = `<pfp>${profilePicBase64}</pfp><media>${mediaBase64}</media>`;
|
||||||
|
|
||||||
await fetch('/send', {
|
await fetch('/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ message })
|
body: JSON.stringify({ message })
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchMessages();
|
fetchMessages();
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@ -92,51 +106,70 @@
|
|||||||
|
|
||||||
const base64Img = /^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,/;
|
const base64Img = /^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,/;
|
||||||
const base64Vid = /^data:video\/(mp4|webm|ogg);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 pfpMatch = msg.match(/<pfp>(.*?)<\/pfp>/);
|
||||||
const mediaMatch = msg.match(/<media>(.*?)<\/media>/);
|
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])
|
const profilePicSrc = pfpMatch && base64Img.test(pfpMatch[1])
|
||||||
? pfpMatch[1]
|
? DOMPurify.sanitize(pfpMatch[1])
|
||||||
: '/static/default_pfp.jpg';
|
: '/static/default_pfp.jpg';
|
||||||
|
|
||||||
const profilePic = `<img src="${profilePicSrc}" class="profile-pic" alt="Profile Picture">`;
|
const profilePic = `<img src="${profilePicSrc}" class="profile-pic" alt="Profile Picture">`;
|
||||||
|
|
||||||
let media = "";
|
let media = "";
|
||||||
if (mediaMatch) {
|
if (mediaMatch) {
|
||||||
const src = mediaMatch[1];
|
const src = DOMPurify.sanitize(mediaMatch[1]);
|
||||||
if (base64Img.test(src)) {
|
if (base64Img.test(src)) {
|
||||||
media = `<img src="${src}" class="media-img" alt="Media" onclick="openMediaModal('${src}', false)">`;
|
media = `<img src="${src}" class="media-img" alt="Media" onclick="openMediaModal('${src}', false)">`;
|
||||||
} else if (base64Vid.test(src)) {
|
} 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');
|
showNotification(messageText || 'New media message');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="message-row">
|
<div class="message-row">
|
||||||
${profilePic}
|
${profilePic}
|
||||||
<div class="message-bubble">
|
<div class="message-bubble">
|
||||||
${messageText ? `<p>${DOMPurify.sanitize(messageText)}</p>` : ''}
|
${messageText ? `<p>${messageText}</p>` : ''}
|
||||||
${media}
|
${media}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
messagesDiv.innerHTML = DOMPurify.sanitize(messagesHTML, {
|
||||||
|
SAFE_FOR_JQUERY: true,
|
||||||
|
ADD_ATTR: ['onclick']
|
||||||
|
});
|
||||||
|
|
||||||
messagesDiv.scrollTo({ top: messagesDiv.scrollHeight, behavior: 'smooth' });
|
messagesDiv.scrollTo({ top: messagesDiv.scrollHeight, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const input = document.getElementById('messageInput');
|
const input = document.getElementById('messageInput');
|
||||||
const msg = input.value.trim();
|
const msg = input.value.trim();
|
||||||
|
|
||||||
// Skip sending if message is truly empty
|
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
const sanitizedMsg = DOMPurify.sanitize(msg);
|
const sanitizedMsg = DOMPurify.sanitize(msg, {
|
||||||
|
ALLOWED_TAGS: [],
|
||||||
|
ALLOWED_ATTR: []
|
||||||
|
});
|
||||||
|
|
||||||
const message = `<pfp>${profilePicBase64}</pfp>${sanitizedMsg}`;
|
const message = `<pfp>${profilePicBase64}</pfp>${sanitizedMsg}`;
|
||||||
|
|
||||||
await fetch('/send', {
|
await fetch('/send', {
|
||||||
@ -149,7 +182,6 @@
|
|||||||
fetchMessages();
|
fetchMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleSettings() {
|
function toggleSettings() {
|
||||||
const modal = document.getElementById('settings');
|
const modal = document.getElementById('settings');
|
||||||
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||||||
@ -200,6 +232,81 @@
|
|||||||
profilePicBase64 = savedPic;
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -2,113 +2,129 @@ html, body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
background-color: #1e1f22;
|
background-color: #121212;
|
||||||
color: #e1e1e1;
|
color: #e1e1e1;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
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 {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px;
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Messages area */
|
/* Messages area */
|
||||||
#messages {
|
#messages {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 15px;
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid #333b41;
|
background: rgba(44, 47, 54, 0.7);
|
||||||
background-color: #2c2f36;
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 10px;
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-row {
|
.message-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-pic {
|
.profile-pic {
|
||||||
width: 50px;
|
width: 48px;
|
||||||
height: 50px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background-color: #444c56;
|
background: linear-gradient(to bottom right, #3a3f4b, #2e333d);
|
||||||
padding: 10px 15px;
|
padding: 12px 18px;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble p {
|
.message-bubble p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.6;
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media content */
|
/* Media content */
|
||||||
.media-img,
|
.media-img,
|
||||||
.media-video {
|
.media-video {
|
||||||
width: 500px;
|
width: 480px;
|
||||||
height: 500px;
|
height: 480px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input section */
|
/* Input section */
|
||||||
.input-container {
|
.input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 10px;
|
padding: 12px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 30px;
|
||||||
border: 1px solid #444c56;
|
border: 1px solid #3a3f4b;
|
||||||
background-color: #2c2f36;
|
background-color: #1f2128;
|
||||||
color: #e1e1e1;
|
color: #e1e1e1;
|
||||||
|
font-size: 15px;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:focus {
|
input[type="text"]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #4c8bf5;
|
border-color: #4c8bf5;
|
||||||
|
box-shadow: 0 0 0 3px rgba(76, 139, 245, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button styles */
|
/* Button styles */
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 22px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 20px;
|
border-radius: 30px;
|
||||||
background-color: #4c8bf5;
|
background: linear-gradient(135deg, #4c8bf5, #3b74d4);
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #3978d1;
|
background: linear-gradient(135deg, #3b74d4, #2f5eb8);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
button:active {
|
||||||
background-color: #2962a1;
|
background: #2f5eb8;
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings overlay */
|
/* Settings overlay */
|
||||||
@ -119,25 +135,28 @@ button:active {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings content */
|
/* Settings content */
|
||||||
.settings-content {
|
.settings-content {
|
||||||
background-color: #2c2f36;
|
background-color: #1f2229;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
width: 300px;
|
width: 320px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 14px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-content h2 {
|
.settings-content h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #e1e1e1;
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal styles */
|
/* Modal styles */
|
||||||
@ -150,7 +169,7 @@ button:active {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: rgba(0,0,0,0.8);
|
background-color: rgba(0,0,0,0.85);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@ -160,7 +179,7 @@ button:active {
|
|||||||
#mediaModal video {
|
#mediaModal video {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,28 +187,28 @@ button:active {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
font-size: 30px;
|
font-size: 32px;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================= */
|
/* Light Theme Modernization */
|
||||||
/* Light Theme */
|
|
||||||
/* ============================= */
|
|
||||||
body.light-theme {
|
body.light-theme {
|
||||||
background-color: #f9f9f9;
|
background-color: #f1f3f5;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme #messages {
|
body.light-theme #messages {
|
||||||
background-color: #ffffff;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
border-color: #dcdcdc;
|
backdrop-filter: blur(12px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme .message-bubble {
|
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"] {
|
body.light-theme input[type="text"] {
|
||||||
@ -200,26 +219,18 @@ body.light-theme input[type="text"] {
|
|||||||
|
|
||||||
body.light-theme input[type="text"]:focus {
|
body.light-theme input[type="text"]:focus {
|
||||||
border-color: #4c8bf5;
|
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 {
|
body.light-theme button {
|
||||||
background-color: #4c8bf5;
|
background: linear-gradient(135deg, #4c8bf5, #3b74d4);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme button:hover {
|
|
||||||
background-color: #3978d1;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light-theme button:active {
|
|
||||||
background-color: #2962a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light-theme .settings-content {
|
body.light-theme .settings-content {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #1a1a1a;
|
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 {
|
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
|
FROM debian:12
|
||||||
|
|
||||||
# Install system dependencies, Tor, and Rust
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
build-essential \
|
build-essential \
|
||||||
@ -10,30 +8,22 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& curl https://sh.rustup.rs -sSf | sh -s -- -y \
|
&& curl https://sh.rustup.rs -sSf | sh -s -- -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add Rust to PATH
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Clone the repository
|
|
||||||
RUN git clone https://github.com/umutcamliyurt/Amnezichat.git .
|
RUN git clone https://github.com/umutcamliyurt/Amnezichat.git .
|
||||||
|
|
||||||
# Navigate to the server directory
|
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
|
|
||||||
# Build the Rust project in release mode
|
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# Expose port 8080 for the application
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Configure Tor hidden service
|
|
||||||
RUN mkdir -p /var/lib/tor/hidden_service && \
|
RUN mkdir -p /var/lib/tor/hidden_service && \
|
||||||
echo "HiddenServiceDir /var/lib/tor/hidden_service" >> /etc/tor/torrc && \
|
echo "HiddenServiceDir /var/lib/tor/hidden_service" >> /etc/tor/torrc && \
|
||||||
echo "HiddenServicePort 80 127.0.0.1:8080" >> /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 && \
|
chown -R debian-tor:debian-tor /var/lib/tor/hidden_service && \
|
||||||
chmod 700 /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
|
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 build --release
|
||||||
cargo run --release
|
cargo run --release
|
||||||
</pre>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user