Add initial support for send/receive on CDN2.
parent
1290d0ead9
commit
37a35e8f70
|
@ -115,6 +115,7 @@ android {
|
|||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
|
@ -194,6 +195,7 @@ android {
|
|||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||
|
|
|
@ -19,6 +19,8 @@ public abstract class Attachment {
|
|||
@Nullable
|
||||
private final String fileName;
|
||||
|
||||
private final int cdnNumber;
|
||||
|
||||
@Nullable
|
||||
private final String location;
|
||||
|
||||
|
@ -53,7 +55,7 @@ public abstract class Attachment {
|
|||
private final TransformProperties transformProperties;
|
||||
|
||||
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
|
||||
@Nullable String location, @Nullable String key, @Nullable String relay,
|
||||
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
|
@ -63,6 +65,7 @@ public abstract class Attachment {
|
|||
this.transferState = transferState;
|
||||
this.size = size;
|
||||
this.fileName = fileName;
|
||||
this.cdnNumber = cdnNumber;
|
||||
this.location = location;
|
||||
this.key = key;
|
||||
this.relay = relay;
|
||||
|
@ -108,6 +111,10 @@ public abstract class Attachment {
|
|||
return contentType;
|
||||
}
|
||||
|
||||
public int getCdnNumber() {
|
||||
return cdnNumber;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getLocation() {
|
||||
return location;
|
||||
|
|
|
@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.attachments;
|
|||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
@ -24,14 +22,14 @@ public class DatabaseAttachment extends Attachment {
|
|||
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
||||
boolean hasData, boolean hasThumbnail,
|
||||
String contentType, int transferProgress, long size,
|
||||
String fileName, String location, String key, String relay,
|
||||
String fileName, int cdnNumber, String location, String key, String relay,
|
||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable TransformProperties transformProperties, int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
|
|
|
@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
|||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
@ -18,13 +18,13 @@ import java.util.List;
|
|||
public class PointerAttachment extends Attachment {
|
||||
|
||||
private PointerAttachment(@NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName, @NonNull String location,
|
||||
@Nullable String fileName, int cdnNumber, @NonNull String location,
|
||||
@Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -93,7 +93,8 @@ public class PointerAttachment extends Attachment {
|
|||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
pointer.get().asPointer().getSize().or(0),
|
||||
pointer.get().asPointer().getFileName().orNull(),
|
||||
String.valueOf(pointer.get().asPointer().getId()),
|
||||
pointer.get().asPointer().getCdnNumber(),
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orNull(),
|
||||
fastPreflightId,
|
||||
|
@ -114,7 +115,8 @@ public class PointerAttachment extends Attachment {
|
|||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
|
||||
pointer.getFileName(),
|
||||
String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0),
|
||||
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null);
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
||||
|
@ -27,7 +27,7 @@ public class UriAttachment extends Attachment {
|
|||
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ public class AttachmentDatabase extends Database {
|
|||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
static final String UPLOAD_TIMESTAMP = "upload_timestamp";
|
||||
static final String CDN_NUMBER = "cdn_number";
|
||||
|
||||
public static final String DIRECTORY = "parts";
|
||||
|
||||
|
@ -139,13 +140,14 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
private static final String[] PROJECTION = new String[] {ROW_ID,
|
||||
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
|
||||
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
|
||||
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
|
||||
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE,
|
||||
QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT,
|
||||
CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID,
|
||||
DATA_HASH, BLUR_HASH, TRANSFORM_PROPERTIES, TRANSFER_FILE,
|
||||
DISPLAY_ORDER, UPLOAD_TIMESTAMP };
|
||||
CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL,
|
||||
TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL,
|
||||
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
|
||||
FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM,
|
||||
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, BLUR_HASH,
|
||||
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
|
||||
UPLOAD_TIMESTAMP };
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
|
||||
MMS_ID + " INTEGER, " +
|
||||
|
@ -184,7 +186,8 @@ public class AttachmentDatabase extends Database {
|
|||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " +
|
||||
TRANSFER_FILE + " TEXT DEFAULT NULL, " +
|
||||
DISPLAY_ORDER + " INTEGER DEFAULT 0, " +
|
||||
UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0);";
|
||||
UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
CDN_NUMBER + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
|
||||
|
@ -532,6 +535,7 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(CDN_NUMBER, 0);
|
||||
values.put(CONTENT_LOCATION, (String)null);
|
||||
values.put(CONTENT_DISPOSITION, (String)null);
|
||||
values.put(DIGEST, (byte[])null);
|
||||
|
@ -584,6 +588,7 @@ public class AttachmentDatabase extends Database {
|
|||
contentValues.put(DATA_RANDOM, sourceDataInfo.random);
|
||||
|
||||
contentValues.put(TRANSFER_STATE, sourceAttachment.getTransferState());
|
||||
contentValues.put(CDN_NUMBER, sourceAttachment.getCdnNumber());
|
||||
contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation());
|
||||
contentValues.put(DIGEST, sourceAttachment.getDigest());
|
||||
contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey());
|
||||
|
@ -630,6 +635,7 @@ public class AttachmentDatabase extends Database {
|
|||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(CDN_NUMBER, attachment.getCdnNumber());
|
||||
values.put(CONTENT_LOCATION, attachment.getLocation());
|
||||
values.put(DIGEST, attachment.getDigest());
|
||||
values.put(CONTENT_DISPOSITION, attachment.getKey());
|
||||
|
@ -1105,6 +1111,7 @@ public class AttachmentDatabase extends Database {
|
|||
object.getInt(TRANSFER_STATE),
|
||||
object.getLong(SIZE),
|
||||
object.getString(FILE_NAME),
|
||||
object.getInt(CDN_NUMBER),
|
||||
object.getString(CONTENT_LOCATION),
|
||||
object.getString(CONTENT_DISPOSITION),
|
||||
object.getString(NAME),
|
||||
|
@ -1138,6 +1145,7 @@ public class AttachmentDatabase extends Database {
|
|||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(CDN_NUMBER)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
|
||||
|
@ -1197,6 +1205,7 @@ public class AttachmentDatabase extends Database {
|
|||
contentValues.put(CONTENT_TYPE, template.getContentType());
|
||||
contentValues.put(TRANSFER_STATE, attachment.getTransferState());
|
||||
contentValues.put(UNIQUE_ID, uniqueId);
|
||||
contentValues.put(CDN_NUMBER, useTemplateUpload ? template.getCdnNumber() : attachment.getCdnNumber());
|
||||
contentValues.put(CONTENT_LOCATION, useTemplateUpload ? template.getLocation() : attachment.getLocation());
|
||||
contentValues.put(DIGEST, useTemplateUpload ? template.getDigest() : attachment.getDigest());
|
||||
contentValues.put(CONTENT_DISPOSITION, useTemplateUpload ? template.getKey() : attachment.getKey());
|
||||
|
|
|
@ -282,7 +282,7 @@ public final class GroupDatabase extends Database {
|
|||
contentValues.put(MEMBERS, RecipientId.toSerializedList(members));
|
||||
|
||||
if (avatar != null) {
|
||||
contentValues.put(AVATAR_ID, avatar.getId());
|
||||
contentValues.put(AVATAR_ID, avatar.getRemoteId().getV2().get());
|
||||
contentValues.put(AVATAR_KEY, avatar.getKey());
|
||||
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
|
||||
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
|
||||
|
@ -326,7 +326,7 @@ public final class GroupDatabase extends Database {
|
|||
if (title != null) contentValues.put(TITLE, title);
|
||||
|
||||
if (avatar != null) {
|
||||
contentValues.put(AVATAR_ID, avatar.getId());
|
||||
contentValues.put(AVATAR_ID, avatar.getRemoteId().getV2().get());
|
||||
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
|
||||
contentValues.put(AVATAR_KEY, avatar.getKey());
|
||||
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
|
||||
|
|
|
@ -31,6 +31,7 @@ public class MediaDatabase extends Database {
|
|||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", "
|
||||
|
|
|
@ -198,6 +198,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
|
||||
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
|
||||
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
|
||||
"'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," +
|
||||
"'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," +
|
||||
|
|
|
@ -346,6 +346,7 @@ public class MmsSmsDatabase extends Database {
|
|||
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
|
||||
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
|
||||
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
|
||||
"'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " +
|
||||
"'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " +
|
||||
|
@ -455,30 +456,6 @@ public class MmsSmsDatabase extends Database {
|
|||
mmsColumnsPresent.add(MmsDatabase.STATUS);
|
||||
mmsColumnsPresent.add(MmsDatabase.UNIDENTIFIED);
|
||||
mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE);
|
||||
|
||||
mmsColumnsPresent.add(AttachmentDatabase.ROW_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.MMS_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.SIZE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.FILE_NAME);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.DATA);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.THUMBNAIL);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_TYPE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_LOCATION);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.DIGEST);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.FAST_PREFLIGHT_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.VOICE_NOTE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.WIDTH);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.HEIGHT);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.QUOTE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.STICKER_PACK_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.STICKER_PACK_KEY);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.STICKER_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.CAPTION);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.NAME);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.ATTACHMENT_JSON_ALIAS);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_ID);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_AUTHOR);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY);
|
||||
|
|
|
@ -126,8 +126,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int AVATAR_LOCATION_MIGRATION = 54;
|
||||
private static final int GROUPS_V2 = 55;
|
||||
private static final int ATTACHMENT_UPLOAD_TIMESTAMP = 56;
|
||||
private static final int ATTACHMENT_CDN_NUMBER = 57;
|
||||
|
||||
private static final int DATABASE_VERSION = 56;
|
||||
private static final int DATABASE_VERSION = ATTACHMENT_CDN_NUMBER;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -863,6 +864,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE part ADD COLUMN upload_timestamp DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < ATTACHMENT_CDN_NUMBER) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN cdn_number INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
|||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.IasKeyStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -83,8 +84,11 @@ public class ApplicationDependencies {
|
|||
if (messageSender == null) {
|
||||
messageSender = provider.provideSignalServiceMessageSender();
|
||||
} else {
|
||||
messageSender.setMessagePipe(IncomingMessageObserver.getPipe(), IncomingMessageObserver.getUnidentifiedPipe());
|
||||
messageSender.setIsMultiDevice(TextSecurePreferences.isMultiDevice(application));
|
||||
messageSender.update(
|
||||
IncomingMessageObserver.getPipe(),
|
||||
IncomingMessageObserver.getUnidentifiedPipe(),
|
||||
TextSecurePreferences.isMultiDevice(application),
|
||||
FeatureFlags.attachmentsV3());
|
||||
}
|
||||
|
||||
return messageSender;
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.AlarmSleepTimer;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -79,6 +80,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||
new SignalProtocolStoreImpl(context),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
TextSecurePreferences.isMultiDevice(context),
|
||||
FeatureFlags.attachmentsV3(),
|
||||
Optional.fromNullable(IncomingMessageObserver.getPipe()),
|
||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
|
|
|
@ -271,7 +271,7 @@ public final class GroupV1MessageProcessor {
|
|||
|
||||
if (group.getAvatar().isPresent() && group.getAvatar().get().isPointer()) {
|
||||
builder.setAvatar(AttachmentPointer.newBuilder()
|
||||
.setId(group.getAvatar().get().asPointer().getId())
|
||||
.setCdnId(group.getAvatar().get().asPointer().getRemoteId().getV2().get())
|
||||
.setKey(ByteString.copyFrom(group.getAvatar().get().asPointer().getKey()))
|
||||
.setContentType(group.getAvatar().get().getContentType()));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
|
@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
@ -182,13 +184,8 @@ public class AttachmentDownloadJob extends BaseJob {
|
|||
}
|
||||
|
||||
try {
|
||||
long id = Long.parseLong(attachment.getLocation());
|
||||
byte[] key = Base64.decode(attachment.getKey());
|
||||
String relay = null;
|
||||
|
||||
if (TextUtils.isEmpty(attachment.getRelay())) {
|
||||
relay = attachment.getRelay();
|
||||
}
|
||||
final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation());
|
||||
final byte[] key = Base64.decode(attachment.getKey());
|
||||
|
||||
if (attachment.getDigest() != null) {
|
||||
Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest()));
|
||||
|
@ -196,7 +193,7 @@ public class AttachmentDownloadJob extends BaseJob {
|
|||
Log.i(TAG, "Downloading attachment with no digest...");
|
||||
}
|
||||
|
||||
return new SignalServiceAttachmentPointer(id, null, key,
|
||||
return new SignalServiceAttachmentPointer(attachment.getCdnNumber(), remoteId, null, key,
|
||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
||||
Optional.absent(),
|
||||
0, 0,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -13,14 +11,12 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
|||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
|
@ -90,7 +86,7 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
attachment.deleteOnExit();
|
||||
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent(), System.currentTimeMillis());
|
||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent(), System.currentTimeMillis());
|
||||
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
|
||||
|
||||
AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream);
|
||||
|
|
|
@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.jobs;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
|
@ -43,6 +44,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||
|
@ -184,10 +186,11 @@ public abstract class PushSendJob extends SendJob {
|
|||
}
|
||||
|
||||
try {
|
||||
long id = Long.parseLong(attachment.getLocation());
|
||||
byte[] key = Base64.decode(attachment.getKey());
|
||||
final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation());
|
||||
final byte[] key = Base64.decode(attachment.getKey());
|
||||
|
||||
return new SignalServiceAttachmentPointer(id,
|
||||
return new SignalServiceAttachmentPointer(attachment.getCdnNumber(),
|
||||
remoteId,
|
||||
attachment.getContentType(),
|
||||
key,
|
||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
||||
|
|
|
@ -121,6 +121,16 @@ public class SignalServiceNetworkAccess {
|
|||
final SignalCdnUrl omanGoogleCdn = new SignalCdnUrl("https://www.google.com.om/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl qatarGoogleCdn = new SignalCdnUrl("https://www.google.com.qa/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
|
||||
final SignalCdnUrl baseGoogleCdn2 = new SignalCdnUrl("https://www.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl baseAndroidCdn2 = new SignalCdnUrl("https://android.clients.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC);
|
||||
final SignalCdnUrl mapsOneAndroidCdn2 = new SignalCdnUrl("https://clients3.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC);
|
||||
final SignalCdnUrl mapsTwoAndroidCdn2 = new SignalCdnUrl("https://clients4.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC);
|
||||
final SignalCdnUrl mailAndroidCdn2 = new SignalCdnUrl("https://inbox.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl egyptGoogleCdn2 = new SignalCdnUrl("https://www.google.com.eg/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl uaeGoogleCdn2 = new SignalCdnUrl("https://www.google.ae/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl omanGoogleCdn2 = new SignalCdnUrl("https://www.google.com.om/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalCdnUrl qatarGoogleCdn2 = new SignalCdnUrl("https://www.google.com.qa/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
|
||||
final SignalContactDiscoveryUrl baseGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
final SignalContactDiscoveryUrl baseAndroidDiscovery = new SignalContactDiscoveryUrl("https://android.clients.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC);
|
||||
final SignalContactDiscoveryUrl mapsOneAndroidDiscovery = new SignalContactDiscoveryUrl("https://clients3.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC);
|
||||
|
@ -165,6 +175,7 @@ public class SignalServiceNetworkAccess {
|
|||
this.censorshipConfiguration = new HashMap<String, SignalServiceConfiguration>() {{
|
||||
put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {egyptGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn, mailAndroidCdn},
|
||||
new SignalCdnUrl[] {egyptGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2, mailAndroidCdn2},
|
||||
new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
|
@ -174,6 +185,7 @@ public class SignalServiceNetworkAccess {
|
|||
|
||||
put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
|
||||
new SignalCdnUrl[] {uaeGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2},
|
||||
new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
|
@ -183,6 +195,7 @@ public class SignalServiceNetworkAccess {
|
|||
|
||||
put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
|
||||
new SignalCdnUrl[] {omanGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2},
|
||||
new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
|
@ -193,6 +206,7 @@ public class SignalServiceNetworkAccess {
|
|||
|
||||
put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {qatarGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
|
||||
new SignalCdnUrl[] {qatarGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2},
|
||||
new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
|
@ -203,6 +217,7 @@ public class SignalServiceNetworkAccess {
|
|||
|
||||
this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))},
|
||||
new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))},
|
||||
new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, new SignalServiceTrustStore(context))},
|
||||
new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))},
|
||||
new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) },
|
||||
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
|
||||
|
|
|
@ -54,6 +54,7 @@ public final class FeatureFlags {
|
|||
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String STORAGE_SERVICE = "android.storageService.2";
|
||||
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -66,7 +67,8 @@ public final class FeatureFlags {
|
|||
PINS_MEGAPHONE_KILL_SWITCH,
|
||||
PROFILE_NAMES_MEGAPHONE,
|
||||
MESSAGE_REQUESTS,
|
||||
STORAGE_SERVICE
|
||||
STORAGE_SERVICE,
|
||||
ATTACHMENTS_V3
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -88,7 +90,8 @@ public final class FeatureFlags {
|
|||
*/
|
||||
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
|
||||
PINS_MEGAPHONE_KILL_SWITCH,
|
||||
STORAGE_SERVICE
|
||||
STORAGE_SERVICE,
|
||||
ATTACHMENTS_V3
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -213,6 +216,11 @@ public final class FeatureFlags {
|
|||
return getValue(STORAGE_SERVICE, false);
|
||||
}
|
||||
|
||||
/** Whether or not we use the attachments v3 form. */
|
||||
public static boolean attachmentsV3() {
|
||||
return getValue(ATTACHMENTS_V3, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -26,7 +26,8 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
|||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
@ -219,7 +220,7 @@ public class SignalServiceMessagePipe {
|
|||
}
|
||||
}
|
||||
|
||||
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException {
|
||||
public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws IOException {
|
||||
try {
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
|
@ -233,7 +234,27 @@ public class SignalServiceMessagePipe {
|
|||
throw new IOException("Non-successful response: " + response.first());
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class);
|
||||
return JsonUtil.fromJson(response.second(), AttachmentV2UploadAttributes.class);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws IOException {
|
||||
try {
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
.setVerb("GET")
|
||||
.setPath("/v3/attachments/form/upload")
|
||||
.build();
|
||||
|
||||
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||
|
||||
if (response.first() < 200 || response.first() >= 300) {
|
||||
throw new IOException("Non-successful response: " + response.first());
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(response.second(), AttachmentV3UploadAttributes.class);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ public class SignalServiceMessageReceiver {
|
|||
{
|
||||
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
|
||||
|
||||
socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener);
|
||||
socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
|
||||
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
|||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
@ -50,12 +51,14 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
|||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
|
||||
|
@ -110,6 +113,7 @@ public class SignalServiceMessageSender {
|
|||
private final AtomicReference<Optional<SignalServiceMessagePipe>> pipe;
|
||||
private final AtomicReference<Optional<SignalServiceMessagePipe>> unidentifiedPipe;
|
||||
private final AtomicBoolean isMultiDevice;
|
||||
private final AtomicBoolean attachmentsV3;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceMessageSender.
|
||||
|
@ -127,12 +131,13 @@ public class SignalServiceMessageSender {
|
|||
SignalProtocolStore store,
|
||||
String signalAgent,
|
||||
boolean isMultiDevice,
|
||||
boolean attachmentsV3,
|
||||
Optional<SignalServiceMessagePipe> pipe,
|
||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
||||
Optional<EventListener> eventListener,
|
||||
ClientZkProfileOperations clientZkProfileOperations)
|
||||
{
|
||||
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations);
|
||||
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, attachmentsV3, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations);
|
||||
}
|
||||
|
||||
public SignalServiceMessageSender(SignalServiceConfiguration urls,
|
||||
|
@ -140,6 +145,7 @@ public class SignalServiceMessageSender {
|
|||
SignalProtocolStore store,
|
||||
String signalAgent,
|
||||
boolean isMultiDevice,
|
||||
boolean attachmentsV3,
|
||||
Optional<SignalServiceMessagePipe> pipe,
|
||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
||||
Optional<EventListener> eventListener,
|
||||
|
@ -151,6 +157,7 @@ public class SignalServiceMessageSender {
|
|||
this.pipe = new AtomicReference<>(pipe);
|
||||
this.unidentifiedPipe = new AtomicReference<>(unidentifiedPipe);
|
||||
this.isMultiDevice = new AtomicBoolean(isMultiDevice);
|
||||
this.attachmentsV3 = new AtomicBoolean(attachmentsV3);
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
|
@ -336,13 +343,11 @@ public class SignalServiceMessageSender {
|
|||
socket.cancelInFlightRequests();
|
||||
}
|
||||
|
||||
public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) {
|
||||
public void update(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe, boolean isMultiDevice, boolean attachmentsV3) {
|
||||
this.pipe.set(Optional.fromNullable(pipe));
|
||||
this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe));
|
||||
}
|
||||
|
||||
public void setIsMultiDevice(boolean isMultiDevice) {
|
||||
this.isMultiDevice.set(isMultiDevice);
|
||||
this.attachmentsV3.set(attachmentsV3);
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment) throws IOException {
|
||||
|
@ -357,26 +362,35 @@ public class SignalServiceMessageSender {
|
|||
attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
|
||||
AttachmentUploadAttributes uploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
if (attachmentsV3.get()) {
|
||||
return uploadAttachmentV3(attachment, attachmentKey, attachmentData);
|
||||
} else {
|
||||
return uploadAttachmentV2(attachment, attachmentKey, attachmentData);
|
||||
}
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentPointer uploadAttachmentV2(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
AttachmentV2UploadAttributes v2UploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
|
||||
if (localPipe.isPresent()) {
|
||||
Log.d(TAG, "Using pipe to retrieve attachment upload attributes...");
|
||||
try {
|
||||
uploadAttributes = localPipe.get().getAttachmentUploadAttributes();
|
||||
v2UploadAttributes = localPipe.get().getAttachmentV2UploadAttributes();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back...");
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadAttributes == null) {
|
||||
if (v2UploadAttributes == null) {
|
||||
Log.d(TAG, "Not using pipe to retrieve attachment upload attributes...");
|
||||
uploadAttributes = socket.getAttachmentUploadAttributes();
|
||||
v2UploadAttributes = socket.getAttachmentV2UploadAttributes();
|
||||
}
|
||||
|
||||
Pair<Long, byte[]> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, uploadAttributes);
|
||||
Pair<Long, byte[]> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes);
|
||||
|
||||
return new SignalServiceAttachmentPointer(attachmentIdAndDigest.first(),
|
||||
return new SignalServiceAttachmentPointer(0,
|
||||
new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
|
@ -390,6 +404,41 @@ public class SignalServiceMessageSender {
|
|||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException {
|
||||
AttachmentV3UploadAttributes v3UploadAttributes = null;
|
||||
Optional<SignalServiceMessagePipe> localPipe = pipe.get();
|
||||
|
||||
if (localPipe.isPresent()) {
|
||||
Log.d(TAG, "Using pipe to retrieve attachment upload attributes...");
|
||||
try {
|
||||
v3UploadAttributes = localPipe.get().getAttachmentV3UploadAttributes();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back...");
|
||||
}
|
||||
}
|
||||
|
||||
if (v3UploadAttributes == null) {
|
||||
Log.d(TAG, "Not using pipe to retrieve attachment upload attributes...");
|
||||
v3UploadAttributes = socket.getAttachmentV3UploadAttributes();
|
||||
}
|
||||
|
||||
byte[] digest = socket.uploadAttachment(attachmentData, v3UploadAttributes);
|
||||
return new SignalServiceAttachmentPointer(v3UploadAttributes.getCdn(),
|
||||
new SignalServiceAttachmentRemoteId(v3UploadAttributes.getKey()),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.of(digest),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.getCaption(),
|
||||
attachment.getBlurHash(),
|
||||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
|
||||
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
throws IOException, UntrustedIdentityException
|
||||
|
@ -1205,12 +1254,20 @@ public class SignalServiceMessageSender {
|
|||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
|
||||
.setCdnNumber(attachment.getCdnNumber())
|
||||
.setContentType(attachment.getContentType())
|
||||
.setId(attachment.getId())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get());
|
||||
|
||||
if (attachment.getRemoteId().getV2().isPresent()) {
|
||||
builder.setCdnId(attachment.getRemoteId().getV2().get());
|
||||
}
|
||||
|
||||
if (attachment.getRemoteId().getV3().isPresent()) {
|
||||
builder.setCdnKey(attachment.getRemoteId().getV3().get());
|
||||
}
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
|
@ -1245,8 +1302,7 @@ public class SignalServiceMessageSender {
|
|||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceAttachmentPointer pointer = uploadAttachment(attachment);
|
||||
return createAttachmentPointer(pointer);
|
||||
return createAttachmentPointer(uploadAttachment(attachment));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,28 +18,31 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
|||
*/
|
||||
public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
||||
|
||||
private final long id;
|
||||
private final byte[] key;
|
||||
private final Optional<Integer> size;
|
||||
private final Optional<byte[]> preview;
|
||||
private final Optional<byte[]> digest;
|
||||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
private final long uploadTimestamp;
|
||||
private final int cdnNumber;
|
||||
private final SignalServiceAttachmentRemoteId remoteId;
|
||||
private final byte[] key;
|
||||
private final Optional<Integer> size;
|
||||
private final Optional<byte[]> preview;
|
||||
private final Optional<byte[]> digest;
|
||||
private final Optional<String> fileName;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
private final long uploadTimestamp;
|
||||
|
||||
public SignalServiceAttachmentPointer(long id, String contentType, byte[] key,
|
||||
Optional<Integer> size, Optional<byte[]> preview,
|
||||
int width, int height,
|
||||
Optional<byte[]> digest, Optional<String> fileName,
|
||||
boolean voiceNote, Optional<String> caption,
|
||||
Optional<String> blurHash, long uploadTimestamp)
|
||||
public SignalServiceAttachmentPointer(int cdnNumber, SignalServiceAttachmentRemoteId remoteId,
|
||||
String contentType, byte[] key,
|
||||
Optional<Integer> size, Optional<byte[]> preview, int width,
|
||||
int height, Optional<byte[]> digest,
|
||||
Optional<String> fileName, boolean voiceNote,
|
||||
Optional<String> caption, Optional<String> blurHash,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType);
|
||||
this.id = id;
|
||||
this.cdnNumber = cdnNumber;
|
||||
this.remoteId = remoteId;
|
||||
this.key = key;
|
||||
this.size = size;
|
||||
this.preview = preview;
|
||||
|
@ -53,8 +56,12 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment {
|
|||
this.uploadTimestamp = uploadTimestamp;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
public int getCdnNumber() {
|
||||
return cdnNumber;
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentRemoteId getRemoteId() {
|
||||
return remoteId;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
|
||||
/**
|
||||
* Represents a signal service attachment identifier. This can be either a CDN key or a long, but
|
||||
* not both at once. Attachments V2 used a long as an attachment identifier. This lacks sufficient
|
||||
* entropy to reduce the likelihood of any two uploads going to the same location within a 30-day
|
||||
* window. Attachments V3 uses an opaque string as an attachment identifier which provides more
|
||||
* flexibility in the amount of entropy present.
|
||||
*/
|
||||
public final class SignalServiceAttachmentRemoteId {
|
||||
private final Optional<Long> v2;
|
||||
private final Optional<String> v3;
|
||||
|
||||
public SignalServiceAttachmentRemoteId(long v2) {
|
||||
this.v2 = Optional.of(v2);
|
||||
this.v3 = Optional.absent();
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentRemoteId(String v3) {
|
||||
this.v2 = Optional.absent();
|
||||
this.v3 = Optional.of(v3);
|
||||
}
|
||||
|
||||
public Optional<Long> getV2() {
|
||||
return v2;
|
||||
}
|
||||
|
||||
public Optional<String> getV3() {
|
||||
return v3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (v2.isPresent()) {
|
||||
return v2.get().toString();
|
||||
} else {
|
||||
return v3.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static SignalServiceAttachmentRemoteId from(AttachmentPointer attachmentPointer) throws ProtocolInvalidMessageException {
|
||||
switch (attachmentPointer.getAttachmentIdentifierCase()) {
|
||||
case CDNID:
|
||||
return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnId());
|
||||
case CDNKEY:
|
||||
return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnKey());
|
||||
case ATTACHMENTIDENTIFIER_NOT_SET:
|
||||
throw new ProtocolInvalidMessageException(new InvalidMessageException("AttachmentPointer CDN location not set"), null, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesses that strings which contain values parseable to {@code long} should use an id-based
|
||||
* CDN path. Otherwise, use key-based CDN path.
|
||||
*/
|
||||
public static SignalServiceAttachmentRemoteId from(String string) {
|
||||
try {
|
||||
return new SignalServiceAttachmentRemoteId(Long.parseLong(string));
|
||||
} catch (NumberFormatException e) {
|
||||
return new SignalServiceAttachmentRemoteId(string);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -562,7 +562,7 @@ public final class SignalServiceContent {
|
|||
Optional.<byte[]>absent());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) {
|
||||
private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (!content.hasQuote()) return null;
|
||||
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = new LinkedList<>();
|
||||
|
@ -586,7 +586,7 @@ public final class SignalServiceContent {
|
|||
}
|
||||
}
|
||||
|
||||
private static List<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) {
|
||||
private static List<SignalServiceDataMessage.Preview> createPreviews(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getPreviewCount() <= 0) return null;
|
||||
|
||||
List<SignalServiceDataMessage.Preview> results = new LinkedList<>();
|
||||
|
@ -606,7 +606,7 @@ public final class SignalServiceContent {
|
|||
return results;
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) {
|
||||
private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (!content.hasSticker() ||
|
||||
!content.getSticker().hasPackId() ||
|
||||
!content.getSticker().hasPackKey() ||
|
||||
|
@ -641,7 +641,7 @@ public final class SignalServiceContent {
|
|||
reaction.getTargetSentTimestamp());
|
||||
}
|
||||
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) {
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getContactCount() <= 0) return null;
|
||||
|
||||
List<SharedContact> results = new LinkedList<>();
|
||||
|
@ -736,8 +736,9 @@ public final class SignalServiceContent {
|
|||
return results;
|
||||
}
|
||||
|
||||
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) {
|
||||
return new SignalServiceAttachmentPointer(pointer.getId(),
|
||||
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws ProtocolInvalidMessageException {
|
||||
return new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
|
||||
|
@ -795,7 +796,8 @@ public final class SignalServiceContent {
|
|||
if (content.getGroup().hasAvatar()) {
|
||||
SignalServiceProtos.AttachmentPointer pointer = content.getGroup().getAvatar();
|
||||
|
||||
avatar = new SignalServiceAttachmentPointer(pointer.getId(),
|
||||
avatar = new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
Optional.of(pointer.getSize()),
|
||||
|
|
|
@ -11,6 +11,7 @@ public final class SignalServiceConfiguration {
|
|||
|
||||
private final SignalServiceUrl[] signalServiceUrls;
|
||||
private final SignalCdnUrl[] signalCdnUrls;
|
||||
private final SignalCdnUrl[] signalCdn2Urls;
|
||||
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
|
||||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
private final SignalStorageUrl[] signalStorageUrls;
|
||||
|
@ -20,6 +21,7 @@ public final class SignalServiceConfiguration {
|
|||
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
|
||||
SignalCdnUrl[] signalCdnUrls,
|
||||
SignalCdnUrl[] signalCdn2Urls,
|
||||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
|
||||
SignalStorageUrl[] signalStorageUrls,
|
||||
|
@ -29,6 +31,7 @@ public final class SignalServiceConfiguration {
|
|||
{
|
||||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrls = signalCdnUrls;
|
||||
this.signalCdn2Urls = signalCdn2Urls;
|
||||
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
|
||||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
this.signalStorageUrls = signalStorageUrls;
|
||||
|
@ -45,6 +48,10 @@ public final class SignalServiceConfiguration {
|
|||
return signalCdnUrls;
|
||||
}
|
||||
|
||||
public SignalCdnUrl[] getSignalCdn2Urls() {
|
||||
return signalCdn2Urls;
|
||||
}
|
||||
|
||||
public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() {
|
||||
return signalContactDiscoveryUrls;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.whispersystems.signalservice.internal.push;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class AttachmentUploadAttributes {
|
||||
public class AttachmentV2UploadAttributes {
|
||||
@JsonProperty
|
||||
private String url;
|
||||
|
||||
|
@ -34,7 +34,7 @@ public class AttachmentUploadAttributes {
|
|||
@JsonProperty
|
||||
private String attachmentIdString;
|
||||
|
||||
public AttachmentUploadAttributes() {}
|
||||
public AttachmentV2UploadAttributes() {}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
|
@ -0,0 +1,38 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class AttachmentV3UploadAttributes {
|
||||
@JsonProperty
|
||||
private int cdn;
|
||||
|
||||
@JsonProperty
|
||||
private String key;
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, String> headers;
|
||||
|
||||
@JsonProperty
|
||||
private String signedUploadLocation;
|
||||
|
||||
public AttachmentV3UploadAttributes() {
|
||||
}
|
||||
|
||||
public int getCdn() {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getSignedUploadLocation() {
|
||||
return signedUploadLocation;
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import org.whispersystems.signalservice.FeatureFlags;
|
|||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
|
@ -114,6 +115,7 @@ import okhttp3.Call;
|
|||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.Dns;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
|
@ -159,7 +161,8 @@ public class PushServiceSocket {
|
|||
private static final String MESSAGE_PATH = "/v1/messages/%s";
|
||||
private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d";
|
||||
private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s";
|
||||
private static final String ATTACHMENT_PATH = "/v2/attachments/form/upload";
|
||||
private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload";
|
||||
private static final String ATTACHMENT_V3_PATH = "/v3/attachments/form/upload";
|
||||
|
||||
private static final String PROFILE_PATH = "/v1/profile/%s";
|
||||
private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s";
|
||||
|
@ -167,14 +170,15 @@ public class PushServiceSocket {
|
|||
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery";
|
||||
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
|
||||
|
||||
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
|
||||
private static final String KBS_AUTH_PATH = "/v1/backup/auth";
|
||||
|
||||
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
||||
private static final String AVATAR_UPLOAD_PATH = "";
|
||||
private static final String ATTACHMENT_KEY_DOWNLOAD_PATH = "attachments/%s";
|
||||
private static final String ATTACHMENT_ID_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
||||
private static final String AVATAR_UPLOAD_PATH = "";
|
||||
|
||||
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||
|
@ -189,6 +193,7 @@ public class PushServiceSocket {
|
|||
|
||||
private final ServiceConnectionHolder[] serviceClients;
|
||||
private final ConnectionHolder[] cdnClients;
|
||||
private final ConnectionHolder[] cdn2Clients;
|
||||
private final ConnectionHolder[] contactDiscoveryClients;
|
||||
private final ConnectionHolder[] keyBackupServiceClients;
|
||||
private final ConnectionHolder[] storageClients;
|
||||
|
@ -207,6 +212,7 @@ public class PushServiceSocket {
|
|||
this.signalAgent = signalAgent;
|
||||
this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.cdn2Clients = createConnectionHolders(configuration.getSignalCdn2Urls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||
|
@ -516,17 +522,23 @@ public class PushServiceSocket {
|
|||
makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity));
|
||||
}
|
||||
|
||||
public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener);
|
||||
final String path;
|
||||
if (cdnPath.getV2().isPresent()) {
|
||||
path = String.format(Locale.US, ATTACHMENT_ID_DOWNLOAD_PATH, cdnPath.getV2().get());
|
||||
} else {
|
||||
path = String.format(Locale.US, ATTACHMENT_KEY_DOWNLOAD_PATH, cdnPath.getV3().get());
|
||||
}
|
||||
downloadFromCdn(destination, cdnNumber, path, maxSizeBytes, listener);
|
||||
}
|
||||
|
||||
public void retrieveSticker(File destination, byte[] packId, int stickerId)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
downloadFromCdn(destination, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
downloadFromCdn(destination, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
}
|
||||
|
||||
public byte[] retrieveSticker(byte[] packId, int stickerId)
|
||||
|
@ -535,7 +547,7 @@ public class PushServiceSocket {
|
|||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
downloadFromCdn(output, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
downloadFromCdn(output, 0, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
@ -546,7 +558,7 @@ public class PushServiceSocket {
|
|||
String hexPackId = Hex.toStringCondensed(packId);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
downloadFromCdn(output, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
|
||||
downloadFromCdn(output, 0, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
|
||||
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
@ -611,7 +623,7 @@ public class PushServiceSocket {
|
|||
public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
downloadFromCdn(destination, path, maxSizeBytes, null);
|
||||
downloadFromCdn(destination, 0, path, maxSizeBytes, null);
|
||||
}
|
||||
|
||||
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
|
@ -867,10 +879,20 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public AttachmentUploadAttributes getAttachmentUploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_PATH, "GET", null);
|
||||
public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_V2_PATH, "GET", null);
|
||||
try {
|
||||
return JsonUtil.fromJson(response, AttachmentUploadAttributes.class);
|
||||
return JsonUtil.fromJson(response, AttachmentV2UploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
String response = makeServiceRequest(ATTACHMENT_V3_PATH, "GET", null);
|
||||
try {
|
||||
return JsonUtil.fromJson(response, AttachmentV3UploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
|
@ -890,7 +912,7 @@ public class PushServiceSocket {
|
|||
null, null);
|
||||
}
|
||||
|
||||
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes)
|
||||
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
long id = Long.parseLong(uploadAttributes.getAttachmentId());
|
||||
|
@ -905,20 +927,31 @@ public class PushServiceSocket {
|
|||
return new Pair<>(id, digest);
|
||||
}
|
||||
|
||||
private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener)
|
||||
public byte[] uploadAttachment(PushAttachmentData attachment, AttachmentV3UploadAttributes uploadAttributes) throws IOException {
|
||||
String resumableUploadUrl = getResumableUploadUrl(uploadAttributes.getSignedUploadLocation(), uploadAttributes.getHeaders());
|
||||
return uploadToCdn2(resumableUploadUrl,
|
||||
attachment.getData(),
|
||||
"application/octet-stream",
|
||||
attachment.getDataSize(),
|
||||
attachment.getOutputStreamFactory(),
|
||||
attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
}
|
||||
|
||||
private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) {
|
||||
downloadFromCdn(outputStream, destination.length(), path, maxSizeBytes, listener);
|
||||
downloadFromCdn(outputStream, destination.length(), cdnNumber, path, maxSizeBytes, listener);
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener)
|
||||
private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
|
||||
ConnectionHolder connectionHolder = getRandom(cdnNumber == 2 ? cdn2Clients : cdnClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
|
@ -1046,6 +1079,107 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
private String getResumableUploadUrl(String signedUrl, Map<String, String> headers) throws IOException {
|
||||
ConnectionHolder connectionHolder = getRandom(cdn2Clients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
final HttpUrl endpointUrl = HttpUrl.get(connectionHolder.url);
|
||||
final HttpUrl signedHttpUrl;
|
||||
try {
|
||||
signedHttpUrl = HttpUrl.get(signedUrl);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Server returned a malformed signed url: " + signedUrl);
|
||||
throw new IOException("Server returned a malformed signed url", e);
|
||||
}
|
||||
|
||||
final HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme(endpointUrl.scheme())
|
||||
.host(endpointUrl.host())
|
||||
.port(endpointUrl.port())
|
||||
.encodedPath(endpointUrl.encodedPath())
|
||||
.addEncodedPathSegments(signedHttpUrl.encodedPath().substring(1))
|
||||
.encodedQuery(signedHttpUrl.encodedQuery())
|
||||
.encodedFragment(signedHttpUrl.encodedFragment());
|
||||
|
||||
Request.Builder request = new Request.Builder().url(urlBuilder.build())
|
||||
.post(RequestBody.create(null, ""));
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
request.header(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.header("host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
try {
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return response.header("location");
|
||||
} else {
|
||||
throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
}
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException {
|
||||
ConnectionHolder connectionHolder = getRandom(cdn2Clients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener, cancelationSignal);
|
||||
Request.Builder request = new Request.Builder().url(resumableUrl)
|
||||
.put(file);
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.header("host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
try {
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
|
||||
if (response.isSuccessful()) return file.getTransmittedDigest();
|
||||
else throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
|
|
|
@ -377,7 +377,10 @@ message AttachmentPointer {
|
|||
VOICE_MESSAGE = 1;
|
||||
}
|
||||
|
||||
optional fixed64 id = 1;
|
||||
oneof attachment_identifier {
|
||||
fixed64 cdnId = 1;
|
||||
string cdnKey = 15;
|
||||
}
|
||||
optional string contentType = 2;
|
||||
optional bytes key = 3;
|
||||
optional uint32 size = 4;
|
||||
|
@ -390,6 +393,8 @@ message AttachmentPointer {
|
|||
optional string caption = 11;
|
||||
optional string blurHash = 12;
|
||||
optional uint64 uploadTimestamp = 13;
|
||||
optional uint32 cdnNumber = 14;
|
||||
// Next ID: 16
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
|
|
Loading…
Reference in New Issue