diff --git a/src/network/backends/session_open_group_server.cpp b/src/network/backends/session_open_group_server.cpp index a4c65ff0..0ad07efd 100644 --- a/src/network/backends/session_open_group_server.cpp +++ b/src/network/backends/session_open_group_server.cpp @@ -19,24 +19,41 @@ namespace session::network::open_group_server { constexpr std::string_view ENDPOINT_ROOM = "room/{}"; constexpr std::string_view ENDPOINT_FILE = "room/{}/file/{}"; +// Some clients (e.g. older Android versions) generate community file download urls without the +// room segment (`{base_url}/file/{file_id}`). We accept these as a fallback so their attachments +// remain downloadable. +constexpr std::string_view LEGACY_ENDPOINT_FILE = "file/{}"; + std::optional parse_download_url(std::string_view url) { // Expected format: {base_url}/room/{room}/file/{file_id}(?:#d) // Examples: - // https://example.com/room/file/123 - // https://example.com/room/file/123#d + // https://example.com/room/test/file/123 + // https://example.com/room/test/file/123#d + // Legacy fallback (no room, e.g. from older Android clients): {base_url}/file/{file_id} + // https://example.com/file/123 DownloadInfo info{}; + int64_t file_id; + auto match = backends::match_endpoint(ENDPOINT_FILE, url); - if (!match || match->base.empty() || match->captures.size() != 2) - return std::nullopt; + if (match && !match->base.empty() && match->captures.size() == 2) { + info.room = std::string{match->captures[0]}; - info.room = std::string{match->captures[0]}; + if (!quic::parse_int(match->captures[1], file_id)) + return std::nullopt; + } else { + // Fall back to the legacy room-less endpoint. The room can't be recovered from such a url + // so it's left empty for the caller to populate from context (the conversation the + // attachment belongs to). + match = backends::match_endpoint(LEGACY_ENDPOINT_FILE, url); - int64_t file_id; + if (!match || match->base.empty() || match->captures.size() != 1) + return std::nullopt; - if (!quic::parse_int(match->captures[1], file_id)) - return std::nullopt; + if (!quic::parse_int(match->captures[0], file_id)) + return std::nullopt; + } info.file_id = file_id; info.base_url = match->base; diff --git a/tests/test_backed_session_open_group_server.cpp b/tests/test_backed_session_open_group_server.cpp index bff9f8ff..2dbc2b4e 100644 --- a/tests/test_backed_session_open_group_server.cpp +++ b/tests/test_backed_session_open_group_server.cpp @@ -44,6 +44,24 @@ TEST_CASE("Download url parsing", "[backend][session_open_group_server]") { CHECK(parsed_download_url->file_id == 123); CHECK_FALSE(parsed_download_url->wants_stream_decryption); + // Falls back to the legacy room-less format (used by some older clients, e.g. Android), + // leaving the room empty for the caller to populate from context + parsed_download_url = open_group_server::parse_download_url("https://example.com/file/123"sv); + REQUIRE(parsed_download_url.has_value()); + CHECK(parsed_download_url->base_url == "https://example.com"sv); + CHECK(parsed_download_url->room.empty()); + CHECK(parsed_download_url->file_id == 123); + CHECK_FALSE(parsed_download_url->wants_stream_decryption); + + // Handles the legacy room-less format with a trailing slash and stream-decryption fragment + parsed_download_url = + open_group_server::parse_download_url("https://example.com/file/123/#d"sv); + REQUIRE(parsed_download_url.has_value()); + CHECK(parsed_download_url->base_url == "https://example.com"sv); + CHECK(parsed_download_url->room.empty()); + CHECK(parsed_download_url->file_id == 123); + CHECK(parsed_download_url->wants_stream_decryption); + // Doesn't have an issue with a url that isn't in the right format parsed_download_url = open_group_server::parse_download_url("https://example.com/test/test2/test3/5432"sv);