2 Commits

Author SHA1 Message Date
c562152019 Merge branch 'master' of https://gitea.communiquons.org/pierre/MatrixGW
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-02 22:49:56 +01:00
2ea20e6de4 feat: can upload files in conversations 2026-03-02 22:49:47 +01:00
11 changed files with 247 additions and 19 deletions

View File

@@ -95,6 +95,44 @@ dependencies = [
"syn 2.0.116",
]
[[package]]
name = "actix-multipart"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"derive_more 0.99.20",
"futures-core",
"futures-util",
"httparse",
"local-waker",
"log",
"memchr",
"mime",
"rand 0.8.5",
"serde",
"serde_json",
"serde_plain",
"tempfile",
"tokio",
]
[[package]]
name = "actix-multipart-derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b"
dependencies = [
"darling",
"parse-size",
"proc-macro2",
"quote",
"syn 2.0.116",
]
[[package]]
name = "actix-remote-ip"
version = "0.1.0"
@@ -243,16 +281,18 @@ dependencies = [
[[package]]
name = "actix-ws"
version = "0.3.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12d4f2fbee3ef7a22fa6cb0e416b962237a167ed0419f22d4e451da2d7f082f8"
checksum = "decf53c3cdd63dd6f289980b430238f9a2f6d19f8bce8e418272e08d3da43f0f"
dependencies = [
"actix-codec",
"actix-http",
"actix-web",
"bytestring",
"futures-core",
"futures-sink",
"tokio",
"tokio-util",
]
[[package]]
@@ -402,9 +442,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.101"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "anymap2"
@@ -872,9 +912,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.59"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
@@ -882,9 +922,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.59"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
@@ -994,6 +1034,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.10.0"
@@ -1301,6 +1347,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.116",
]
[[package]]
name = "derive_more"
version = "1.0.0"
@@ -1336,7 +1395,7 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
@@ -3213,6 +3272,7 @@ name = "matrixgw_backend"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-multipart",
"actix-remote-ip",
"actix-session",
"actix-web",
@@ -3535,6 +3595,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -4577,6 +4643,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.4"
@@ -5071,6 +5146,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-sink",
"futures-util",
"pin-project-lite",
"tokio",
]

View File

@@ -6,14 +6,15 @@ edition = "2024"
[dependencies]
env_logger = "0.11.9"
log = "0.4.29"
clap = { version = "4.5.59", features = ["derive", "env"] }
anyhow = "1.0.101"
clap = { version = "4.5.60", features = ["derive", "env"] }
anyhow = "1.0.102"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.49.0", features = ["full"] }
actix-web = "4.13.0"
actix-session = { version = "0.11.0", features = ["redis-session"] }
actix-remote-ip = "0.1.0"
actix-cors = "0.7.1"
actix-multipart = "0.7.2"
light-openid = "1.1.0"
bytes = "1.11.1"
sha2 = "0.11.0-rc.5"
@@ -32,7 +33,7 @@ url = "2.5.8"
ractor = "0.15.10"
serde_json = "1.0.149"
lazy-regex = "3.6.0"
actix-ws = "0.3.1"
actix-ws = "0.4.0"
infer = "0.19.0"
rust-embed = "8.11.0"
mime_guess = "2.0.5"

View File

@@ -2,11 +2,14 @@ use crate::controllers::HttpResult;
use crate::controllers::matrix::matrix_media_controller;
use crate::controllers::matrix::matrix_media_controller::MediaQuery;
use crate::controllers::matrix::matrix_room_controller::RoomIdInPath;
use crate::controllers::server_controller::ServerConstraints;
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
use actix_multipart::form::MultipartForm;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest, HttpResponse, web};
use futures_util::{StreamExt, stream};
use matrix_sdk::Room;
use matrix_sdk::attachment::AttachmentConfig;
use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind};
use matrix_sdk::media::MediaEventContent;
use matrix_sdk::room::MessagesOptions;
@@ -23,6 +26,7 @@ use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent};
use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::io::Read;
#[derive(Serialize)]
pub struct APIEvent {
@@ -136,6 +140,55 @@ pub async fn send_text_message(
Ok(HttpResponse::Accepted().finish())
}
#[derive(Debug, actix_multipart::form::MultipartForm)]
pub struct SendFileForm {
#[multipart]
file: actix_multipart::form::tempfile::TempFile,
}
pub async fn send_file(
client: MatrixClientExtractor,
path: web::Path<RoomIdInPath>,
req: HttpRequest,
) -> HttpResult {
let Some(payload) = client.auth.payload else {
return Ok(HttpResponse::BadRequest().body("No payload included in request!"));
};
// Reconstruct multipart form from authenticated request
let mut form = MultipartForm::<SendFileForm>::from_request(
&req,
&mut Payload::from(bytes::Bytes::from(payload)),
)
.await?;
// Read attachment to end
let mut buff = Vec::with_capacity(form.file.size);
form.file.file.read_to_end(&mut buff)?;
if form.file.size > ServerConstraints::default().max_upload_file_size {
return Ok(HttpResponse::NotFound().json("Uploaded file is too large!"));
}
let Some(room) = client.client.client.get_room(&path.room_id) else {
return Ok(HttpResponse::NotFound().json("Room not found!"));
};
let Some(file_name) = form.file.file_name.as_deref() else {
return Ok(HttpResponse::BadRequest().json("File name must be specified!"));
};
let Some(mime_type) = form.file.content_type.as_ref() else {
return Ok(HttpResponse::BadRequest().json("File content type must be specified!"));
};
// Do send the file
room.send_attachment(file_name, mime_type, buff, AttachmentConfig::new())
.await?;
Ok(HttpResponse::Accepted().finish())
}
#[derive(serde::Deserialize)]
pub struct EventIdInPath {
pub(crate) event_id: OwnedEventId,

View File

@@ -39,6 +39,7 @@ pub struct ServerConstraints {
pub token_name: LenConstraints,
pub token_ip_net: LenConstraints,
pub token_max_inactivity: LenConstraints,
pub max_upload_file_size: usize,
}
impl Default for ServerConstraints {
@@ -47,6 +48,7 @@ impl Default for ServerConstraints {
token_name: LenConstraints::new(5, 255),
token_ip_net: LenConstraints::max_only(44),
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
max_upload_file_size: 20_000_000,
}
}
}

View File

@@ -13,6 +13,7 @@ use matrixgw_backend::controllers::matrix::{
matrix_event_controller, matrix_media_controller, matrix_profile_controller,
matrix_room_controller, matrix_space_controller,
};
use matrixgw_backend::controllers::server_controller::ServerConstraints;
use matrixgw_backend::controllers::{
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
static_controller, tokens_controller, ws_controller,
@@ -75,6 +76,9 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default())
.wrap(session_mw)
.wrap(cors)
.app_data(web::PayloadConfig::new(
ServerConstraints::default().max_upload_file_size,
))
.app_data(web::Data::new(manager_actor_clone.clone()))
.app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
@@ -182,6 +186,10 @@ async fn main() -> std::io::Result<()> {
"/api/matrix/room/{room_id}/send_text_message",
web::post().to(matrix_event_controller::send_text_message),
)
.route(
"/api/matrix/room/{room_id}/send_file",
web::post().to(matrix_event_controller::send_file),
)
.route(
"/api/matrix/room/{room_id}/event/{event_id}/set_text_content",
web::post().to(matrix_event_controller::set_text_content),

View File

@@ -18,6 +18,7 @@
"date-and-time": "^4.2.0",
"dayjs": "^1.11.19",
"emoji-picker-react": "^4.17.4",
"filesize": "^11.0.13",
"is-cidr": "^6.0.3",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
@@ -2486,6 +2487,15 @@
"node": ">=16.0.0"
}
},
"node_modules/filesize": {
"version": "11.0.13",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.13.tgz",
"integrity": "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">= 10.8.0"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",

View File

@@ -20,6 +20,7 @@
"date-and-time": "^4.2.0",
"dayjs": "^1.11.19",
"emoji-picker-react": "^4.17.4",
"filesize": "^11.0.13",
"is-cidr": "^6.0.3",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",

View File

@@ -16,6 +16,7 @@ export interface ServerConstraints {
token_name: LenConstraint;
token_ip_net: LenConstraint;
token_max_inactivity: LenConstraint;
max_upload_file_size: number;
}
export interface LenConstraint {

View File

@@ -68,7 +68,7 @@ export class MatrixApiEvent {
*/
static async GetRoomEvents(
room: Room,
from?: string
from?: string,
): Promise<MatrixEventsList> {
return (
await APIClient.exec({
@@ -86,7 +86,7 @@ export class MatrixApiEvent {
static GetEventFileURL(
room: Room,
event_id: string,
thumbnail: boolean
thumbnail: boolean,
): string {
return `${APIClient.ActualBackendURL()}/matrix/room/${
room.id
@@ -104,13 +104,26 @@ export class MatrixApiEvent {
});
}
/**
* Send file message
*/
static async SendFileMessage(room: Room, file: Blob): Promise<void> {
const formData = new FormData();
formData.set("file", file);
await APIClient.exec({
method: "POST",
uri: `/matrix/room/${room.id}/send_file`,
formData,
});
}
/**
* Edit text message content
*/
static async SetTextMessageContent(
room: Room,
event_id: string,
content: string
content: string,
): Promise<void> {
await APIClient.exec({
method: "POST",
@@ -125,7 +138,7 @@ export class MatrixApiEvent {
static async ReactToEvent(
room: Room,
event_id: string,
key: string
key: string,
): Promise<void> {
await APIClient.exec({
method: "POST",

View File

@@ -0,0 +1,33 @@
import { filesize } from "filesize";
/**
* Select a file to upload
*/
export async function selectFileToUpload(p: {
allowedTypes?: string[];
maxSize?: number;
}): Promise<Blob | null> {
// Create file element
const fileEl = document.createElement("input");
fileEl.type = "file";
if (p.allowedTypes && p.allowedTypes.length > 0)
fileEl.accept = p.allowedTypes.join(",");
fileEl.click();
// Wait for a file to be chosen
await new Promise((res, _rej) =>
fileEl.addEventListener("change", () => res(null)),
);
if ((fileEl.files?.length ?? 0) === 0) return null;
const file = fileEl.files![0];
// Check file size
if (p.maxSize && file.size > p.maxSize) {
throw new Error(
`The file is too big ! (max accepted file size : ${filesize(p.maxSize)})`,
);
}
return file;
}

View File

@@ -1,18 +1,23 @@
import AttachFileIcon from "@mui/icons-material/AttachFile";
import SendIcon from "@mui/icons-material/Send";
import { IconButton, TextField } from "@mui/material";
import React, { type FormEvent } from "react";
import { IconButton, TextField, Tooltip } from "@mui/material";
import React, { type SyntheticEvent } from "react";
import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent";
import type { Room } from "../../api/matrix/MatrixApiRoom";
import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider";
import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider";
import { selectFileToUpload } from "../../utils/FilesUtils";
import { ServerApi } from "../../api/ServerApi";
import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider";
export function SendMessageForm(p: { room: Room }): React.ReactElement {
const loadingMessage = useLoadingMessage();
const alert = useAlert();
const snackbar = useSnackbar();
const [text, setText] = React.useState("");
const handleTextSubmit = async (e: FormEvent) => {
const handleTextSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
if (text === "") return;
@@ -31,6 +36,26 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
}
};
const handleFileSubmit = async () => {
try {
const file = await selectFileToUpload({
maxSize: ServerApi.Config.constraints.max_upload_file_size,
});
if (!file) return;
loadingMessage.show("Uploading file...");
await MatrixApiEvent.SendFileMessage(p.room, file);
snackbar("The file was successfully uploaded!");
} catch (e) {
console.error(e);
alert(`Failed to upload file! ${e}`);
} finally {
loadingMessage.hide();
}
};
return (
<form onSubmit={handleTextSubmit}>
<div
@@ -50,6 +75,11 @@ export function SendMessageForm(p: { room: Room }): React.ReactElement {
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Tooltip title="Send a file">
<IconButton size="small" onClick={handleFileSubmit}>
<AttachFileIcon />
</IconButton>
</Tooltip>
<IconButton
size="small"
style={{ visibility: text === "" ? "hidden" : "visible" }}