Add voice messages

This commit is contained in:
Umut Çamliyurt 2025-04-19 13:30:28 +03:00 committed by umutcamliyurt
parent ec7027c694
commit 899f9cae6c
12 changed files with 523 additions and 378 deletions

View File

@ -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/)

View File

@ -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/)

View File

@ -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
View 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

View File

@ -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

View File

@ -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(());
}

View File

@ -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());
}

View File

@ -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();">&times;</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>

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

@ -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

View File

@ -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>