mirror of
https://github.com/umutcamliyurt/Amnezichat.git
synced 2025-05-05 03:20:48 +01:00
Replace Egui with Web UI
This commit is contained in:
parent
64ac6eb981
commit
6cc3eb6a8b
@ -70,6 +70,8 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en
|
|||||||
|
|
||||||
## Client usage:
|
## Client usage:
|
||||||
|
|
||||||
|
**For Web UI connect to http://localhost:8000**
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install curl build-essential git tor
|
sudo apt install curl build-essential git tor
|
||||||
sudo systemctl enable --now tor.service
|
sudo systemctl enable --now tor.service
|
||||||
|
@ -70,6 +70,8 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s
|
|||||||
|
|
||||||
## İstemci kullanımı:
|
## İstemci kullanımı:
|
||||||
|
|
||||||
|
**Web UI için http://localhost:8000 adresine bağlanın**
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install curl build-essential git tor
|
sudo apt install curl build-essential git tor
|
||||||
sudo systemctl enable --now tor.service
|
sudo systemctl enable --now tor.service
|
||||||
|
@ -21,7 +21,4 @@ rpassword = "7.3.1"
|
|||||||
sha3 = "0.10.8"
|
sha3 = "0.10.8"
|
||||||
x25519-dalek = "2.0.1"
|
x25519-dalek = "2.0.1"
|
||||||
ed25519-dalek = "2.1.1"
|
ed25519-dalek = "2.1.1"
|
||||||
eframe = "0.24.1"
|
rocket = { version = "0.5", features = ["json"] }
|
||||||
image = "0.24"
|
|
||||||
rfd = "0.12"
|
|
||||||
winapi = { version = "0.3", features = ["winuser", "windef"] }
|
|
8
client/Rocket.toml
Normal file
8
client/Rocket.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[default]
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = 8000
|
||||||
|
|
||||||
|
[default.limits]
|
||||||
|
forms = "2 MiB"
|
||||||
|
json = "2 MiB"
|
||||||
|
file = "2 MiB"
|
@ -2,32 +2,35 @@ use crate::encrypt_data;
|
|||||||
use crate::receive_and_fetch_messages;
|
use crate::receive_and_fetch_messages;
|
||||||
use crate::send_encrypted_message;
|
use crate::send_encrypted_message;
|
||||||
use crate::pad_message;
|
use crate::pad_message;
|
||||||
use eframe::egui;
|
use rocket::{get, post, routes, serde::json::Json};
|
||||||
use image::GenericImageView;
|
use rocket::fs::NamedFile;
|
||||||
use rfd::FileDialog;
|
use rocket::tokio;
|
||||||
use std::fs;
|
use rocket::fs::FileServer;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use regex::Regex;
|
use std::path::PathBuf;
|
||||||
use base64;
|
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MessagingApp {
|
pub struct MessagingApp {
|
||||||
username: String,
|
username: String,
|
||||||
message_input: String,
|
|
||||||
messages: Arc<Mutex<Vec<String>>>,
|
messages: Arc<Mutex<Vec<String>>>,
|
||||||
shared_hybrid_secret: Arc<std::string::String>,
|
shared_hybrid_secret: Arc<String>,
|
||||||
shared_room_id: Arc<String>,
|
shared_room_id: Arc<String>,
|
||||||
shared_url: Arc<String>,
|
shared_url: Arc<String>,
|
||||||
image_data: Option<String>,
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct MessageInput {
|
||||||
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessagingApp {
|
impl MessagingApp {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
username: String,
|
username: String,
|
||||||
shared_hybrid_secret: Arc<std::string::String>,
|
shared_hybrid_secret: Arc<String>,
|
||||||
shared_room_id: Arc<String>,
|
shared_room_id: Arc<Mutex<String>>,
|
||||||
shared_url: Arc<String>,
|
shared_url: Arc<Mutex<String>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let messages = Arc::new(Mutex::new(vec![]));
|
let messages = Arc::new(Mutex::new(vec![]));
|
||||||
let messages_clone = Arc::clone(&messages);
|
let messages_clone = Arc::clone(&messages);
|
||||||
@ -35,160 +38,125 @@ impl MessagingApp {
|
|||||||
let shared_room_id_clone = Arc::clone(&shared_room_id);
|
let shared_room_id_clone = Arc::clone(&shared_room_id);
|
||||||
let shared_url_clone = Arc::clone(&shared_url);
|
let shared_url_clone = Arc::clone(&shared_url);
|
||||||
|
|
||||||
thread::spawn(move || loop {
|
let room_id = Arc::new(shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone());
|
||||||
match receive_and_fetch_messages(
|
let url = Arc::new(shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone());
|
||||||
&shared_room_id_clone,
|
|
||||||
&shared_hybrid_secret_clone,
|
tokio::spawn(async move {
|
||||||
&shared_url_clone,
|
loop {
|
||||||
true,
|
let room_id_str = shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone();
|
||||||
) {
|
let url_str = shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone();
|
||||||
Ok(new_messages) => {
|
|
||||||
let mut msgs = messages_clone.lock().unwrap();
|
match receive_and_fetch_messages(
|
||||||
msgs.clear();
|
&room_id_str,
|
||||||
msgs.extend(new_messages);
|
&shared_hybrid_secret_clone,
|
||||||
}
|
&url_str,
|
||||||
Err(e) => {
|
true,
|
||||||
eprintln!("Error fetching messages: {}", e);
|
) {
|
||||||
|
Ok(new_messages) => {
|
||||||
|
let mut msgs = messages_clone.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||||
|
msgs.clear();
|
||||||
|
msgs.extend(new_messages);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error fetching messages: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_secs(10));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MessagingApp {
|
MessagingApp {
|
||||||
username,
|
username,
|
||||||
message_input: String::new(),
|
|
||||||
messages,
|
messages,
|
||||||
shared_hybrid_secret,
|
shared_hybrid_secret,
|
||||||
shared_room_id,
|
shared_room_id: room_id,
|
||||||
shared_url,
|
shared_url: url,
|
||||||
image_data: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_image_upload(&mut self) {
|
#[get("/messages")]
|
||||||
if let Some(file_path) = FileDialog::new().add_filter("Image files", &["png", "jpg", "jpeg", "bmp", "gif"]).pick_file() {
|
async fn get_messages(app: &rocket::State<MessagingApp>) -> Json<Vec<String>> {
|
||||||
match fs::read(&file_path) {
|
let result = fetch_and_update_messages(&app).await;
|
||||||
Ok(data) => {
|
|
||||||
let encoded = base64::encode(data);
|
match result {
|
||||||
self.image_data = Some(format!("[IMAGE_DATA]:{}[END DATA]", encoded));
|
Ok(msgs) => Json(msgs),
|
||||||
}
|
Err(e) => {
|
||||||
Err(e) => eprintln!("Error reading image file: {}", e),
|
eprintln!("Error fetching messages: {}", e);
|
||||||
|
|
||||||
|
// Return current messages if fetching fails
|
||||||
|
let msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||||
|
Json(msgs.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_and_update_messages(app: &rocket::State<MessagingApp>) -> Result<Vec<String>, String> {
|
||||||
|
let room_id_str = app.shared_room_id.clone();
|
||||||
|
let url_str = app.shared_url.clone();
|
||||||
|
|
||||||
|
let new_messages = tokio::task::block_in_place(move || {
|
||||||
|
receive_and_fetch_messages(
|
||||||
|
&room_id_str,
|
||||||
|
&app.shared_hybrid_secret,
|
||||||
|
&url_str,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}).map_err(|e| format!("Error fetching messages: {}", e))?;
|
||||||
|
|
||||||
|
// Update the in-memory message storage
|
||||||
|
let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||||
|
msgs.clear();
|
||||||
|
msgs.extend(new_messages.clone());
|
||||||
|
|
||||||
|
Ok(new_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/send", data = "<input>")]
|
||||||
|
async fn post_message(
|
||||||
|
input: Json<MessageInput>,
|
||||||
|
app: &rocket::State<MessagingApp>
|
||||||
|
) -> Result<&'static str, rocket::http::Status> {
|
||||||
|
// Create the formatted message once
|
||||||
|
let formatted_message = format!("<strong>{}</strong>: {}", app.username, input.message);
|
||||||
|
let padded_message = pad_message(&formatted_message, 2048);
|
||||||
|
|
||||||
|
let result = tokio::task::block_in_place(|| {
|
||||||
|
// Encrypt the message
|
||||||
|
let encrypted = encrypt_data(&padded_message, &app.shared_hybrid_secret)
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Encryption error: {}", e);
|
||||||
|
rocket::http::Status::InternalServerError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Send the encrypted message
|
||||||
|
send_encrypted_message(&encrypted, &app.shared_room_id, &app.shared_url)
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error sending message: {}", e);
|
||||||
|
rocket::http::Status::InternalServerError
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
{
|
||||||
|
let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||||
|
msgs.push(formatted_message);
|
||||||
}
|
}
|
||||||
|
Ok("Message sent")
|
||||||
}
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for MessagingApp {
|
#[get("/")]
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
async fn serve_webpage() -> Option<NamedFile> {
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
NamedFile::open(PathBuf::from("static/index.html")).await.ok()
|
||||||
ui.vertical(|ui| {
|
|
||||||
let chat_area_height = ui.available_height() - 80.0;
|
|
||||||
|
|
||||||
egui::Frame::none()
|
|
||||||
.fill(egui::Color32::from_black_alpha(50))
|
|
||||||
.rounding(10.0)
|
|
||||||
.inner_margin(egui::style::Margin::same(10.0))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.set_height(chat_area_height);
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.auto_shrink([false, true])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let messages = self.messages.lock().unwrap();
|
|
||||||
let re = Regex::new(r"</?strong>").unwrap();
|
|
||||||
|
|
||||||
for message in messages.iter() {
|
|
||||||
if message.contains("[IMAGE_DATA]:") {
|
|
||||||
if let Some(encoded) = message.split("[IMAGE_DATA]:").nth(1) {
|
|
||||||
if let Some(end_idx) = encoded.find("[END DATA]") {
|
|
||||||
let image_data = &encoded[..end_idx];
|
|
||||||
match base64::decode(image_data) {
|
|
||||||
Ok(decoded) => {
|
|
||||||
if let Ok(image) = image::load_from_memory(&decoded) {
|
|
||||||
let size = image.dimensions();
|
|
||||||
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
|
||||||
[size.0 as usize, size.1 as usize],
|
|
||||||
&image.to_rgba8(),
|
|
||||||
);
|
|
||||||
let texture = ctx.load_texture(
|
|
||||||
"received_image",
|
|
||||||
color_image,
|
|
||||||
egui::TextureOptions::LINEAR,
|
|
||||||
);
|
|
||||||
ui.image(&texture);
|
|
||||||
} else {
|
|
||||||
eprintln!("Failed to decode image format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Error decoding base64 image: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let cleaned_message = re.replace_all(message, "");
|
|
||||||
ui.label(
|
|
||||||
egui::RichText::new(cleaned_message.as_ref())
|
|
||||||
.size(16.0)
|
|
||||||
.color(egui::Color32::WHITE),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let input_box_width = ui.available_width() * 0.65;
|
|
||||||
let button_width = ui.available_width() * 0.15;
|
|
||||||
|
|
||||||
let text_edit = egui::TextEdit::singleline(&mut self.message_input)
|
|
||||||
.hint_text("Type a message...")
|
|
||||||
.text_color(egui::Color32::WHITE)
|
|
||||||
.frame(true);
|
|
||||||
ui.add_sized([input_box_width, 40.0], text_edit);
|
|
||||||
|
|
||||||
if ui.add_sized([button_width, 40.0], egui::Button::new("Send")).clicked() {
|
|
||||||
let mut message = format!("<strong>{}</strong>: {}", self.username, self.message_input);
|
|
||||||
if let Some(image) = &self.image_data {
|
|
||||||
message.push_str(image);
|
|
||||||
self.image_data = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pad the message to a fixed length (e.g., 2048 bytes)
|
|
||||||
let padded_message = pad_message(&message, 2048);
|
|
||||||
|
|
||||||
if let Err(e) = send_encrypted_message(
|
|
||||||
&encrypt_data(&padded_message, &self.shared_hybrid_secret).unwrap(),
|
|
||||||
&self.shared_room_id,
|
|
||||||
&self.shared_url,
|
|
||||||
) {
|
|
||||||
eprintln!("Error sending message: {}", e);
|
|
||||||
} else {
|
|
||||||
self.message_input.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.add_sized([button_width, 40.0], egui::Button::new("Upload Image")).clicked() {
|
|
||||||
self.handle_image_upload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_gui(
|
pub fn create_rocket(app: MessagingApp) -> rocket::Rocket<rocket::Build> {
|
||||||
username: String,
|
rocket::build()
|
||||||
shared_hybrid_secret: Arc<std::string::String>,
|
.manage(app)
|
||||||
shared_room_id: Arc<String>,
|
.mount("/", routes![get_messages, post_message, serve_webpage])
|
||||||
shared_url: Arc<String>,
|
.mount("/static", FileServer::from("static"))
|
||||||
) -> Result<(), eframe::Error> {
|
}
|
||||||
let app = MessagingApp::new(
|
|
||||||
username,
|
|
||||||
shared_hybrid_secret,
|
|
||||||
shared_room_id,
|
|
||||||
shared_url,
|
|
||||||
);
|
|
||||||
let native_options = eframe::NativeOptions {
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
eframe::run_native("Amnezichat", native_options, Box::new(|_| Box::new(app)))
|
|
||||||
}
|
|
@ -4,7 +4,8 @@ mod network_operations;
|
|||||||
mod key_exchange;
|
mod key_exchange;
|
||||||
mod authentication;
|
mod authentication;
|
||||||
mod encryption;
|
mod encryption;
|
||||||
use gui::run_gui;
|
use gui::create_rocket;
|
||||||
|
use gui::MessagingApp;
|
||||||
use key_operations::key_operations_dilithium;
|
use key_operations::key_operations_dilithium;
|
||||||
use key_operations::key_operations_eddsa;
|
use key_operations::key_operations_eddsa;
|
||||||
use network_operations::create_client_with_proxy;
|
use network_operations::create_client_with_proxy;
|
||||||
@ -469,21 +470,29 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
// Handle GUI or CLI messaging
|
// Handle GUI or CLI messaging
|
||||||
if interface_choice.to_lowercase() == "gui" {
|
if interface_choice.to_lowercase() == "gui" {
|
||||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
let rt = rocket::tokio::runtime::Runtime::new().unwrap();
|
||||||
let shared_room_id_for_gui: Arc<String> = {
|
rt.block_on(async {
|
||||||
let locked = shared_room_id.lock().unwrap();
|
// Wrap only for passing to run_gui
|
||||||
Arc::new(locked.clone())
|
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||||
};
|
|
||||||
let shared_url_for_gui: Arc<String> = {
|
// Correctly clone Arc<Mutex<String>> instead of Arc<String>
|
||||||
let locked = shared_url.lock().unwrap();
|
let shared_room_id_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_room_id);
|
||||||
Arc::new(locked.clone())
|
let shared_url_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_url);
|
||||||
};
|
|
||||||
let _ = run_gui(
|
// Pass the arguments
|
||||||
username.clone(),
|
let app = MessagingApp::new(
|
||||||
shared_hybrid_secret_for_gui,
|
username,
|
||||||
shared_room_id_for_gui,
|
shared_hybrid_secret_for_gui,
|
||||||
shared_url_for_gui,
|
shared_room_id_for_gui,
|
||||||
);
|
shared_url_for_gui,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Await the async launch function
|
||||||
|
if let Err(e) = create_rocket(app).launch().await {
|
||||||
|
eprintln!("Rocket server failed: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
loop {
|
loop {
|
||||||
let mut message = String::new();
|
let mut message = String::new();
|
||||||
@ -741,26 +750,28 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
|
|
||||||
if interface_choice.to_lowercase() == "gui" {
|
if interface_choice.to_lowercase() == "gui" {
|
||||||
// Wrap only for passing to run_gui
|
let rt = rocket::tokio::runtime::Runtime::new().unwrap();
|
||||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
rt.block_on(async {
|
||||||
|
// Wrap only for passing to run_gui
|
||||||
|
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||||
|
|
||||||
let shared_room_id_for_gui: Arc<String> = {
|
// Correctly clone Arc<Mutex<String>> instead of Arc<String>
|
||||||
let locked = shared_room_id.lock().unwrap();
|
let shared_room_id_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_room_id);
|
||||||
Arc::new(locked.clone())
|
let shared_url_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_url);
|
||||||
};
|
|
||||||
|
|
||||||
let shared_url_for_gui: Arc<String> = {
|
// Pass the arguments
|
||||||
let locked = shared_url.lock().unwrap();
|
let app = MessagingApp::new(
|
||||||
Arc::new(locked.clone())
|
username,
|
||||||
};
|
shared_hybrid_secret_for_gui,
|
||||||
|
shared_room_id_for_gui,
|
||||||
|
shared_url_for_gui,
|
||||||
|
);
|
||||||
|
|
||||||
// Pass the arguments
|
// Await the async launch function
|
||||||
let _ = run_gui(
|
if let Err(e) = create_rocket(app).launch().await {
|
||||||
username.clone(),
|
eprintln!("Rocket server failed: {}", e);
|
||||||
shared_hybrid_secret_for_gui,
|
}
|
||||||
shared_room_id_for_gui,
|
});
|
||||||
shared_url_for_gui,
|
|
||||||
);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
loop {
|
loop {
|
||||||
|
@ -487,8 +487,13 @@ pub fn receive_and_fetch_messages(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If gui is false, skip messages containing `[IMAGE_DATA]:`
|
// If gui is false, skip messages containing `<media>`
|
||||||
if !gui && unpadded_message.contains("[IMAGE_DATA]:") {
|
if !gui && unpadded_message.contains("<media>") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If gui is false, skip messages containing `<pfp>`
|
||||||
|
if !gui && unpadded_message.contains("<pfp>") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
147
client/static/index.html
Normal file
147
client/static/index.html
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Amnezichat</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
<script src="/static/purify.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let profilePicBase64 = "";
|
||||||
|
|
||||||
|
// Handle profile picture upload
|
||||||
|
function handleProfilePicChange(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = function () {
|
||||||
|
profilePicBase64 = reader.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle media upload (image/video)
|
||||||
|
function handleMediaChange(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = async function () {
|
||||||
|
const mediaBase64 = reader.result;
|
||||||
|
|
||||||
|
let mediaMessage = { message: `<media>${mediaBase64}</media>` };
|
||||||
|
|
||||||
|
// Include profile picture in media message if available
|
||||||
|
if (profilePicBase64) {
|
||||||
|
mediaMessage.message = `<pfp>${profilePicBase64}</pfp>` + mediaMessage.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send media as a separate message
|
||||||
|
await fetch('/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(mediaMessage)
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchMessages();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch messages from the server
|
||||||
|
async function fetchMessages() {
|
||||||
|
const response = await fetch('/messages');
|
||||||
|
const messages = await response.json();
|
||||||
|
|
||||||
|
const messageHTML = messages.map(msg => {
|
||||||
|
const base64ImagePattern = /^data:image\/(png|jpeg|jpg|gif);base64,/;
|
||||||
|
const base64VideoPattern = /^data:video\/(mp4|webm|ogg);base64,/;
|
||||||
|
const pfpMatch = msg.match(/<pfp>(.*?)<\/pfp>/);
|
||||||
|
const mediaMatch = msg.match(/<media>(.*?)<\/media>/);
|
||||||
|
let messageText = msg.replace(/<pfp>.*?<\/pfp>/, '').replace(/<media>.*?<\/media>/, '');
|
||||||
|
let profilePic = "";
|
||||||
|
let mediaContent = "";
|
||||||
|
|
||||||
|
// Display profile picture if available
|
||||||
|
if (pfpMatch && base64ImagePattern.test(pfpMatch[1])) {
|
||||||
|
profilePic = `<img src="${pfpMatch[1]}" alt="Profile Picture" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display media as a separate full-size element
|
||||||
|
if (mediaMatch && (base64ImagePattern.test(mediaMatch[1]) || base64VideoPattern.test(mediaMatch[1]))) {
|
||||||
|
if (base64ImagePattern.test(mediaMatch[1])) {
|
||||||
|
mediaContent = `<div>${profilePic}<img src="${mediaMatch[1]}" alt="Media" style="width: 100%; max-width: 600px; margin-top: 10px;"></div>`;
|
||||||
|
} else if (base64VideoPattern.test(mediaMatch[1])) {
|
||||||
|
mediaContent = `<div>${profilePic}<video controls style="width: 100%; max-width: 600px; margin-top: 10px;"><source src="${mediaMatch[1]}" type="video/mp4">Your browser does not support the video tag.</video></div>`;
|
||||||
|
}
|
||||||
|
return mediaContent; // Media is a separate message
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div style="display: flex; align-items: center; margin-bottom: 10px;">${profilePic}<p>${DOMPurify.sanitize(messageText)}</p></div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
document.getElementById('messages').innerHTML = messageHTML;
|
||||||
|
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send text message to the server
|
||||||
|
async function sendMessage() {
|
||||||
|
const message = document.getElementById('messageInput').value;
|
||||||
|
if (message.trim() !== "") {
|
||||||
|
let messageData = { message: DOMPurify.sanitize(message) };
|
||||||
|
|
||||||
|
// Include profile picture in text message if available
|
||||||
|
if (profilePicBase64) {
|
||||||
|
messageData.message = `<pfp>${profilePicBase64}</pfp>` + messageData.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch('/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(messageData)
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('messageInput').value = '';
|
||||||
|
fetchMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
const modal = document.getElementById('settings');
|
||||||
|
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettings() {
|
||||||
|
document.getElementById('settings').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="fetchMessages()">
|
||||||
|
<div class="container">
|
||||||
|
<div id="messages"></div>
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off">
|
||||||
|
<button id="settingsButton" onclick="toggleSettings()">Settings</button>
|
||||||
|
<button onclick="sendMessage()">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings">
|
||||||
|
<div class="settings-content">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<!-- Hidden file input for selecting profile picture -->
|
||||||
|
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)">
|
||||||
|
<!-- Button that opens the file input for profile picture -->
|
||||||
|
<button onclick="document.getElementById('profilePicInput').click()">Choose Profile Picture</button>
|
||||||
|
|
||||||
|
<!-- Hidden file input for selecting media (image or video) -->
|
||||||
|
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)">
|
||||||
|
<!-- Button that opens the file input for media -->
|
||||||
|
<button onclick="document.getElementById('mediaInput').click()">Send Media</button>
|
||||||
|
|
||||||
|
<button onclick="closeSettings()">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
client/static/purify.min.js
vendored
Normal file
3
client/static/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
140
client/static/styles.css
Normal file
140
client/static/styles.css
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/* Reset the body to cover the full page */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Helvetica Neue', Arial, sans-serif; /* Clean sans-serif font */
|
||||||
|
background-color: #1e1f22; /* Dark background for the whole page */
|
||||||
|
color: #e1e1e1; /* Light text for contrast */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center the content and give it full height */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box; /* Ensure padding is included in height calculations */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for the messages area */
|
||||||
|
#messages {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #333b41; /* Darker border */
|
||||||
|
background-color: #2c2f36; /* Dark background for message container */
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px; /* Space between messages */
|
||||||
|
margin-bottom: 20px; /* Space between messages and input */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling each message */
|
||||||
|
#messages p {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #444c56; /* Slightly lighter gray message bubbles */
|
||||||
|
border-radius: 20px;
|
||||||
|
max-width: 75%; /* Keep messages from stretching too wide */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling input and button area */
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end; /* Align input and buttons to the bottom */
|
||||||
|
width: 100%; /* Ensure it takes full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
flex-grow: 1; /* Make the input take up remaining space */
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #444c56; /* Dark border */
|
||||||
|
background-color: #2c2f36; /* Dark background */
|
||||||
|
color: #e1e1e1; /* Light text for input */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4c8bf5; /* Light blue when focused */
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px; /* Rounded buttons */
|
||||||
|
background-color: #4c8bf5; /* Signal's blue color */
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #3978d1; /* Slightly darker blue on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #2962a1; /* Even darker blue when pressed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for the settings button inside the input container */
|
||||||
|
#settingsButton {
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px; /* Rounded button */
|
||||||
|
background-color: #4c8bf5; /* Signal's blue */
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsButton:hover {
|
||||||
|
background-color: #3978d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settingsButton:active {
|
||||||
|
background-color: #2962a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal for settings (hidden by default) */
|
||||||
|
#settings {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7); /* Dark background for modal */
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
background-color: #2c2f36; /* Dark background for modal */
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #e1e1e1; /* Light text for headings */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #4c8bf5; /* Blue background for buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for profile picture preview */
|
||||||
|
#profilePicPreview {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%; /* Circular image */
|
||||||
|
margin-top: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid #444c56; /* Dark border around the profile picture */
|
||||||
|
}
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 308 KiB |
@ -1,3 +1,8 @@
|
|||||||
[default]
|
[default]
|
||||||
address = "0.0.0.0"
|
address = "0.0.0.0"
|
||||||
port = 8080
|
port = 8080
|
||||||
|
|
||||||
|
[default.limits]
|
||||||
|
forms = "2 MiB"
|
||||||
|
json = "2 MiB"
|
||||||
|
file = "2 MiB"
|
||||||
|
@ -51,8 +51,8 @@
|
|||||||
<a href="#" class="close-button">×</a>
|
<a href="#" class="close-button">×</a>
|
||||||
<h2>Installation Instructions</h2>
|
<h2>Installation Instructions</h2>
|
||||||
|
|
||||||
<!-- Option 1: Build it yourself -->
|
<!-- Build it yourself -->
|
||||||
<h3>Option 1: Build it Yourself</h3>
|
<h3>Build it Yourself</h3>
|
||||||
<pre>
|
<pre>
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install curl build-essential git tor
|
sudo apt install curl build-essential git tor
|
||||||
@ -63,10 +63,6 @@ cd Amnezichat/client/
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
cargo run --release
|
cargo run --release
|
||||||
</pre>
|
</pre>
|
||||||
<!-- Option 2: Get it from GitHub Releases -->
|
|
||||||
<h3>Option 2: Get it from GitHub Releases</h3>
|
|
||||||
<p>If you prefer not to build it yourself, you can download precompiled binaries directly from the
|
|
||||||
<a href="https://github.com/umutcamliyurt/Amnezichat/releases/" target="_blank" style="color: white;">GitHub Releases page</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -5,15 +5,15 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: linear-gradient(135deg, #2c3e50, #4ca1af);
|
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||||||
color: #ecf0f1;
|
color: #ecf0f1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
background: rgba(44, 62, 80, 0.95);
|
background: rgba(15, 30, 60, 0.95);
|
||||||
border: 1px solid #34495e;
|
border: 1px solid #0f3b5f;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.6);
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@ -37,7 +37,7 @@ a.download-button {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: linear-gradient(45deg, #1abc9c, #16a085);
|
background: linear-gradient(45deg, #2980b9, #1e90ff);
|
||||||
padding: 12px 25px;
|
padding: 12px 25px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -46,7 +46,7 @@ a.download-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.download-button:hover {
|
a.download-button:hover {
|
||||||
background: linear-gradient(45deg, #16a085, #1abc9c);
|
background: linear-gradient(45deg, #1e90ff, #2980b9);
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,14 +56,14 @@ a.download-button:hover {
|
|||||||
|
|
||||||
.main-links a {
|
.main-links a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #1abc9c;
|
color: #2980b9;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-links a:hover {
|
.main-links a:hover {
|
||||||
color: #16a085;
|
color: #1e90ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
details {
|
||||||
@ -102,9 +102,9 @@ details p, details ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: #2c3e50;
|
background: #0f1e3c;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #34495e;
|
border: 1px solid #0a2b4f;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@ -112,7 +112,7 @@ details p, details ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content pre {
|
.modal-content pre {
|
||||||
background: #34495e;
|
background: #0a2b4f;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #ecf0f1;
|
color: #ecf0f1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user