// ## Device to Device Protocol
//
// ### General Information
//
// **Encryption cipher:** XSalsa20-Poly1305, unless otherwise specified.
//
// All strings are UTF-8 encoded.

syntax = "proto3";

package d2d;

option java_package = "ch.threema.protobuf.d2d";
option java_multiple_files = true;
option php_namespace = "Threema\\Protocols\\D2d";

import "common.proto";
import "md-d2d-sync.proto";

// D2D protocol versions.
//
// Note 1: The most significant byte is the major version and the least
// significant byte is the minor version.
//
// Note 2: Once the D2D protocol is more stable, an unknown major version can be
// interpreted as incompatible. For now, we only have 0.X versions that define
// in which way they break compatibility.
enum ProtocolVersion {
  // The version is unspecified.
  UNSPECIFIED = 0;

  // V0.1
  //
  // Devices using this version use the opportunistic (but problematic) group
  // sync mechanism via pure CSP reflection. D2D group sync reflection was
  // totally underspecified but is partially supported on the receiving side.
  V0_1 = 0x0001;

  // V0.2
  //
  // Builds on V0.1 with backwards compatibility to V0.1. Devices using this
  // version use the explicit group sync mechanism via D2D sync reflection.
  //
  // Upon reception, V0.2 devices detecting a reflected message will switch over
  // the version, and:
  //
  // - for V0.1 in combination with a CSP group message, fall back to the
  //   backwards compatibility mode and update the group according to the
  //   message.
  // - for V0.2+ in combination with a CSP group message, ignore it.
  V0_2 = 0x0002;

  // V0.3
  //
  // Builds on V0.2 but removes the backwards compatibility to V0.1.
  //
  // Upon reception, V0.2 devices detecting a reflected message will switch over
  // the version, and:
  //
  // - for V0.1 in combination with a CSP group message, spit out a warning that
  //   the group is going to desync.
  // - for V0.2+ in combination with a CSP group message, ignore it.
  V0_3 = 0x0003;
}

// Data shared across all devices and transmitted during the handshake.
message SharedDeviceData {
  // Random amount of padding, ignored by the receiver
  bytes padding = 1;

  // Current lowest protocol version that must be supported by all devices
  uint32 version = 2;
}

// Metadata about a device, determined by the device itself.
message DeviceInfo {
  // Random amount of padding, ignored by the receiver
  bytes padding = 1;

  // Platform
  enum Platform {
    // Unknown platform
    UNSPECIFIED = 0;
    // Android
    ANDROID = 1;
    // Apple iOS
    IOS = 2;
    // Desktop application
    DESKTOP = 3;
    // Web application
    WEB = 4;
  }
  Platform platform = 2;

  // Platform details (smartphone model / browser), e.g. "Firefox 91.0.2" or
  // "iPhone 11 Pro"
  string platform_details = 3;

  // App version, e.g. "4.52" (Android) or "4.6.12b2653" (iOS)
  string app_version = 4;

  // User defined device label (e.g. "PC at Work"), may be empty if not set.
  // Recommended to not not exceed 64 grapheme clusters.
  string label = 5;
}

// A transaction scope. Used in the d2m transaction messages.
message TransactionScope {
  enum Scope {
    USER_PROFILE_SYNC = 0;
    CONTACT_SYNC = 1;
    GROUP_SYNC = 2;
    DISTRIBUTION_LIST_SYNC = 3;
    SETTINGS_SYNC = 4;
    MDM_PARAMETER_SYNC = 5;
    NEW_DEVICE_SYNC = 6;
    DROP_DEVICE = 7;
    WORK_SYNC_DELTA = 8;
  }
  Scope scope = 1;
}

// Root message
message Envelope {
  // Random amount of padding, ignored by the receiver
  bytes padding = 1;

  // Sender device id
  fixed64 device_id = 13;

  // D2D (`ProtocolVersion`) the device used when it sent this message.
  //
  // If `0`, assume V0.1 (`0x0001`).
  uint32 protocol_version = 3;

  // The enveloped reflected message
  oneof content {
    OutgoingMessage outgoing_message = 2;
    OutgoingMessageUpdate outgoing_message_update = 10;
    IncomingMessage incoming_message = 4;
    IncomingMessageUpdate incoming_message_update = 11;
    UserProfileSync user_profile_sync = 5;
    ContactSync contact_sync = 6;
    GroupSync group_sync = 7;
    DistributionListSync distribution_list_sync = 8;
    SettingsSync settings_sync = 9;
    MdmParameterSync mdm_parameter_sync = 12;
  };
}

// Unique conversation identifier.
message ConversationId {
  // A contact's Threema ID, distribution list ID or group identity to identify
  // the conversation.
  oneof id {
    string contact = 1;
    fixed64 distribution_list = 2;
    common.GroupIdentity group = 3;
  }
}

// An outgoing message, reflected to other devices.
//
// When sending this message:
//
// 1. [...]
// 2. Set `nonces` to the nonces of the associated CSP
//    `e2e.message-with-metadata` (or `e2e.legacy-message`) messages that
//    contained the `body` in encrypted form.¹
//
// When receiving this message:
//
// 1. Add all `nonces` to the CSP nonce storage (discarding any nonces that
//    already exist in the nonce storage).
// 2. If a message with the same `message_id` exists within the associated
//    `conversation`, discard the message and abort these steps.
// 3. [...]
//
// ¹: For contacts and distribution lists, there will be exactly one nonce. For
// groups, there will be as many nonces as there are group members minus one.
message OutgoingMessage {
  // Conversation ID of the enclosed message.
  //
  // Note: If the conversation is of type group, group and group creator id of
  // the enclosed CSP E2E message must match the values of the supplied group
  // identity. Otherwise, the message must be considered invalid.
  ConversationId conversation = 1;

  // Unique ID of the enclosed message
  fixed64 message_id = 2;

  // Optional thread message ID (the message ID of the last incoming message in
  // the current conversation)
  optional fixed64 thread_message_id = 6;

  // Unix-ish timestamp in milliseconds for when the enclosed message has been
  // created
  //
  // Note: Take this value from the
  // `csp.payload.legacy-message`/`csp.payload.message-with-metadata-box` that
  // enclosed the message.
  uint64 created_at = 3;

  // Enclosed message's type
  common.CspE2eMessageType type = 4;

  // The message's body, i.e. the unpadded `csp.e2e.container.padded-data`
  bytes body = 5;

  // Nonces the message was encrypted with towards each receiver (the shared
  // secret derived from the long-term keys).
  //
  // Optional for now, always required in a future version.
  repeated bytes nonces = 7;
}

// Update one or more existing outgoing messages.
message OutgoingMessageUpdate {
  // Mark the referred message as sent (acknowledged by the chat server).
  //
  // Note 1: The timestamp of the `reflect-ack`/`reflected` message determines
  // the timestamp for when the referred message has been sent.
  //
  // Note 2: This indicates that the referred message has been successfully
  // stored in the message queue of the server. It does NOT indicate that the
  // referred message has been delivered to the intended receiver.
  message Sent {}

  message Update {
    // Conversation ID of the referred message.
    ConversationId conversation = 1;

    // Unique ID of the referred message
    fixed64 message_id = 2;

    // Update type
    oneof update {
      // Mark the referred message as sent
      Sent sent = 3;
    }
  }

  // Updates
  repeated Update updates = 1;
}

// An incoming message, reflected to other devices.
//
//
// When sending this message:
//
// 1. [...]
// 2. Set `nonce` to the nonce of `e2e.message-with-metadata` (or
//    `e2e.legacy-message`) that contained the `body` in encrypted form.
//
// When receiving this message:
//
// 1. Add `nonce` to the CSP nonce storage (discard a nonces that already exist
//    in the nonce storage).
// 2. If a message with the same `message_id` exists within the associated
//    `conversation`, discard the message and abort these steps.
// 3. [...]
message IncomingMessage {
  reserved 4; // Skipped by accident, available to use!

  // Sender's Threema ID
  string sender_identity = 1;

  // Unique ID of the enclosed message
  fixed64 message_id = 2;

  // Unix-ish timestamp in milliseconds for when the enclosed message has been
  // created.
  //
  // Note: Take this value from the
  // `csp.payload.legacy-message`/`csp.payload.message-with-metadata-box` that
  // enclosed the message.
  uint64 created_at = 3;

  // Enclosed message's type.
  common.CspE2eMessageType type = 5;

  // The message's body, i.e. the unpadded `csp.e2e.container.padded-data`
  bytes body = 6;

  // Nonce the message was encrypted with by the sender (the shared secret
  // derived from the long-term keys).
  //
  // Optional for now, always required in a future version.
  bytes nonce = 7;
}

// Update one or more existing incoming messages.
message IncomingMessageUpdate {
  // Mark the referred message as read.
  //
  // Note: This may only be used when _read receipts_ have been turned off, i.e.
  // as a replacement for reflecting `delivery-receipt` type _read_ (`0x02`).
  message Read {
    // Unix-ish timestamp in milliseconds for when the referred message has been
    // read.
    uint64 at = 1;
  }

  message Update {
    // Conversation ID of the referred message.
    ConversationId conversation = 1;

    // Unique ID of the referred message
    fixed64 message_id = 2;

    // Update type
    oneof update {
      // Mark the referred message as read
      Read read = 3;
    }
  }

  // Updates
  repeated Update updates = 1;
}

// User profile synchronisation message.
message UserProfileSync {
  // Update the user's profile
  message Update { d2d_sync.UserProfile user_profile = 1; }

  // Synchronisation type
  oneof action { Update update = 1; }
}

// Contact synchronisation message.
message ContactSync {
  // Create a Threema contact.
  message Create { d2d_sync.Contact contact = 1; }

  // Update a Threema contact.
  message Update { d2d_sync.Contact contact = 1; }

  // Synchronisation type
  oneof action {
    // Create a Threema contact
    Create create = 1;
    // Update a Threema contact
    Update update = 2;
  }
}

// Group synchronisation message.
message GroupSync {
  // Create a group.
  message Create { d2d_sync.Group group = 1; }

  // Update a group.
  //
  // When receiving this variant:
  //
  // 1. Let `current` be a snapshot of the current group state.
  // 2. Persist the update to the group.
  // 3. Let `member-changes` be an empty list.
  // 4. For each `identity`, `state-change` pair of `member_state_changes`:
  //    1. If `state-change` is `ADDED` and `identity` does not exist in
  //       `current.members`, add the tuple `identity`, `state-change` to
  //       `member-changes`.
  //    2. If `state-change` is `KICKED` or `LEFT` and `identity` does exist in
  //       `current.members`, add the tuple `identity`, `state-change` to
  //       `member-changes`.
  // 5. Group `member-changes` by their associated `state-change` and add
  //    appropriate status messages to the associated conversation.
  message Update {
    d2d_sync.Group group = 1;

    enum MemberStateChange {
      // The member has been added
      ADDED = 0;
      // The member has been kicked from the group.
      KICKED = 1;
      // The member left the group.
      LEFT = 2;
    }

    // A map of the member identity string to the member state change.
    map<string, MemberStateChange> member_state_changes = 2;
  }

  // Delete a group.
  message Delete {
    // Unique group identity
    common.GroupIdentity group_identity = 1;
  }

  // Synchronisation type
  oneof action {
    // Create a group
    Create create = 1;
    // Update a group
    Update update = 2;
    // Delete a group
    Delete delete = 3;
  }
}

// Distribution list synchronisation message.
message DistributionListSync {
  // Create a distribution list.
  message Create { d2d_sync.DistributionList distribution_list = 1; }

  // Update a distribution list.
  message Update { d2d_sync.DistributionList distribution_list = 1; }

  // Delete a distribution list.
  message Delete {
    // Unique ID of the distribution list
    fixed64 distribution_list_id = 1;
  }

  // Synchronisation type
  oneof action {
    // Create a distribution list
    Create create = 1;
    // Update a distribution list
    Update update = 2;
    // Delete a distribution list
    Delete delete = 3;
  }
}

// Settings synchronisation message.
message SettingsSync {
  // Update settings.
  message Update { d2d_sync.Settings settings = 1; }

  // Synchronisation type
  oneof action { Update update = 1; }
}

// MDM parameter synchronisation message.
message MdmParameterSync {
  // Update MDM parameters.
  //
  // When receiving this variant, run the _MDM Merge And Apply Steps_.
  message Update { d2d_sync.MdmParameters parameters = 1; }

  // Synchronisation type
  oneof action { Update update = 1; }
}
