[Server] Add deck share links and public deck visibility (#7241)

* [Server] Add deck share links and public deck visibility

* Address server review comments for deck share links

* Document transaction teardown in deck share rollback paths

* Address second round of deck share review comments

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-20 20:22:16 +02:00 committed by GitHub
parent a5e94d8a4f
commit 073ec29c4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1202 additions and 23 deletions

View file

@ -15,9 +15,17 @@ set(PROTO_FILES
command_deck_del.proto
command_deck_del_dir.proto
command_deck_download.proto
command_deck_download_public.proto
command_deck_list.proto
command_deck_list_other_user.proto
command_deck_new_dir.proto
command_deck_select.proto
command_deck_set_visibility.proto
command_deck_share_create.proto
command_deck_share_download.proto
command_deck_share_list.proto
command_deck_share_list_mine.proto
command_deck_share_remove.proto
command_deck_upload.proto
command_del_counter.proto
command_delete_arrow.proto
@ -137,6 +145,10 @@ set(PROTO_FILES
response_card_art_rule_entry.proto
response_deck_download.proto
response_deck_list.proto
response_deck_share_create.proto
response_deck_share_download.proto
response_deck_share_list.proto
response_deck_share_list_mine.proto
response_deck_upload.proto
response_dump_zone.proto
response_forgotpasswordrequest.proto
@ -175,6 +187,8 @@ set(PROTO_FILES
serverinfo_cardcounter.proto
serverinfo_chat_message.proto
serverinfo_counter.proto
serverinfo_deck_share_item.proto
serverinfo_deck_share_summary.proto
serverinfo_deckstorage.proto
serverinfo_game.proto
serverinfo_gametype.proto

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "session_commands.proto";
message Command_DeckDownloadPublic {
extend SessionCommand {
optional Command_DeckDownloadPublic ext = 1031;
}
optional uint32 deck_id = 1;
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "session_commands.proto";
message Command_DeckListOtherUser {
extend SessionCommand {
optional Command_DeckListOtherUser ext = 1029;
}
optional string user_name = 1;
}

View file

@ -0,0 +1,13 @@
syntax = "proto2";
import "session_commands.proto";
message Command_DeckSetVisibility {
extend SessionCommand {
optional Command_DeckSetVisibility ext = 1030;
}
// Set the public visibility of a single deck (mutually exclusive with folder_path).
optional uint32 deck_id = 1;
// Set the public visibility of a folder (all decks under it inherit).
optional string folder_path = 2;
optional bool is_public = 3;
}

View file

@ -0,0 +1,24 @@
syntax = "proto2";
import "session_commands.proto";
message DeckShareItem {
// Reference an existing deck in the sharer's personal deck storage.
// Mutually exclusive with deck_list.
optional uint32 deck_id = 1;
// Inline deck content in the native format.
// Mutually exclusive with deck_id.
optional string deck_list = 2;
// Color identity of the deck (e.g. "WUBRG"), computed by the sharing client.
optional string color_identity = 3;
}
message Command_DeckShareCreate {
extend SessionCommand {
optional Command_DeckShareCreate ext = 1026;
}
optional string name = 1;
repeated DeckShareItem items = 2;
// Path of a folder in the sharer's personal deck storage. When set, all
// decks in that folder are shared (resolved by the server).
optional string folder_path = 3;
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "session_commands.proto";
message Command_DeckShareDownload {
extend SessionCommand {
optional Command_DeckShareDownload ext = 1028;
}
optional string token = 1;
optional uint32 item_id = 2;
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "session_commands.proto";
message Command_DeckShareList {
extend SessionCommand {
optional Command_DeckShareList ext = 1027;
}
optional string token = 1;
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "session_commands.proto";
// Requests the list of share bundles created by the calling user, so they can
// be reviewed and revoked before they expire.
message Command_DeckShareListMine {
extend SessionCommand {
optional Command_DeckShareListMine ext = 1032;
}
}

View file

@ -0,0 +1,11 @@
syntax = "proto2";
import "session_commands.proto";
// Revokes one of the calling user's own share bundles. The referenced items
// are removed by cascade.
message Command_DeckShareRemove {
extend SessionCommand {
optional Command_DeckShareRemove ext = 1033;
}
optional uint32 share_id = 1;
}

View file

@ -8,4 +8,10 @@ message Command_DeckUpload {
optional string path = 1; // to upload a new deck
optional uint32 deck_id = 2; // to replace an existing deck
optional string deck_list = 3;
optional bool is_public = 4; // mark the deck public on upload (publish)
// The server derives the banner card and tags from deck_list, so clients only
// need to send the color identity, which cannot be computed server-side.
reserved 5, 6, 8;
reserved "banner_card_name", "banner_card_provider", "tags";
optional string color_identity = 7;
}

View file

@ -81,6 +81,10 @@ message Response {
REPLAY_LIST = 1100; // Response listing replays
REPLAY_DOWNLOAD = 1101; // Response for replay download
REPLAY_GET_CODE = 1102; // Response containing replay code
DECK_SHARE_CREATE = 1103; // Response to deck share creation
DECK_SHARE_LIST = 1104; // Response listing shared decks
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
}

View file

@ -0,0 +1,11 @@
syntax = "proto2";
import "response.proto";
message Response_DeckShareCreate {
extend Response {
optional Response_DeckShareCreate ext = 1103;
}
optional string token = 1;
optional uint64 expires_at = 2;
optional uint32 item_count = 3;
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "response.proto";
message Response_DeckShareDownload {
extend Response {
optional Response_DeckShareDownload ext = 1105;
}
optional string deck = 1;
}

View file

@ -0,0 +1,12 @@
syntax = "proto2";
import "response.proto";
import "serverinfo_deck_share_item.proto";
message Response_DeckShareList {
extend Response {
optional Response_DeckShareList ext = 1104;
}
optional string name = 1;
optional uint64 expires_at = 2;
repeated ServerInfo_DeckShareItem items = 3;
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
import "response.proto";
import "serverinfo_deck_share_summary.proto";
message Response_DeckShareListMine {
extend Response {
optional Response_DeckShareListMine ext = 1106;
}
repeated ServerInfo_DeckShareSummary shares = 1;
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
message ServerInfo_DeckShareItem {
optional uint32 id = 1;
optional string name = 2;
repeated string tags = 3;
optional string banner_card = 4;
optional string game_format = 5;
optional string color_identity = 6;
}

View file

@ -0,0 +1,10 @@
syntax = "proto2";
// A share bundle created by a user, as reported by a "list my shares" query.
message ServerInfo_DeckShareSummary {
optional uint32 id = 1;
optional string name = 2;
optional uint64 creation_time = 3;
optional uint64 expires_at = 4;
optional uint32 item_count = 5;
}

View file

@ -1,10 +1,21 @@
syntax = "proto2";
message ServerInfo_DeckStorage_File {
optional uint32 creation_time = 1;
optional bool is_public = 2;
// Preview metadata computed by the uploading client, so other clients can
// render this deck (e.g. in a visual storage grid) without downloading the
// full deck list. Empty for decks uploaded before the metadata columns.
optional string banner_card_name = 3;
optional string banner_card_provider = 4;
optional string color_identity = 5;
// Tag names associated with the deck. Empty for decks uploaded before the
// tags column existed.
repeated string tags = 6;
}
message ServerInfo_DeckStorage_Folder {
repeated ServerInfo_DeckStorage_TreeItem items = 1;
optional bool is_public = 2;
}
message ServerInfo_DeckStorage_TreeItem {

View file

@ -28,6 +28,14 @@ message SessionCommand {
FORGOT_PASSWORD_CHALLENGE = 1023;
REQUEST_PASSWORD_SALT = 1024;
SET_CARD_ART_PARAMS = 1025;
DECK_SHARE_CREATE = 1026;
DECK_SHARE_LIST = 1027;
DECK_SHARE_DOWNLOAD = 1028;
DECK_LIST_OTHER_USER = 1029;
DECK_SET_VISIBILITY = 1030;
DECK_DOWNLOAD_PUBLIC = 1031;
DECK_SHARE_LIST_MINE = 1032;
DECK_SHARE_REMOVE = 1033;
REPLAY_LIST = 1100;
REPLAY_DOWNLOAD = 1101;
REPLAY_MODIFY_MATCH = 1102;