add PubSubManager, AvatarManager and AxolotlManager
This commit is contained in:
parent
f1e1cf9653
commit
c077e4e8da
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.4.1'
|
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.12.1"
|
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ configurations {
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
ratchetFrom '2.12.0'
|
ratchetFrom '2.12.2'
|
||||||
java {
|
java {
|
||||||
target '**/*.java'
|
target '**/*.java'
|
||||||
googleJavaFormat('1.8').aosp().reflowLongStrings()
|
googleJavaFormat('1.8').aosp().reflowLongStrings()
|
||||||
|
@ -80,11 +80,11 @@ dependencies {
|
||||||
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
|
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
|
||||||
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
||||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.5'
|
implementation 'androidx.exifinterface:exifinterface:1.3.5'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.7.0'
|
implementation 'com.google.android.material:material:1.8.0'
|
||||||
|
|
||||||
implementation "androidx.emoji2:emoji2:1.2.0"
|
implementation "androidx.emoji2:emoji2:1.2.0"
|
||||||
freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0"
|
freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "e2dbbac3327bc8ef188286642b379e7d",
|
"identityHash": "2972255ca35c75ece48909471313d20a",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "account",
|
"tableName": "account",
|
||||||
|
@ -124,6 +124,634 @@
|
||||||
],
|
],
|
||||||
"foreignKeys": []
|
"foreignKeys": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tableName": "avatar_additional",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `avatarId` INTEGER NOT NULL, `avatar_external_url` TEXT, `avatar_external_id` TEXT, `avatar_external_type` TEXT, `avatar_external_bytes` INTEGER NOT NULL, `avatar_external_height` INTEGER NOT NULL, `avatar_external_width` INTEGER NOT NULL, FOREIGN KEY(`avatarId`) REFERENCES `avatar`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarId",
|
||||||
|
"columnName": "avatarId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.url",
|
||||||
|
"columnName": "avatar_external_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.id",
|
||||||
|
"columnName": "avatar_external_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.type",
|
||||||
|
"columnName": "avatar_external_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.bytes",
|
||||||
|
"columnName": "avatar_external_bytes",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.height",
|
||||||
|
"columnName": "avatar_external_height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "external.width",
|
||||||
|
"columnName": "avatar_external_width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_avatar_additional_avatarId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"avatarId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_avatar_additional_avatarId` ON `${TABLE_NAME}` (`avatarId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "avatar",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"avatarId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "avatar",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `thumb_id` TEXT, `thumb_type` TEXT, `thumb_bytes` INTEGER NOT NULL, `thumb_height` INTEGER NOT NULL, `thumb_width` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail.id",
|
||||||
|
"columnName": "thumb_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail.type",
|
||||||
|
"columnName": "thumb_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail.bytes",
|
||||||
|
"columnName": "thumb_bytes",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail.height",
|
||||||
|
"columnName": "thumb_height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnail.width",
|
||||||
|
"columnName": "thumb_width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_avatar_accountId_address",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_avatar_accountId_address` ON `${TABLE_NAME}` (`accountId`, `address`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_device_list",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `receivedAt` INTEGER NOT NULL, `errorCondition` TEXT, `isParsingIssue` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "receivedAt",
|
||||||
|
"columnName": "receivedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "errorCondition",
|
||||||
|
"columnName": "errorCondition",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isParsingIssue",
|
||||||
|
"columnName": "isParsingIssue",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_device_list_accountId_address",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_axolotl_device_list_accountId_address` ON `${TABLE_NAME}` (`accountId`, `address`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_device_list_item",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `deviceListId` INTEGER NOT NULL, `deviceId` INTEGER, FOREIGN KEY(`deviceListId`) REFERENCES `axolotl_device_list`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceListId",
|
||||||
|
"columnName": "deviceListId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceId",
|
||||||
|
"columnName": "deviceId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_device_list_item_deviceListId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"deviceListId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_axolotl_device_list_item_deviceListId` ON `${TABLE_NAME}` (`deviceListId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_device_list_item_deviceListId_deviceId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"deviceListId",
|
||||||
|
"deviceId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_axolotl_device_list_item_deviceListId_deviceId` ON `${TABLE_NAME}` (`deviceListId`, `deviceId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "axolotl_device_list",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"deviceListId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_identity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `deviceId` INTEGER NOT NULL, `identityKey` BLOB NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceId",
|
||||||
|
"columnName": "deviceId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "identityKey",
|
||||||
|
"columnName": "identityKey",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_identity_accountId_address_deviceId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address",
|
||||||
|
"deviceId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_axolotl_identity_accountId_address_deviceId` ON `${TABLE_NAME}` (`accountId`, `address`, `deviceId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_identity_key_pair",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `identityKeyPair` BLOB NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "identityKeyPair",
|
||||||
|
"columnName": "identityKeyPair",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_identity_key_pair_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_axolotl_identity_key_pair_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_pre_key",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `preKeyId` INTEGER NOT NULL, `preKeyRecord` BLOB NOT NULL, `removed` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "preKeyId",
|
||||||
|
"columnName": "preKeyId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "preKeyRecord",
|
||||||
|
"columnName": "preKeyRecord",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "removed",
|
||||||
|
"columnName": "removed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_pre_key_accountId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_axolotl_pre_key_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_session",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `deviceId` INTEGER NOT NULL, `sessionRecord` BLOB NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceId",
|
||||||
|
"columnName": "deviceId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sessionRecord",
|
||||||
|
"columnName": "sessionRecord",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_session_accountId_address_deviceId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address",
|
||||||
|
"deviceId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_axolotl_session_accountId_address_deviceId` ON `${TABLE_NAME}` (`accountId`, `address`, `deviceId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "axolotl_signed_pre_key",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `signedPreKeyId` INTEGER NOT NULL, `signedPreKeyRecord` BLOB NOT NULL, `removed` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signedPreKeyId",
|
||||||
|
"columnName": "signedPreKeyId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signedPreKeyRecord",
|
||||||
|
"columnName": "signedPreKeyRecord",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "removed",
|
||||||
|
"columnName": "removed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_axolotl_signed_pre_key_accountId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_axolotl_signed_pre_key_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tableName": "blocked",
|
"tableName": "blocked",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
@ -179,6 +807,85 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bookmark",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `name` TEXT, `nick` TEXT, `autoJoin` INTEGER NOT NULL, `password` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "nick",
|
||||||
|
"columnName": "nick",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autoJoin",
|
||||||
|
"columnName": "autoJoin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bookmark_accountId_address",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmark_accountId_address` ON `${TABLE_NAME}` (`accountId`, `address`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tableName": "chat",
|
"tableName": "chat",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `type` TEXT, `archived` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `type` TEXT, `archived` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
@ -981,6 +1688,67 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tableName": "nick",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `nick` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "nick",
|
||||||
|
"columnName": "nick",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_nick_accountId_address",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"accountId",
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nick_accountId_address` ON `${TABLE_NAME}` (`accountId`, `address`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "account",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tableName": "presence",
|
"tableName": "presence",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `resource` TEXT NOT NULL, `type` TEXT, `show` TEXT, `status` TEXT, `vCardPhoto` TEXT, `occupantId` TEXT, `mucUserAffiliation` TEXT, `mucUserRole` TEXT, `mucUserJid` TEXT, `mucUserSelf` INTEGER NOT NULL, `discoId` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`discoId`) REFERENCES `disco`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `resource` TEXT NOT NULL, `type` TEXT, `show` TEXT, `status` TEXT, `vCardPhoto` TEXT, `occupantId` TEXT, `mucUserAffiliation` TEXT, `mucUserRole` TEXT, `mucUserJid` TEXT, `mucUserSelf` INTEGER NOT NULL, `discoId` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`discoId`) REFERENCES `disco`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
@ -1344,7 +2112,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2dbbac3327bc8ef188286642b379e7d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2972255ca35c75ece48909471313d20a')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,6 +5,7 @@ import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
import eu.siacs.conversations.utils.XmlHelper;
|
import eu.siacs.conversations.utils.XmlHelper;
|
||||||
import eu.siacs.conversations.xmpp.InvalidJid;
|
import eu.siacs.conversations.xmpp.InvalidJid;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
@ -189,6 +190,11 @@ public class Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLongAttribute(final String name) {
|
||||||
|
final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name)));
|
||||||
|
return value == null ? 0 : value;
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<Integer> getOptionalIntAttribute(final String name) {
|
public Optional<Integer> getOptionalIntAttribute(final String name) {
|
||||||
final String value = getAttribute(name);
|
final String value = getAttribute(name);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package eu.siacs.conversations.xml;
|
package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
public final class Namespace {
|
public final class Namespace {
|
||||||
|
|
||||||
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
|
public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
|
||||||
public static final String AVATAR_DATA = "urn:xmpp:avatar:data";
|
public static final String AVATAR_DATA = "urn:xmpp:avatar:data";
|
||||||
public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata";
|
public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata";
|
||||||
|
public static final String AXOLOTL = "eu.siacs.conversations.axolotl";
|
||||||
|
public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles";
|
||||||
|
public static final String AXOLOTL_DEVICE_LIST = AXOLOTL + ".devicelist";
|
||||||
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
|
public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
|
||||||
public static final String BIND2 = "urn:xmpp:bind:0";
|
public static final String BIND2 = "urn:xmpp:bind:0";
|
||||||
public static final String BLOCKING = "urn:xmpp:blocking";
|
public static final String BLOCKING = "urn:xmpp:blocking";
|
||||||
public static final String BOOKMARKS = "storage:bookmarks";
|
public static final String BOOKMARKS = "storage:bookmarks";
|
||||||
public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:0";
|
public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1";
|
||||||
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
|
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
|
||||||
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
|
||||||
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
|
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
|
||||||
|
@ -70,9 +74,15 @@ public final class Namespace {
|
||||||
public static final String PARS = "urn:xmpp:pars:0";
|
public static final String PARS = "urn:xmpp:pars:0";
|
||||||
public static final String PING = "urn:xmpp:ping";
|
public static final String PING = "urn:xmpp:ping";
|
||||||
public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
|
public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
|
||||||
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
|
|
||||||
public static final String PUBSUB_OWNER = PUBSUB + "#owner";
|
public static final String PUBSUB_OWNER = PUBSUB + "#owner";
|
||||||
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
|
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
|
||||||
|
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
|
||||||
|
public static final String PUB_SUB = "http://jabber.org/protocol/pubsub";
|
||||||
|
public static final String PUB_SUB_ERROR = PUB_SUB + "#errors";
|
||||||
|
public static final String PUB_SUB_EVENT = PUB_SUB + "#event";
|
||||||
|
public static final String PUB_SUB_OWNER = PUB_SUB + "#owner";
|
||||||
|
public static final String PUB_SUB_PERSISTENT_ITEMS = PUB_SUB + "#persistent-items";
|
||||||
|
public static final String PUB_SUB_PUBLISH_OPTIONS = PUB_SUB + "#publish-options";
|
||||||
public static final String PUSH = "urn:xmpp:push:0";
|
public static final String PUSH = "urn:xmpp:push:0";
|
||||||
public static final String REGISTER = "jabber:iq:register";
|
public static final String REGISTER = "jabber:iq:register";
|
||||||
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
|
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
package im.conversations.android.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import im.conversations.android.database.dao.AxolotlDao;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.xmpp.axolotl.AxolotlAddress;
|
||||||
|
import java.util.List;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||||
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SessionRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
|
||||||
|
public class AxolotlDatabaseStore implements SignalProtocolStore {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Account account;
|
||||||
|
|
||||||
|
public AxolotlDatabaseStore(final Context context, final Account account) {
|
||||||
|
this.context = context;
|
||||||
|
this.account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AxolotlDao axolotlDao() {
|
||||||
|
return ConversationsDatabase.getInstance(context).axolotlDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityKeyPair getIdentityKeyPair() {
|
||||||
|
return axolotlDao().getOrCreateIdentityKeyPair(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLocalRegistrationId() {
|
||||||
|
return account.getPublicDeviceIdInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean saveIdentity(
|
||||||
|
final SignalProtocolAddress signalProtocolAddress, IdentityKey identityKey) {
|
||||||
|
final var address = AxolotlAddress.cast(signalProtocolAddress);
|
||||||
|
return axolotlDao()
|
||||||
|
.setIdentity(account, address.getJid(), address.getDeviceId(), identityKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTrustedIdentity(
|
||||||
|
SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||||
|
// TODO return false for Direction==Sending and Trust == untrusted
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||||
|
final var preKey = axolotlDao().getPreKey(account.id, preKeyId);
|
||||||
|
if (preKey == null) {
|
||||||
|
throw new InvalidKeyIdException(String.format("PreKey %d does not exist", preKeyId));
|
||||||
|
}
|
||||||
|
return preKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storePreKey(int preKeyId, PreKeyRecord preKeyRecord) {
|
||||||
|
axolotlDao().setPreKey(account, preKeyId, preKeyRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsPreKey(int preKeyId) {
|
||||||
|
return axolotlDao().hasPreKey(account.id, preKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removePreKey(int preKeyId) {
|
||||||
|
axolotlDao().markPreKeyAsRemoved(account.id, preKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionRecord loadSession(final SignalProtocolAddress signalProtocolAddress) {
|
||||||
|
final var address = AxolotlAddress.cast(signalProtocolAddress);
|
||||||
|
final var sessionRecord =
|
||||||
|
axolotlDao().getSessionRecord(account.id, address.getJid(), address.getDeviceId());
|
||||||
|
return sessionRecord == null ? new SessionRecord() : sessionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getSubDeviceSessions(String name) {
|
||||||
|
return axolotlDao().getSessionDeviceIds(account.id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeSession(SignalProtocolAddress signalProtocolAddress, SessionRecord record) {
|
||||||
|
final var address = AxolotlAddress.cast(signalProtocolAddress);
|
||||||
|
axolotlDao().setSessionRecord(account, address.getJid(), address.getDeviceId(), record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsSession(SignalProtocolAddress signalProtocolAddress) {
|
||||||
|
final var address = AxolotlAddress.cast(signalProtocolAddress);
|
||||||
|
return axolotlDao().hasSession(account.id, address.getJid(), address.getDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteSession(SignalProtocolAddress signalProtocolAddress) {
|
||||||
|
final var address = AxolotlAddress.cast(signalProtocolAddress);
|
||||||
|
axolotlDao().deleteSession(account.id, address.getJid(), address.getDeviceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteAllSessions(String name) {
|
||||||
|
axolotlDao().deleteSessions(account.id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
|
||||||
|
final var signedPreKeyRecord = axolotlDao().getSignedPreKey(account.id, signedPreKeyId);
|
||||||
|
if (signedPreKeyRecord == null) {
|
||||||
|
throw new InvalidKeyIdException(
|
||||||
|
String.format("signedPreKey %d not found", signedPreKeyId));
|
||||||
|
}
|
||||||
|
return signedPreKeyRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SignedPreKeyRecord> loadSignedPreKeys() {
|
||||||
|
return axolotlDao().getSignedPreKeys(account.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
|
||||||
|
axolotlDao().setSignedPreKey(account, signedPreKeyId, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsSignedPreKey(int signedPreKeyId) {
|
||||||
|
return axolotlDao().hasSignedPreKey(account.id, signedPreKeyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeSignedPreKey(int signedPreKeyId) {
|
||||||
|
axolotlDao().markSignedPreKeyAsRemoved(account.id, signedPreKeyId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,27 @@ import androidx.room.Room;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
import im.conversations.android.database.dao.AccountDao;
|
import im.conversations.android.database.dao.AccountDao;
|
||||||
|
import im.conversations.android.database.dao.AvatarDao;
|
||||||
|
import im.conversations.android.database.dao.AxolotlDao;
|
||||||
import im.conversations.android.database.dao.BlockingDao;
|
import im.conversations.android.database.dao.BlockingDao;
|
||||||
|
import im.conversations.android.database.dao.BookmarkDao;
|
||||||
import im.conversations.android.database.dao.DiscoDao;
|
import im.conversations.android.database.dao.DiscoDao;
|
||||||
import im.conversations.android.database.dao.MessageDao;
|
import im.conversations.android.database.dao.MessageDao;
|
||||||
|
import im.conversations.android.database.dao.NickDao;
|
||||||
import im.conversations.android.database.dao.PresenceDao;
|
import im.conversations.android.database.dao.PresenceDao;
|
||||||
import im.conversations.android.database.dao.RosterDao;
|
import im.conversations.android.database.dao.RosterDao;
|
||||||
import im.conversations.android.database.entity.AccountEntity;
|
import im.conversations.android.database.entity.AccountEntity;
|
||||||
|
import im.conversations.android.database.entity.AvatarAdditionalEntity;
|
||||||
|
import im.conversations.android.database.entity.AvatarEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlDeviceListEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlDeviceListItemEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlIdentityEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlIdentityKeyPairEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlPreKeyEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlSessionEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlSignedPreKeyEntity;
|
||||||
import im.conversations.android.database.entity.BlockedItemEntity;
|
import im.conversations.android.database.entity.BlockedItemEntity;
|
||||||
|
import im.conversations.android.database.entity.BookmarkEntity;
|
||||||
import im.conversations.android.database.entity.ChatEntity;
|
import im.conversations.android.database.entity.ChatEntity;
|
||||||
import im.conversations.android.database.entity.DiscoEntity;
|
import im.conversations.android.database.entity.DiscoEntity;
|
||||||
import im.conversations.android.database.entity.DiscoExtensionEntity;
|
import im.conversations.android.database.entity.DiscoExtensionEntity;
|
||||||
|
@ -24,6 +38,7 @@ import im.conversations.android.database.entity.DiscoItemEntity;
|
||||||
import im.conversations.android.database.entity.MessageEntity;
|
import im.conversations.android.database.entity.MessageEntity;
|
||||||
import im.conversations.android.database.entity.MessagePartEntity;
|
import im.conversations.android.database.entity.MessagePartEntity;
|
||||||
import im.conversations.android.database.entity.MessageVersionEntity;
|
import im.conversations.android.database.entity.MessageVersionEntity;
|
||||||
|
import im.conversations.android.database.entity.NickEntity;
|
||||||
import im.conversations.android.database.entity.PresenceEntity;
|
import im.conversations.android.database.entity.PresenceEntity;
|
||||||
import im.conversations.android.database.entity.ReactionEntity;
|
import im.conversations.android.database.entity.ReactionEntity;
|
||||||
import im.conversations.android.database.entity.RosterItemEntity;
|
import im.conversations.android.database.entity.RosterItemEntity;
|
||||||
|
@ -32,7 +47,17 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
|
||||||
@Database(
|
@Database(
|
||||||
entities = {
|
entities = {
|
||||||
AccountEntity.class,
|
AccountEntity.class,
|
||||||
|
AvatarAdditionalEntity.class,
|
||||||
|
AvatarEntity.class,
|
||||||
|
AxolotlDeviceListEntity.class,
|
||||||
|
AxolotlDeviceListItemEntity.class,
|
||||||
|
AxolotlIdentityEntity.class,
|
||||||
|
AxolotlIdentityKeyPairEntity.class,
|
||||||
|
AxolotlPreKeyEntity.class,
|
||||||
|
AxolotlSessionEntity.class,
|
||||||
|
AxolotlSignedPreKeyEntity.class,
|
||||||
BlockedItemEntity.class,
|
BlockedItemEntity.class,
|
||||||
|
BookmarkEntity.class,
|
||||||
ChatEntity.class,
|
ChatEntity.class,
|
||||||
DiscoEntity.class,
|
DiscoEntity.class,
|
||||||
DiscoExtensionEntity.class,
|
DiscoExtensionEntity.class,
|
||||||
|
@ -44,6 +69,7 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
|
||||||
MessageEntity.class,
|
MessageEntity.class,
|
||||||
MessagePartEntity.class,
|
MessagePartEntity.class,
|
||||||
MessageVersionEntity.class,
|
MessageVersionEntity.class,
|
||||||
|
NickEntity.class,
|
||||||
PresenceEntity.class,
|
PresenceEntity.class,
|
||||||
ReactionEntity.class,
|
ReactionEntity.class,
|
||||||
RosterItemEntity.class,
|
RosterItemEntity.class,
|
||||||
|
@ -73,12 +99,20 @@ public abstract class ConversationsDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
|
||||||
|
public abstract AvatarDao avatarDao();
|
||||||
|
|
||||||
|
public abstract AxolotlDao axolotlDao();
|
||||||
|
|
||||||
public abstract BlockingDao blockingDao();
|
public abstract BlockingDao blockingDao();
|
||||||
|
|
||||||
|
public abstract BookmarkDao bookmarkDao();
|
||||||
|
|
||||||
public abstract DiscoDao discoDao();
|
public abstract DiscoDao discoDao();
|
||||||
|
|
||||||
public abstract MessageDao messageDao();
|
public abstract MessageDao messageDao();
|
||||||
|
|
||||||
|
public abstract NickDao nickDao();
|
||||||
|
|
||||||
public abstract PresenceDao presenceDao();
|
public abstract PresenceDao presenceDao();
|
||||||
|
|
||||||
public abstract RosterDao rosterDao();
|
public abstract RosterDao rosterDao();
|
||||||
|
|
|
@ -2,7 +2,14 @@ package im.conversations.android.database;
|
||||||
|
|
||||||
import androidx.room.TypeConverter;
|
import androidx.room.TypeConverter;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SessionRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
|
||||||
public final class Converters {
|
public final class Converters {
|
||||||
|
|
||||||
|
@ -27,4 +34,89 @@ public final class Converters {
|
||||||
public static String fromJid(final Jid jid) {
|
public static String fromJid(final Jid jid) {
|
||||||
return jid == null ? null : jid.toEscapedString();
|
return jid == null ? null : jid.toEscapedString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] fromIdentityKey(final IdentityKey identityKey) {
|
||||||
|
return identityKey == null ? null : identityKey.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static IdentityKey toIdentityKey(final byte[] serialized) {
|
||||||
|
if (serialized == null || serialized.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new IdentityKey(serialized, 0);
|
||||||
|
} catch (final InvalidKeyException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] fromSessionRecord(final SessionRecord sessionRecord) {
|
||||||
|
return sessionRecord == null ? null : sessionRecord.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static SessionRecord toSessionRecord(final byte[] serialized) {
|
||||||
|
if (serialized == null || serialized.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new SessionRecord(serialized);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] fromSignedPreKey(final SignedPreKeyRecord signedPreKeyRecord) {
|
||||||
|
return signedPreKeyRecord == null ? null : signedPreKeyRecord.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static SignedPreKeyRecord toSignedPreKey(final byte[] serialized) {
|
||||||
|
if (serialized == null || serialized.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new SignedPreKeyRecord(serialized);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] fromPreKey(final PreKeyRecord preKeyRecord) {
|
||||||
|
return preKeyRecord == null ? null : preKeyRecord.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static PreKeyRecord toPreKey(final byte[] serialized) {
|
||||||
|
if (serialized == null || serialized.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new PreKeyRecord(serialized);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] fromIdentityKeyPair(final IdentityKeyPair identityKeyPair) {
|
||||||
|
return identityKeyPair == null ? null : identityKeyPair.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static IdentityKeyPair toIdentityKeyPair(final byte[] serialized) {
|
||||||
|
if (serialized == null || serialized.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new IdentityKeyPair(serialized);
|
||||||
|
} catch (final InvalidKeyException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.entity.AvatarAdditionalEntity;
|
||||||
|
import im.conversations.android.database.entity.AvatarEntity;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class AvatarDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract long insert(AvatarEntity avatar);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract void insert(Collection<AvatarAdditionalEntity> entities);
|
||||||
|
|
||||||
|
public void set(
|
||||||
|
final Account account,
|
||||||
|
final Jid address,
|
||||||
|
final Info thumbnail,
|
||||||
|
final Collection<Info> additional) {
|
||||||
|
final long id = insert(AvatarEntity.of(account, address, thumbnail));
|
||||||
|
insert(Collections2.transform(additional, a -> AvatarAdditionalEntity.of(id, a)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.entity.AxolotlDeviceListEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlDeviceListItemEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlIdentityEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlIdentityKeyPairEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlPreKeyEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlSessionEntity;
|
||||||
|
import im.conversations.android.database.entity.AxolotlSignedPreKeyEntity;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.xmpp.model.error.Condition;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
import org.whispersystems.libsignal.ecc.Curve;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SessionRecord;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class AxolotlDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract long insert(AxolotlDeviceListEntity entity);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract void insert(Collection<AxolotlDeviceListItemEntity> entities);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public void setDeviceList(Account account, Jid from, Set<Integer> deviceIds) {
|
||||||
|
final var listId = insert(AxolotlDeviceListEntity.of(account.id, from));
|
||||||
|
insert(
|
||||||
|
Collections2.transform(
|
||||||
|
deviceIds, deviceId -> AxolotlDeviceListItemEntity.of(listId, deviceId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT EXISTS(SELECT deviceId FROM axolotl_device_list JOIN axolotl_device_list_item"
|
||||||
|
+ " ON axolotl_device_list.id=axolotl_device_list_item.deviceId WHERE"
|
||||||
|
+ " accountId=:account AND address=:address AND deviceId=:deviceId)")
|
||||||
|
public abstract boolean hasDeviceId(final long account, final Jid address, final int deviceId);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public void setDeviceListError(final Account account, final Jid address, Condition condition) {
|
||||||
|
insert(AxolotlDeviceListEntity.of(account.id, address, condition.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public void setDeviceListParsingError(final Account account, final Jid address) {
|
||||||
|
insert(AxolotlDeviceListEntity.ofParsingIssue(account.id, address));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public IdentityKeyPair getOrCreateIdentityKeyPair(final Account account) {
|
||||||
|
final var existing = getIdentityKeyPair(account.id);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
final var ecKeyPair = Curve.generateKeyPair();
|
||||||
|
final var identityKeyPair =
|
||||||
|
new IdentityKeyPair(
|
||||||
|
new IdentityKey(ecKeyPair.getPublicKey()), ecKeyPair.getPrivateKey());
|
||||||
|
insert(AxolotlIdentityKeyPairEntity.of(account, identityKeyPair));
|
||||||
|
return identityKeyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract void insert(AxolotlIdentityKeyPairEntity entity);
|
||||||
|
|
||||||
|
@Query("SELECT identityKeyPair FROM axolotl_identity_key_pair WHERE accountId=:account")
|
||||||
|
protected abstract IdentityKeyPair getIdentityKeyPair(long account);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT signedPreKeyRecord FROM axolotl_signed_pre_key WHERE accountId=:account AND"
|
||||||
|
+ " signedPreKeyId=:signedPreKeyId")
|
||||||
|
public abstract SignedPreKeyRecord getSignedPreKey(long account, int signedPreKeyId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT NOT EXISTS(SELECT signedPreKeyRecord FROM axolotl_signed_pre_key WHERE"
|
||||||
|
+ " accountId=:account AND signedPreKeyId=:signedPreKeyId)")
|
||||||
|
public abstract boolean hasNotSignedPreKey(long account, int signedPreKeyId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT signedPreKeyRecord FROM axolotl_signed_pre_key WHERE accountId=:account ORDER"
|
||||||
|
+ " BY signedPreKeyId DESC LIMIT 1")
|
||||||
|
public abstract SignedPreKeyRecord getLatestSignedPreKey(long account);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public boolean setIdentity(
|
||||||
|
Account account, Jid address, int deviceId, IdentityKey identityKey) {
|
||||||
|
final var existing = getIdentityKey(account.id, address, deviceId);
|
||||||
|
if (existing == null || !existing.equals(identityKey)) {
|
||||||
|
insert(AxolotlIdentityEntity.of(account, address, deviceId, identityKey));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract void insert(AxolotlIdentityEntity axolotlIdentityEntity);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT identityKey FROM AXOLOTL_IDENTITY WHERE accountId=:account AND"
|
||||||
|
+ " address=:address AND deviceId=:deviceId")
|
||||||
|
protected abstract IdentityKey getIdentityKey(long account, Jid address, int deviceId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT preKeyRecord FROM axolotl_pre_key WHERE accountId=:account AND"
|
||||||
|
+ " preKeyid=:preKeyId")
|
||||||
|
public abstract PreKeyRecord getPreKey(long account, int preKeyId);
|
||||||
|
|
||||||
|
@Query("SELECT MAX(preKeyId) FROM axolotl_pre_key WHERE accountId=:account")
|
||||||
|
public abstract Integer getMaxPreKeyId(final long account);
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(id) FROM axolotl_pre_key WHERE accountId=:account AND removed=0")
|
||||||
|
public abstract int getExistingPreKeyCount(final long account);
|
||||||
|
|
||||||
|
public void setPreKey(final Account account, int preKeyId, PreKeyRecord preKeyRecord) {
|
||||||
|
insert(AxolotlPreKeyEntity.of(account, preKeyId, preKeyRecord));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract void insert(AxolotlPreKeyEntity axolotlPreKeyEntity);
|
||||||
|
|
||||||
|
public void setPreKeys(final Account account, final Collection<PreKeyRecord> preKeyRecords) {
|
||||||
|
insertPreKeys(
|
||||||
|
Collections2.transform(
|
||||||
|
preKeyRecords, r -> AxolotlPreKeyEntity.of(account, r.getId(), r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract void insertPreKeys(Collection<AxolotlPreKeyEntity> entities);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract void insert(AxolotlSessionEntity axolotlSessionEntity);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT EXISTS(SELECT id FROM axolotl_pre_key WHERE accountId=:account AND"
|
||||||
|
+ " preKeyId=:preKeyId)")
|
||||||
|
public abstract boolean hasPreKey(long account, int preKeyId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT EXISTS(SELECT id FROM axolotl_signed_pre_key WHERE accountId=:account AND"
|
||||||
|
+ " signedPreKeyId=:signedPreKeyId)")
|
||||||
|
public abstract boolean hasSignedPreKey(long account, int signedPreKeyId);
|
||||||
|
|
||||||
|
@Query("UPDATE axolotl_pre_key SET removed=1 WHERE accountId=:account AND preKeyId=:preKeyId")
|
||||||
|
public abstract void markPreKeyAsRemoved(long account, int preKeyId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"UPDATE axolotl_signed_pre_key SET removed=1 WHERE accountId=:account AND"
|
||||||
|
+ " signedPreKeyId=:signedPreKeyId")
|
||||||
|
public abstract void markSignedPreKeyAsRemoved(long account, int signedPreKeyId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT sessionRecord FROM axolotl_session WHERE accountId=:account AND"
|
||||||
|
+ " address=:address AND deviceId=:deviceId")
|
||||||
|
public abstract SessionRecord getSessionRecord(long account, Jid address, int deviceId);
|
||||||
|
|
||||||
|
@Query("SELECT deviceId FROM axolotl_session WHERE accountId=:account AND address=:address")
|
||||||
|
public abstract List<Integer> getSessionDeviceIds(long account, String address);
|
||||||
|
|
||||||
|
public void setSessionRecord(Account account, Jid address, int deviceId, SessionRecord record) {
|
||||||
|
insert(AxolotlSessionEntity.of(account, address, deviceId, record));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT EXISTS(SELECT id FROM axolotl_session WHERE accountId=:account AND"
|
||||||
|
+ " address=:address AND deviceId=:deviceId)")
|
||||||
|
public abstract boolean hasSession(long account, Jid address, int deviceId);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"DELETE FROM axolotl_session WHERE accountId=:account AND address=:address AND"
|
||||||
|
+ " deviceId=:deviceId")
|
||||||
|
public abstract void deleteSession(long account, Jid address, int deviceId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM axolotl_session WHERE accountId=:account AND address=:address")
|
||||||
|
public abstract void deleteSessions(long account, String address);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT signedPreKeyRecord FROM axolotl_signed_pre_key WHERE accountId=:account AND"
|
||||||
|
+ " removed=0")
|
||||||
|
public abstract List<SignedPreKeyRecord> getSignedPreKeys(long account);
|
||||||
|
|
||||||
|
@Query("SELECT preKeyRecord FROM axolotl_pre_key WHERE accountId=:account AND removed=0")
|
||||||
|
public abstract List<PreKeyRecord> getPreKeys(long account);
|
||||||
|
|
||||||
|
public void setSignedPreKey(Account account, int signedPreKeyId, SignedPreKeyRecord record) {
|
||||||
|
insert(AxolotlSignedPreKeyEntity.of(account, signedPreKeyId, record));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract void insert(AxolotlSignedPreKeyEntity signedPreKeyEntity);
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Transaction;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.entity.BookmarkEntity;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.xmpp.model.bookmark.Conference;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class BookmarkDao {
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmark WHERE accountId=:account")
|
||||||
|
public abstract void deleteAll(final long account);
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmark WHERE accountId=:account and address IN(:addresses)")
|
||||||
|
public abstract void delete(final long account, Collection<Jid> addresses);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract void insert(Collection<BookmarkEntity> bookmarks);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public void updateItems(final Account account, Map<String, Conference> items) {
|
||||||
|
final Collection<Jid> addresses =
|
||||||
|
Collections2.transform(items.keySet(), BookmarkEntity::jidOrNull);
|
||||||
|
delete(account.id, addresses);
|
||||||
|
final var entities =
|
||||||
|
Collections2.transform(
|
||||||
|
items.entrySet(), entry -> BookmarkEntity.of(account.id, entry));
|
||||||
|
// non null filtering is required because BookmarkEntity.of() can return null values if the
|
||||||
|
insert(Collections2.filter(entities, Objects::nonNull));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public void setItems(Account account, Map<String, Conference> items) {
|
||||||
|
deleteAll(account.id);
|
||||||
|
final var entities =
|
||||||
|
Collections2.transform(
|
||||||
|
items.entrySet(), entry -> BookmarkEntity.of(account.id, entry));
|
||||||
|
// non null filtering is required because BookmarkEntity.of() can return null values if the
|
||||||
|
insert(Collections2.filter(entities, Objects::nonNull));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.entity.NickEntity;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class NickDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
protected abstract long insert(NickEntity nickEntity);
|
||||||
|
|
||||||
|
public long set(final Account account, final Jid address, final String nick) {
|
||||||
|
return insert(NickEntity.of(account.id, address, nick));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Embedded;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import im.conversations.android.database.model.AvatarExternal;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "avatar_additional",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AvatarEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"avatarId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {@Index(value = {"avatarId"})})
|
||||||
|
public class AvatarAdditionalEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long avatarId;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Embedded(prefix = "avatar_external_")
|
||||||
|
public AvatarExternal external;
|
||||||
|
|
||||||
|
public static AvatarAdditionalEntity of(final long avatarId, Info info) {
|
||||||
|
final var entity = new AvatarAdditionalEntity();
|
||||||
|
entity.avatarId = avatarId;
|
||||||
|
entity.external = AvatarExternal.of(info);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Embedded;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.AvatarThumbnail;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "avatar",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AvatarEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
@Embedded(prefix = "thumb_")
|
||||||
|
@NonNull
|
||||||
|
public AvatarThumbnail thumbnail;
|
||||||
|
|
||||||
|
public static AvatarEntity of(final Account account, final Jid address, final Info info) {
|
||||||
|
final var entity = new AvatarEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.address = address;
|
||||||
|
entity.thumbnail = AvatarThumbnail.of(info);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_device_list",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AxolotlDeviceListEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
@NonNull public Instant receivedAt;
|
||||||
|
|
||||||
|
public String errorCondition;
|
||||||
|
|
||||||
|
public boolean isParsingIssue;
|
||||||
|
|
||||||
|
public static AxolotlDeviceListEntity of(long accountId, final Jid address) {
|
||||||
|
final var entity = new AxolotlDeviceListEntity();
|
||||||
|
entity.accountId = accountId;
|
||||||
|
entity.address = address;
|
||||||
|
entity.receivedAt = Instant.now();
|
||||||
|
entity.isParsingIssue = false;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AxolotlDeviceListEntity of(
|
||||||
|
final long accountId, final Jid address, final String errorCondition) {
|
||||||
|
final var entity = new AxolotlDeviceListEntity();
|
||||||
|
entity.accountId = accountId;
|
||||||
|
entity.address = address;
|
||||||
|
entity.receivedAt = Instant.now();
|
||||||
|
entity.errorCondition = errorCondition;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AxolotlDeviceListEntity ofParsingIssue(final long account, Jid address) {
|
||||||
|
final var entity = new AxolotlDeviceListEntity();
|
||||||
|
entity.accountId = account;
|
||||||
|
entity.address = address;
|
||||||
|
entity.receivedAt = Instant.now();
|
||||||
|
entity.isParsingIssue = true;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_device_list_item",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AxolotlDeviceListEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"deviceListId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(value = {"deviceListId"}),
|
||||||
|
@Index(
|
||||||
|
value = {"deviceListId", "deviceId"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AxolotlDeviceListItemEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long deviceListId;
|
||||||
|
|
||||||
|
public Integer deviceId;
|
||||||
|
|
||||||
|
public static AxolotlDeviceListItemEntity of(final long deviceListId, final int deviceId) {
|
||||||
|
final var entity = new AxolotlDeviceListItemEntity();
|
||||||
|
entity.deviceListId = deviceListId;
|
||||||
|
entity.deviceId = deviceId;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_identity",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address", "deviceId"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AxolotlIdentityEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
@NonNull public Integer deviceId;
|
||||||
|
|
||||||
|
@NonNull public IdentityKey identityKey;
|
||||||
|
|
||||||
|
public static AxolotlIdentityEntity of(
|
||||||
|
Account account, Jid address, int deviceId, IdentityKey identityKey) {
|
||||||
|
final var entity = new AxolotlIdentityEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.address = address;
|
||||||
|
entity.deviceId = deviceId;
|
||||||
|
entity.identityKey = identityKey;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_identity_key_pair",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AxolotlIdentityKeyPairEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public IdentityKeyPair identityKeyPair;
|
||||||
|
|
||||||
|
public static AxolotlIdentityKeyPairEntity of(
|
||||||
|
final Account account, final IdentityKeyPair identityKeyPair) {
|
||||||
|
final var entity = new AxolotlIdentityKeyPairEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.identityKeyPair = identityKeyPair;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_pre_key",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId"},
|
||||||
|
unique = false)
|
||||||
|
})
|
||||||
|
public class AxolotlPreKeyEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Integer preKeyId;
|
||||||
|
|
||||||
|
@NonNull public PreKeyRecord preKeyRecord;
|
||||||
|
|
||||||
|
public boolean removed = false;
|
||||||
|
|
||||||
|
public static AxolotlPreKeyEntity of(Account account, int preKeyId, PreKeyRecord preKeyRecord) {
|
||||||
|
final var entity = new AxolotlPreKeyEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.preKeyId = preKeyId;
|
||||||
|
entity.preKeyRecord = preKeyRecord;
|
||||||
|
entity.removed = false;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import org.whispersystems.libsignal.state.SessionRecord;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_session",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address", "deviceId"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class AxolotlSessionEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
@NonNull public Integer deviceId;
|
||||||
|
|
||||||
|
@NonNull public SessionRecord sessionRecord;
|
||||||
|
|
||||||
|
public static AxolotlSessionEntity of(
|
||||||
|
Account account, Jid address, int deviceId, SessionRecord record) {
|
||||||
|
final var entity = new AxolotlSessionEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.address = address;
|
||||||
|
entity.deviceId = deviceId;
|
||||||
|
entity.sessionRecord = record;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "axolotl_signed_pre_key",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId"},
|
||||||
|
unique = false)
|
||||||
|
})
|
||||||
|
public class AxolotlSignedPreKeyEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Integer signedPreKeyId;
|
||||||
|
|
||||||
|
@NonNull public SignedPreKeyRecord signedPreKeyRecord;
|
||||||
|
|
||||||
|
public boolean removed = false;
|
||||||
|
|
||||||
|
public static AxolotlSignedPreKeyEntity of(
|
||||||
|
Account account, int signedPreKeyId, SignedPreKeyRecord record) {
|
||||||
|
final var entity = new AxolotlSignedPreKeyEntity();
|
||||||
|
entity.accountId = account.id;
|
||||||
|
entity.signedPreKeyId = signedPreKeyId;
|
||||||
|
entity.signedPreKeyRecord = record;
|
||||||
|
entity.removed = false;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.xmpp.model.bookmark.Conference;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "bookmark",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class BookmarkEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
public String nick;
|
||||||
|
|
||||||
|
public boolean autoJoin;
|
||||||
|
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
public static BookmarkEntity of(
|
||||||
|
final long accountId, final Map.Entry<String, Conference> entry) {
|
||||||
|
final var address = jidOrNull(entry.getKey());
|
||||||
|
final var conference = entry.getValue();
|
||||||
|
if (address == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var entity = new BookmarkEntity();
|
||||||
|
entity.accountId = accountId;
|
||||||
|
entity.address = address;
|
||||||
|
entity.autoJoin = conference.isAutoJoin();
|
||||||
|
entity.name = conference.getConferenceName();
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Jid jidOrNull(final String address) {
|
||||||
|
try {
|
||||||
|
return address == null ? null : Jid.ofEscaped(address);
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package im.conversations.android.database.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.ForeignKey;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "nick",
|
||||||
|
foreignKeys =
|
||||||
|
@ForeignKey(
|
||||||
|
entity = AccountEntity.class,
|
||||||
|
parentColumns = {"id"},
|
||||||
|
childColumns = {"accountId"},
|
||||||
|
onDelete = ForeignKey.CASCADE),
|
||||||
|
indices = {
|
||||||
|
@Index(
|
||||||
|
value = {"accountId", "address"},
|
||||||
|
unique = true)
|
||||||
|
})
|
||||||
|
public class NickEntity {
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public Long id;
|
||||||
|
|
||||||
|
@NonNull public Long accountId;
|
||||||
|
|
||||||
|
@NonNull public Jid address;
|
||||||
|
|
||||||
|
public String nick;
|
||||||
|
|
||||||
|
public static NickEntity of(long account, Jid address, String nick) {
|
||||||
|
final var entity = new NickEntity();
|
||||||
|
entity.accountId = account;
|
||||||
|
entity.address = address;
|
||||||
|
entity.nick = nick;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.hash.Hashing;
|
import com.google.common.hash.Hashing;
|
||||||
import com.google.common.io.ByteSource;
|
import com.google.common.io.ByteSource;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import im.conversations.android.IDs;
|
import im.conversations.android.IDs;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -51,7 +52,15 @@ public class Account {
|
||||||
return IDs.uuid(
|
return IDs.uuid(
|
||||||
ByteSource.wrap(randomSeed).slice(0, 16).hash(Hashing.sha256()).asBytes());
|
ByteSource.wrap(randomSeed).slice(0, 16).hash(Hashing.sha256()).asBytes());
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
return UUID.randomUUID();
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPublicDeviceIdInt() {
|
||||||
|
try {
|
||||||
|
return Math.abs(Ints.fromByteArray(ByteSource.wrap(randomSeed).slice(0, 4).read()));
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
public abstract class AvatarBase {
|
||||||
|
|
||||||
|
public final String id;
|
||||||
|
public final String type;
|
||||||
|
public final long bytes;
|
||||||
|
public final long height;
|
||||||
|
public final long width;
|
||||||
|
|
||||||
|
public AvatarBase(String id, String type, long bytes, long height, long width) {
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
this.bytes = bytes;
|
||||||
|
this.height = height;
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
|
||||||
|
public class AvatarExternal extends AvatarBase {
|
||||||
|
|
||||||
|
public final String url;
|
||||||
|
|
||||||
|
public AvatarExternal(String id, String type, long bytes, long height, long width, String url) {
|
||||||
|
super(id, type, bytes, height, width);
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AvatarExternal of(Info info) {
|
||||||
|
return new AvatarExternal(
|
||||||
|
info.getId(),
|
||||||
|
info.getType(),
|
||||||
|
info.getBytes(),
|
||||||
|
info.getHeight(),
|
||||||
|
info.getWidth(),
|
||||||
|
info.getUrl());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
|
||||||
|
public class AvatarThumbnail extends AvatarBase {
|
||||||
|
|
||||||
|
public AvatarThumbnail(String id, String type, long bytes, long height, long width) {
|
||||||
|
super(id, type, bytes, height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AvatarThumbnail of(Info info) {
|
||||||
|
return new AvatarThumbnail(
|
||||||
|
info.getId(), info.getType(), info.getBytes(), info.getHeight(), info.getWidth());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import im.conversations.android.xmpp.model.error.Error;
|
||||||
|
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||||
|
|
||||||
|
public class IqErrorException extends Exception {
|
||||||
|
|
||||||
|
private final Iq response;
|
||||||
|
|
||||||
|
public IqErrorException(Iq response) {
|
||||||
|
super(getErrorText(response));
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error getError() {
|
||||||
|
return this.response.getError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getErrorText(final Iq response) {
|
||||||
|
final var error = response.getError();
|
||||||
|
final var text = error == null ? null : error.getText();
|
||||||
|
return text == null ? null : text.getContent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,15 @@ import android.content.Context;
|
||||||
import com.google.common.collect.ClassToInstanceMap;
|
import com.google.common.collect.ClassToInstanceMap;
|
||||||
import com.google.common.collect.ImmutableClassToInstanceMap;
|
import com.google.common.collect.ImmutableClassToInstanceMap;
|
||||||
import im.conversations.android.xmpp.manager.AbstractManager;
|
import im.conversations.android.xmpp.manager.AbstractManager;
|
||||||
|
import im.conversations.android.xmpp.manager.AvatarManager;
|
||||||
|
import im.conversations.android.xmpp.manager.AxolotlManager;
|
||||||
import im.conversations.android.xmpp.manager.BlockingManager;
|
import im.conversations.android.xmpp.manager.BlockingManager;
|
||||||
import im.conversations.android.xmpp.manager.BookmarkManager;
|
import im.conversations.android.xmpp.manager.BookmarkManager;
|
||||||
import im.conversations.android.xmpp.manager.CarbonsManager;
|
import im.conversations.android.xmpp.manager.CarbonsManager;
|
||||||
import im.conversations.android.xmpp.manager.DiscoManager;
|
import im.conversations.android.xmpp.manager.DiscoManager;
|
||||||
|
import im.conversations.android.xmpp.manager.NickManager;
|
||||||
import im.conversations.android.xmpp.manager.PresenceManager;
|
import im.conversations.android.xmpp.manager.PresenceManager;
|
||||||
|
import im.conversations.android.xmpp.manager.PubSubManager;
|
||||||
import im.conversations.android.xmpp.manager.RosterManager;
|
import im.conversations.android.xmpp.manager.RosterManager;
|
||||||
|
|
||||||
public final class Managers {
|
public final class Managers {
|
||||||
|
@ -18,11 +22,15 @@ public final class Managers {
|
||||||
public static ClassToInstanceMap<AbstractManager> initialize(
|
public static ClassToInstanceMap<AbstractManager> initialize(
|
||||||
final Context context, final XmppConnection connection) {
|
final Context context, final XmppConnection connection) {
|
||||||
return new ImmutableClassToInstanceMap.Builder<AbstractManager>()
|
return new ImmutableClassToInstanceMap.Builder<AbstractManager>()
|
||||||
|
.put(AvatarManager.class, new AvatarManager(context, connection))
|
||||||
|
.put(AxolotlManager.class, new AxolotlManager(context, connection))
|
||||||
.put(BlockingManager.class, new BlockingManager(context, connection))
|
.put(BlockingManager.class, new BlockingManager(context, connection))
|
||||||
.put(BookmarkManager.class, new BookmarkManager(context, connection))
|
.put(BookmarkManager.class, new BookmarkManager(context, connection))
|
||||||
.put(CarbonsManager.class, new CarbonsManager(context, connection))
|
.put(CarbonsManager.class, new CarbonsManager(context, connection))
|
||||||
.put(DiscoManager.class, new DiscoManager(context, connection))
|
.put(DiscoManager.class, new DiscoManager(context, connection))
|
||||||
|
.put(NickManager.class, new NickManager(context, connection))
|
||||||
.put(PresenceManager.class, new PresenceManager(context, connection))
|
.put(PresenceManager.class, new PresenceManager(context, connection))
|
||||||
|
.put(PubSubManager.class, new PubSubManager(context, connection))
|
||||||
.put(RosterManager.class, new RosterManager(context, connection))
|
.put(RosterManager.class, new RosterManager(context, connection))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2019,8 +2019,7 @@ public class XmppConnection implements Runnable {
|
||||||
} else if (type == Iq.Type.TIMEOUT) {
|
} else if (type == Iq.Type.TIMEOUT) {
|
||||||
future.setException(new TimeoutException());
|
future.setException(new TimeoutException());
|
||||||
} else {
|
} else {
|
||||||
// TODO some sort of IqErrorException
|
future.setException(new IqErrorException(result));
|
||||||
future.setException(new IOException());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return future;
|
return future;
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package im.conversations.android.xmpp.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
|
||||||
|
public class AxolotlAddress extends SignalProtocolAddress {
|
||||||
|
|
||||||
|
private final Jid jid;
|
||||||
|
|
||||||
|
public AxolotlAddress(final Jid jid, int deviceId) {
|
||||||
|
super(jid.toEscapedString(), deviceId);
|
||||||
|
Preconditions.checkArgument(jid.isBareJid(), "AxolotlAddresses must use bare JIDs");
|
||||||
|
this.jid = jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jid getJid() {
|
||||||
|
return this.jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AxolotlAddress cast(final SignalProtocolAddress signalProtocolAddress) {
|
||||||
|
if (signalProtocolAddress instanceof AxolotlAddress) {
|
||||||
|
return (AxolotlAddress) signalProtocolAddress;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"This %s is not a %s",
|
||||||
|
SignalProtocolAddress.class.getSimpleName(),
|
||||||
|
AxolotlAddress.class.getSimpleName()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,13 @@ package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class AbstractManager extends XmppConnection.Delegate {
|
public class AbstractManager extends XmppConnection.Delegate {
|
||||||
|
|
||||||
|
protected static final Executor IO_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
protected AbstractManager(final Context context, final XmppConnection connection) {
|
protected AbstractManager(final Context context, final XmppConnection connection) {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Data;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Info;
|
||||||
|
import im.conversations.android.xmpp.model.avatar.Metadata;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class AvatarManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AvatarManager.class);
|
||||||
|
|
||||||
|
private final Map<Fetch, ListenableFuture<byte[]>> avatarFetches = new HashMap<>();
|
||||||
|
|
||||||
|
public AvatarManager(Context context, XmppConnection connection) {
|
||||||
|
super(context, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleItems(final Jid from, final Items items) {
|
||||||
|
final var itemsMap = items.getItemMap(Metadata.class);
|
||||||
|
final var firstEntry = Iterables.getFirst(itemsMap.entrySet(), null);
|
||||||
|
if (firstEntry == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var itemId = firstEntry.getKey();
|
||||||
|
final var metadata = firstEntry.getValue();
|
||||||
|
final var info = metadata.getExtensions(Info.class);
|
||||||
|
final var thumbnailOptional =
|
||||||
|
Iterables.tryFind(info, i -> Objects.equals(itemId, i.getId()));
|
||||||
|
if (thumbnailOptional.isPresent()) {
|
||||||
|
final var thumbnail = thumbnailOptional.get();
|
||||||
|
if (thumbnail.getUrl() != null) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Thumbnail avatar from {} is hosted on remote URL. We require it to be"
|
||||||
|
+ " hosted on PEP",
|
||||||
|
from);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var additional =
|
||||||
|
Collections2.filter(
|
||||||
|
info,
|
||||||
|
i -> !Objects.equals(itemId, i.getId()) && Objects.nonNull(i.getUrl()));
|
||||||
|
getDatabase().avatarDao().set(getAccount(), from.asBareJid(), thumbnail, additional);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Avatar metadata from {} is lacking thumbnail (info.id must match item id",
|
||||||
|
from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<byte[]> getAvatar(final Jid address, final String id) {
|
||||||
|
final var fetch = new Fetch(address, id);
|
||||||
|
final SettableFuture<byte[]> future;
|
||||||
|
synchronized (avatarFetches) {
|
||||||
|
final var existing = avatarFetches.get(fetch);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
future = SettableFuture.create();
|
||||||
|
avatarFetches.put(fetch, future);
|
||||||
|
}
|
||||||
|
future.setFuture(getCachedOrFetch(address, id));
|
||||||
|
future.addListener(
|
||||||
|
() -> {
|
||||||
|
synchronized (this.avatarFetches) {
|
||||||
|
this.avatarFetches.remove(fetch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getCachedAvatar(final Jid address, final String id) throws IOException {
|
||||||
|
final var cache = getCacheFile(address, id);
|
||||||
|
final byte[] avatar = Files.toByteArray(cache);
|
||||||
|
if (Hashing.sha1().hashBytes(avatar).toString().equalsIgnoreCase(id)) {
|
||||||
|
LOGGER.debug("Avatar {} of {} came from cache", id, address);
|
||||||
|
return avatar;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Cache contained corrupted file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<byte[]> getCachedOrFetch(final Jid address, final String id) {
|
||||||
|
final var cachedFuture = Futures.submit(() -> getCachedAvatar(address, id), IO_EXECUTOR);
|
||||||
|
return Futures.catchingAsync(
|
||||||
|
cachedFuture,
|
||||||
|
Exception.class,
|
||||||
|
exception -> fetchAndCacheAvatar(address, id),
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<byte[]> fetchAvatar(final Jid address, final String id) {
|
||||||
|
return Futures.transform(
|
||||||
|
getManager(PubSubManager.class).fetchItem(address, id, Data.class),
|
||||||
|
Data::asBytes,
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<byte[]> fetchAndCacheAvatar(final Jid address, final String id) {
|
||||||
|
return Futures.transform(
|
||||||
|
fetchAvatar(address, id),
|
||||||
|
avatar -> {
|
||||||
|
final var sha1Hash = Hashing.sha1().hashBytes(avatar).toString();
|
||||||
|
if (sha1Hash.equalsIgnoreCase(id)) {
|
||||||
|
final var cache = getCacheFile(address, id);
|
||||||
|
try {
|
||||||
|
Files.write(avatar, cache);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException("Could not store avatar", e);
|
||||||
|
}
|
||||||
|
LOGGER.info("Cached avatar {} from {}", id, address);
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Avatar sha1hash did not match expected value");
|
||||||
|
},
|
||||||
|
IO_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getCacheFile(final Jid address, final String id) {
|
||||||
|
final var accountCacheDirectory =
|
||||||
|
new File(context.getCacheDir(), String.valueOf(getAccount().id));
|
||||||
|
final var userCacheDirectory =
|
||||||
|
new File(
|
||||||
|
accountCacheDirectory,
|
||||||
|
Hashing.sha256()
|
||||||
|
.hashString(address.toEscapedString(), StandardCharsets.UTF_8)
|
||||||
|
.toString());
|
||||||
|
if (userCacheDirectory.mkdirs()) {
|
||||||
|
LOGGER.debug("Created directory {}", userCacheDirectory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return new File(userCacheDirectory, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fetch {
|
||||||
|
public final Jid address;
|
||||||
|
public final String id;
|
||||||
|
|
||||||
|
private Fetch(Jid address, String id) {
|
||||||
|
this.address = address;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.AxolotlDatabaseStore;
|
||||||
|
import im.conversations.android.xmpp.IqErrorException;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.axolotl.AxolotlAddress;
|
||||||
|
import im.conversations.android.xmpp.model.axolotl.Bundle;
|
||||||
|
import im.conversations.android.xmpp.model.axolotl.DeviceList;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.SessionBuilder;
|
||||||
|
import org.whispersystems.libsignal.SessionCipher;
|
||||||
|
import org.whispersystems.libsignal.UntrustedIdentityException;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyBundle;
|
||||||
|
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||||
|
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||||
|
import org.whispersystems.libsignal.util.KeyHelper;
|
||||||
|
|
||||||
|
public class AxolotlManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AxolotlManager.class);
|
||||||
|
|
||||||
|
private static final int NUM_PRE_KEYS_IN_BUNDLE = 30;
|
||||||
|
|
||||||
|
private final SignalProtocolStore signalProtocolStore;
|
||||||
|
|
||||||
|
public AxolotlManager(Context context, XmppConnection connection) {
|
||||||
|
super(context, connection);
|
||||||
|
this.signalProtocolStore = new AxolotlDatabaseStore(context, connection.getAccount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleItems(final Jid from, final Items items) {
|
||||||
|
final var deviceList = items.getFirstItem(DeviceList.class);
|
||||||
|
if (from == null || deviceList == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var deviceIds = deviceList.getDeviceIds();
|
||||||
|
LOGGER.info("Received {} from {}", deviceIds, from);
|
||||||
|
getDatabase().axolotlDao().setDeviceList(getAccount(), from, deviceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Set<Integer>> fetchDeviceIds(final Jid address) {
|
||||||
|
final var deviceIdsFuture =
|
||||||
|
Futures.transform(
|
||||||
|
getManager(PubSubManager.class)
|
||||||
|
.fetchMostRecentItem(
|
||||||
|
address, Namespace.AXOLOTL_DEVICE_LIST, DeviceList.class),
|
||||||
|
DeviceList::getDeviceIds,
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
// TODO refactor callback into class
|
||||||
|
Futures.addCallback(
|
||||||
|
deviceIdsFuture,
|
||||||
|
new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Set<Integer> deviceIds) {
|
||||||
|
getDatabase().axolotlDao().setDeviceList(getAccount(), address, deviceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Throwable throwable) {
|
||||||
|
if (throwable instanceof TimeoutException) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (throwable instanceof IqErrorException) {
|
||||||
|
final var iqErrorException = (IqErrorException) throwable;
|
||||||
|
final var error = iqErrorException.getError();
|
||||||
|
final var condition = error == null ? null : error.getCondition();
|
||||||
|
if (condition != null) {
|
||||||
|
getDatabase()
|
||||||
|
.axolotlDao()
|
||||||
|
.setDeviceListError(getAccount(), address, condition);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getDatabase().axolotlDao().setDeviceListParsingError(getAccount(), address);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
return deviceIdsFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Bundle> fetchBundle(final Jid address, final int deviceId) {
|
||||||
|
final var node = String.format(Locale.ROOT, "%s:%d", Namespace.AXOLOTL_BUNDLES, deviceId);
|
||||||
|
return getManager(PubSubManager.class).fetchMostRecentItem(address, node, Bundle.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<SessionCipher> getOrCreateSessionCipher(
|
||||||
|
final AxolotlAddress axolotlAddress) {
|
||||||
|
if (signalProtocolStore.containsSession(axolotlAddress)) {
|
||||||
|
return Futures.immediateFuture(new SessionCipher(signalProtocolStore, axolotlAddress));
|
||||||
|
} else {
|
||||||
|
final var bundleFuture =
|
||||||
|
fetchBundle(axolotlAddress.getJid(), axolotlAddress.getDeviceId());
|
||||||
|
return Futures.transform(
|
||||||
|
bundleFuture,
|
||||||
|
bundle -> {
|
||||||
|
buildSession(axolotlAddress, bundle);
|
||||||
|
return new SessionCipher(signalProtocolStore, axolotlAddress);
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSession(final AxolotlAddress address, final Bundle bundle) {
|
||||||
|
final var sessionBuilder = new SessionBuilder(signalProtocolStore, address);
|
||||||
|
final var deviceId = address.getDeviceId();
|
||||||
|
final var preKey = bundle.getRandomPreKey();
|
||||||
|
final var signedPreKey = bundle.getSignedPreKey();
|
||||||
|
final var signedPreKeySignature = bundle.getSignedPreKeySignature();
|
||||||
|
final var identityKey = bundle.getIdentityKey();
|
||||||
|
if (preKey == null) {
|
||||||
|
throw new IllegalArgumentException("No PreKey found in bundle");
|
||||||
|
}
|
||||||
|
if (signedPreKey == null) {
|
||||||
|
throw new IllegalArgumentException("No signed PreKey found in bundle");
|
||||||
|
}
|
||||||
|
if (signedPreKeySignature == null) {
|
||||||
|
throw new IllegalArgumentException("No signed PreKey signature found in bundle");
|
||||||
|
}
|
||||||
|
if (identityKey == null) {
|
||||||
|
throw new IllegalArgumentException("No IdentityKey found in bundle");
|
||||||
|
}
|
||||||
|
final var preKeyBundle =
|
||||||
|
new PreKeyBundle(
|
||||||
|
0,
|
||||||
|
deviceId,
|
||||||
|
preKey.getId(),
|
||||||
|
preKey.asECPublicKey(),
|
||||||
|
signedPreKey.getId(),
|
||||||
|
signedPreKey.asECPublicKey(),
|
||||||
|
signedPreKeySignature.asBytes(),
|
||||||
|
new IdentityKey(identityKey.asECPublicKey()));
|
||||||
|
try {
|
||||||
|
sessionBuilder.process(preKeyBundle);
|
||||||
|
} catch (final InvalidKeyException | UntrustedIdentityException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishIfNecessary() {
|
||||||
|
final int myDeviceId = getAccount().getPublicDeviceIdInt();
|
||||||
|
if (getDatabase()
|
||||||
|
.axolotlDao()
|
||||||
|
.hasDeviceId(getAccount().id, getAccount().address, myDeviceId)
|
||||||
|
&& getManager(DiscoManager.class)
|
||||||
|
.hasAccountFeature(Namespace.PUB_SUB_PERSISTENT_ITEMS)) {
|
||||||
|
LOGGER.info(
|
||||||
|
"device id seems to be current and server supports persistent items. nothing"
|
||||||
|
+ " to do");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var future = publishBundleAndDeviceId();
|
||||||
|
Futures.addCallback(
|
||||||
|
future,
|
||||||
|
new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result) {
|
||||||
|
LOGGER.info("Successfully publish bundle and device ID {}", myDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Throwable throwable) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Could not publish bundle and device ID for account {} ",
|
||||||
|
getAccount().address,
|
||||||
|
throwable);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Void> publishBundleAndDeviceId() {
|
||||||
|
final ListenableFuture<Void> bundleFuture = publishBundle();
|
||||||
|
return Futures.transformAsync(
|
||||||
|
bundleFuture, ignored -> publishDeviceId(), MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Void> publishDeviceId() {
|
||||||
|
final var currentDeviceIdsFuture = fetchDeviceIds(getAccount().address);
|
||||||
|
return Futures.transformAsync(
|
||||||
|
currentDeviceIdsFuture,
|
||||||
|
currentDeviceIds -> {
|
||||||
|
final var myDeviceId = getAccount().getPublicDeviceIdInt();
|
||||||
|
if (currentDeviceIds.contains(myDeviceId)) {
|
||||||
|
return Futures.immediateVoidFuture();
|
||||||
|
} else {
|
||||||
|
final var deviceIds =
|
||||||
|
new ImmutableSet.Builder<Integer>()
|
||||||
|
.addAll(currentDeviceIds)
|
||||||
|
.add(myDeviceId)
|
||||||
|
.build();
|
||||||
|
return publishDeviceIds(deviceIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Void> publishDeviceIds(final Collection<Integer> deviceIds) {
|
||||||
|
final var deviceList = new DeviceList();
|
||||||
|
deviceList.setDeviceIds(deviceIds);
|
||||||
|
return getManager(PubSubManager.class)
|
||||||
|
.publishSingleton(getAccount().address, deviceList, Namespace.AXOLOTL_DEVICE_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Void> publishBundle() {
|
||||||
|
final ListenableFuture<Bundle> bundleFuture =
|
||||||
|
Futures.submit(this::prepareBundle, IO_EXECUTOR);
|
||||||
|
return Futures.transformAsync(
|
||||||
|
bundleFuture,
|
||||||
|
bundle -> {
|
||||||
|
final var node =
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"%s:%d",
|
||||||
|
Namespace.AXOLOTL_BUNDLES,
|
||||||
|
signalProtocolStore.getLocalRegistrationId());
|
||||||
|
return getManager(PubSubManager.class)
|
||||||
|
.publishSingleton(getAccount().address, bundle, node);
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle prepareBundle() {
|
||||||
|
refillPreKeys();
|
||||||
|
final var bundle = new Bundle();
|
||||||
|
bundle.setIdentityKey(
|
||||||
|
signalProtocolStore.getIdentityKeyPair().getPublicKey().getPublicKey());
|
||||||
|
final var signedPreKeyRecord =
|
||||||
|
getDatabase().axolotlDao().getLatestSignedPreKey(getAccount().id);
|
||||||
|
if (signedPreKeyRecord == null) {
|
||||||
|
throw new IllegalStateException("No signed PreKeys have been created yet");
|
||||||
|
}
|
||||||
|
bundle.setSignedPreKey(
|
||||||
|
signedPreKeyRecord.getKeyPair().getPublicKey(), signedPreKeyRecord.getSignature());
|
||||||
|
bundle.setPreKeys(getDatabase().axolotlDao().getPreKeys(getAccount().id));
|
||||||
|
return bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refillPreKeys() {
|
||||||
|
final var accountId = getAccount().id;
|
||||||
|
final var axolotlDao = getDatabase().axolotlDao();
|
||||||
|
final var existing = axolotlDao.getExistingPreKeyCount(accountId);
|
||||||
|
final var max = axolotlDao.getMaxPreKeyId(accountId);
|
||||||
|
final var count = NUM_PRE_KEYS_IN_BUNDLE - existing;
|
||||||
|
final int start = max == null ? 0 : max + 1;
|
||||||
|
final var preKeys = KeyHelper.generatePreKeys(start, count);
|
||||||
|
final int signedPreKeyId = (start + count) / NUM_PRE_KEYS_IN_BUNDLE - 1;
|
||||||
|
if (getDatabase().axolotlDao().hasNotSignedPreKey(getAccount().id, signedPreKeyId)) {
|
||||||
|
final SignedPreKeyRecord signedPreKeyRecord;
|
||||||
|
try {
|
||||||
|
signedPreKeyRecord =
|
||||||
|
KeyHelper.generateSignedPreKey(
|
||||||
|
signalProtocolStore.getIdentityKeyPair(), signedPreKeyId);
|
||||||
|
} catch (final InvalidKeyException e) {
|
||||||
|
throw new IllegalStateException("Could not generate SignedPreKey", e);
|
||||||
|
}
|
||||||
|
signalProtocolStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
|
||||||
|
LOGGER.info("Generated SignedPreKey #{}", signedPreKeyRecord.getId());
|
||||||
|
}
|
||||||
|
axolotlDao.setPreKeys(getAccount(), preKeys);
|
||||||
|
if (count > 0) {
|
||||||
|
LOGGER.info("Generated {} PreKeys starting with {}", preKeys.size(), start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,74 @@
|
||||||
package im.conversations.android.xmpp.manager;
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.entity.BookmarkEntity;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.model.bookmark.Conference;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Retract;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class BookmarkManager extends AbstractManager {
|
public class BookmarkManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(BookmarkManager.class);
|
||||||
|
|
||||||
public BookmarkManager(Context context, XmppConnection connection) {
|
public BookmarkManager(Context context, XmppConnection connection) {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fetch() {}
|
public void fetch() {
|
||||||
|
final var future =
|
||||||
|
getManager(PubSubManager.class).fetchItems(getAccount().address, Conference.class);
|
||||||
|
Futures.addCallback(
|
||||||
|
future,
|
||||||
|
new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final Map<String, Conference> bookmarks) {
|
||||||
|
getDatabase().bookmarkDao().setItems(getAccount(), bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull final Throwable throwable) {
|
||||||
|
LOGGER.warn("Could not fetch bookmarks", throwable);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateItems(final Map<String, Conference> items) {
|
||||||
|
getDatabase().bookmarkDao().updateItems(getAccount(), items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteItems(Collection<Retract> retractions) {
|
||||||
|
final Collection<Jid> addresses =
|
||||||
|
Collections2.transform(retractions, r -> BookmarkEntity.jidOrNull(r.getId()));
|
||||||
|
getDatabase()
|
||||||
|
.bookmarkDao()
|
||||||
|
.delete(getAccount().id, Collections2.filter(addresses, Objects::nonNull));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAllItems() {
|
||||||
|
getDatabase().bookmarkDao().deleteAll(getAccount().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleItems(final Items items) {
|
||||||
|
final var retractions = items.getRetractions();
|
||||||
|
final var itemMap = items.getItemMap(Conference.class);
|
||||||
|
if (retractions.size() > 0) {
|
||||||
|
deleteItems(retractions);
|
||||||
|
}
|
||||||
|
if (itemMap.size() > 0) {
|
||||||
|
updateItems(itemMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,11 @@ public class DiscoManager extends AbstractManager {
|
||||||
Collections.singleton(Namespace.VERSION);
|
Collections.singleton(Namespace.VERSION);
|
||||||
|
|
||||||
private static final Collection<String> FEATURES_NOTIFY =
|
private static final Collection<String> FEATURES_NOTIFY =
|
||||||
Arrays.asList(Namespace.NICK, Namespace.AVATAR_METADATA, Namespace.BOOKMARKS2);
|
Arrays.asList(
|
||||||
|
Namespace.NICK,
|
||||||
|
Namespace.AVATAR_METADATA,
|
||||||
|
Namespace.BOOKMARKS2,
|
||||||
|
Namespace.AXOLOTL_DEVICE_LIST);
|
||||||
|
|
||||||
public DiscoManager(Context context, XmppConnection connection) {
|
public DiscoManager(Context context, XmppConnection connection) {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
|
@ -240,6 +244,10 @@ public class DiscoManager extends AbstractManager {
|
||||||
return getDatabase().discoDao().hasFeature(getAccount().id, entity, feature);
|
return getDatabase().discoDao().hasFeature(getAccount().id, entity, feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasAccountFeature(final String feature) {
|
||||||
|
return hasFeature(getAccount().address, feature);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasServerFeature(final String feature) {
|
public boolean hasServerFeature(final String feature) {
|
||||||
return hasFeature(getAccount().address.getDomain(), feature);
|
return hasFeature(getAccount().address.getDomain(), feature);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.model.nick.Nick;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class NickManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(NickManager.class);
|
||||||
|
|
||||||
|
public NickManager(Context context, XmppConnection connection) {
|
||||||
|
super(context, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleItems(final Jid from, Items items) {
|
||||||
|
final var item = items.getFirstItem(Nick.class);
|
||||||
|
final var nick = item == null ? null : item.getContent();
|
||||||
|
if (from == null || Strings.isNullOrEmpty(nick)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getDatabase().nickDao().set(getAccount(), from.asBareJid(), nick);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.xmpp.ExtensionFactory;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.PubSub;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Event;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Purge;
|
||||||
|
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||||
|
import im.conversations.android.xmpp.model.stanza.Message;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class PubSubManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(PubSubManager.class);
|
||||||
|
|
||||||
|
private static final String SINGLETON_ITEM_ID = "current";
|
||||||
|
|
||||||
|
public PubSubManager(Context context, XmppConnection connection) {
|
||||||
|
super(context, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleEvent(final Message message) {
|
||||||
|
final var event = message.getExtension(Event.class);
|
||||||
|
if (event.hasExtension(Purge.class)) {
|
||||||
|
handlePurge(message);
|
||||||
|
} else if (event.hasExtension(Event.ItemsWrapper.class)) {
|
||||||
|
handleItems(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Extension> ListenableFuture<Map<String, T>> fetchItems(
|
||||||
|
final Jid address, final Class<T> clazz) {
|
||||||
|
final var id = ExtensionFactory.id(clazz);
|
||||||
|
if (id == null) {
|
||||||
|
return Futures.immediateFailedFuture(
|
||||||
|
new IllegalArgumentException(
|
||||||
|
String.format("%s is not a registered extension", clazz.getName())));
|
||||||
|
}
|
||||||
|
return fetchItems(address, id.namespace, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Extension> ListenableFuture<Map<String, T>> fetchItems(
|
||||||
|
final Jid address, final String node, final Class<T> clazz) {
|
||||||
|
final Iq request = new Iq(Iq.Type.GET);
|
||||||
|
request.setTo(address);
|
||||||
|
final var pubSub = request.addExtension(new PubSub());
|
||||||
|
final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
|
||||||
|
itemsWrapper.setNode(node);
|
||||||
|
return Futures.transform(
|
||||||
|
connection.sendIqPacket(request),
|
||||||
|
response -> {
|
||||||
|
final var pubSubResponse = response.getExtension(PubSub.class);
|
||||||
|
if (pubSubResponse == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
final var items = pubSubResponse.getItems();
|
||||||
|
if (items == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return items.getItemMap(clazz);
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Extension> ListenableFuture<T> fetchItem(
|
||||||
|
final Jid address, final String itemId, final Class<T> clazz) {
|
||||||
|
final var id = ExtensionFactory.id(clazz);
|
||||||
|
if (id == null) {
|
||||||
|
return Futures.immediateFailedFuture(
|
||||||
|
new IllegalArgumentException(
|
||||||
|
String.format("%s is not a registered extension", clazz.getName())));
|
||||||
|
}
|
||||||
|
return fetchItem(address, id.namespace, itemId, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Extension> ListenableFuture<T> fetchItem(
|
||||||
|
final Jid address, final String node, final String itemId, final Class<T> clazz) {
|
||||||
|
final Iq request = new Iq(Iq.Type.GET);
|
||||||
|
request.setTo(address);
|
||||||
|
final var pubSub = request.addExtension(new PubSub());
|
||||||
|
final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
|
||||||
|
itemsWrapper.setNode(node);
|
||||||
|
final var item = itemsWrapper.addExtension(new PubSub.Item());
|
||||||
|
item.setId(itemId);
|
||||||
|
return Futures.transform(
|
||||||
|
connection.sendIqPacket(request),
|
||||||
|
response -> {
|
||||||
|
final var pubSubResponse = response.getExtension(PubSub.class);
|
||||||
|
if (pubSubResponse == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
final var items = pubSubResponse.getItems();
|
||||||
|
if (items == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return items.getItemOrThrow(itemId, clazz);
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Extension> ListenableFuture<T> fetchMostRecentItem(
|
||||||
|
final Jid address, final String node, final Class<T> clazz) {
|
||||||
|
final Iq request = new Iq(Iq.Type.GET);
|
||||||
|
request.setTo(address);
|
||||||
|
final var pubSub = request.addExtension(new PubSub());
|
||||||
|
final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
|
||||||
|
itemsWrapper.setNode(node);
|
||||||
|
itemsWrapper.setMaxItems(1);
|
||||||
|
return Futures.transform(
|
||||||
|
connection.sendIqPacket(request),
|
||||||
|
response -> {
|
||||||
|
final var pubSubResponse = response.getExtension(PubSub.class);
|
||||||
|
if (pubSubResponse == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
final var items = pubSubResponse.getItems();
|
||||||
|
if (items == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
return items.getOnlyItem(clazz);
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleItems(final Message message) {
|
||||||
|
final var from = message.getFrom();
|
||||||
|
final var event = message.getExtension(Event.class);
|
||||||
|
final Items items = event.getItems();
|
||||||
|
final var node = items.getNode();
|
||||||
|
if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
|
||||||
|
getManager(BookmarkManager.class).handleItems(items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Namespace.AVATAR_METADATA.equals(node)) {
|
||||||
|
getManager(AvatarManager.class).handleItems(from, items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Namespace.NICK.equals(node)) {
|
||||||
|
getManager(NickManager.class).handleItems(from, items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Namespace.AXOLOTL_DEVICE_LIST.equals(node)) {
|
||||||
|
getManager(AxolotlManager.class).handleItems(from, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePurge(final Message message) {
|
||||||
|
final var from = message.getFrom();
|
||||||
|
final var event = message.getExtension(Event.class);
|
||||||
|
final var purge = event.getPurge();
|
||||||
|
final var node = purge.getNode();
|
||||||
|
if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
|
||||||
|
getManager(BookmarkManager.class).deleteAllItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> publishSingleton(Jid address, Extension item) {
|
||||||
|
final var id = ExtensionFactory.id(item.getClass());
|
||||||
|
return publish(address, item, SINGLETON_ITEM_ID, id.namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> publishSingleton(Jid address, Extension item, final String node) {
|
||||||
|
return publish(address, item, SINGLETON_ITEM_ID, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> publish(Jid address, Extension item, final String itemId) {
|
||||||
|
final var id = ExtensionFactory.id(item.getClass());
|
||||||
|
return publish(address, item, itemId, id.namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> publish(
|
||||||
|
final Jid address,
|
||||||
|
final Extension itemPayload,
|
||||||
|
final String itemId,
|
||||||
|
final String node) {
|
||||||
|
final var iq = new Iq(Iq.Type.SET);
|
||||||
|
iq.setTo(address);
|
||||||
|
final var pubSub = iq.addExtension(new PubSub());
|
||||||
|
final var pubSubItemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
|
||||||
|
pubSubItemsWrapper.setNode(node);
|
||||||
|
final var item = pubSubItemsWrapper.addExtension(new PubSub.Item());
|
||||||
|
item.setId(itemId);
|
||||||
|
item.addExtension(itemPayload);
|
||||||
|
return Futures.transform(
|
||||||
|
connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package im.conversations.android.xmpp.model;
|
||||||
|
|
||||||
|
import com.google.common.base.CharMatcher;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.io.BaseEncoding;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
|
||||||
|
public interface ByteContent {
|
||||||
|
|
||||||
|
String getContent();
|
||||||
|
|
||||||
|
default byte[] asBytes() {
|
||||||
|
final var content = this.getContent();
|
||||||
|
if (Strings.isNullOrEmpty(content)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s element is lacking content", getClass().getName()));
|
||||||
|
}
|
||||||
|
final var contentCleaned = CharMatcher.whitespace().removeFrom(content);
|
||||||
|
if (BaseEncoding.base64().canDecode(contentCleaned)) {
|
||||||
|
return BaseEncoding.base64().decode(contentCleaned);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s element contains invalid base64", getClass().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setContent(final byte[] bytes) {
|
||||||
|
setContent(BaseEncoding.base64().encode(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
Element setContent(final String content);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.conversations.android.xmpp.model.avatar;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.ByteContent;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(namespace = Namespace.AVATAR_DATA)
|
||||||
|
public class Data extends Extension implements ByteContent {
|
||||||
|
|
||||||
|
public Data() {
|
||||||
|
super(Data.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package im.conversations.android.xmpp.model.avatar;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(namespace = Namespace.AVATAR_METADATA)
|
||||||
|
public class Info extends Extension {
|
||||||
|
|
||||||
|
public Info() {
|
||||||
|
super(Info.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getHeight() {
|
||||||
|
return this.getLongAttribute("height");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getWidth() {
|
||||||
|
return this.getLongAttribute("width");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBytes() {
|
||||||
|
return this.getLongAttribute("bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return this.getAttribute("type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return this.getAttribute("url");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return this.getAttribute("id");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android.xmpp.model.avatar;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(namespace = Namespace.AVATAR_METADATA)
|
||||||
|
public class Metadata extends Extension {
|
||||||
|
|
||||||
|
public Metadata() {
|
||||||
|
super(Metadata.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||||
|
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Bundle extends Extension {
|
||||||
|
|
||||||
|
public Bundle() {
|
||||||
|
super(Bundle.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignedPreKey getSignedPreKey() {
|
||||||
|
return this.getExtension(SignedPreKey.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignedPreKeySignature getSignedPreKeySignature() {
|
||||||
|
return this.getExtension(SignedPreKeySignature.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentityKey getIdentityKey() {
|
||||||
|
return this.getExtension(IdentityKey.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreKey getRandomPreKey() {
|
||||||
|
final var preKeys = this.getExtension(PreKeys.class);
|
||||||
|
final Collection<PreKey> preKeyList =
|
||||||
|
preKeys == null ? Collections.emptyList() : preKeys.getExtensions(PreKey.class);
|
||||||
|
return Iterables.get(preKeyList, (int) (preKeyList.size() * Math.random()), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentityKey(final ECPublicKey ecPublicKey) {
|
||||||
|
final var identityKey = this.addExtension(new IdentityKey());
|
||||||
|
identityKey.setContent(ecPublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignedPreKey(final ECPublicKey ecPublicKey, final byte[] signature) {
|
||||||
|
final var signedPreKey = this.addExtension(new SignedPreKey());
|
||||||
|
signedPreKey.setContent(ecPublicKey);
|
||||||
|
final var signedPreKeySignature = this.addExtension(new SignedPreKeySignature());
|
||||||
|
signedPreKeySignature.setContent(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPreKeys(final List<PreKeyRecord> preKeyRecords) {
|
||||||
|
final var preKeys = this.addExtension(new PreKeys());
|
||||||
|
for (final PreKeyRecord preKeyRecord : preKeyRecords) {
|
||||||
|
final var preKey = preKeys.addExtension(new PreKey());
|
||||||
|
preKey.setId(preKeyRecord.getId());
|
||||||
|
preKey.setContent(preKeyRecord.getKeyPair().getPublicKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Device extends Extension {
|
||||||
|
|
||||||
|
public Device() {
|
||||||
|
super(Device.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDeviceId() {
|
||||||
|
return Ints.tryParse(Strings.nullToEmpty(this.getAttribute("id")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceId(int deviceId) {
|
||||||
|
this.setAttribute("id", deviceId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@XmlElement(name = "list")
|
||||||
|
public class DeviceList extends Extension {
|
||||||
|
|
||||||
|
public DeviceList() {
|
||||||
|
super(DeviceList.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Device> getDevices() {
|
||||||
|
return this.getExtensions(Device.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Integer> getDeviceIds() {
|
||||||
|
return ImmutableSet.copyOf(
|
||||||
|
Collections2.filter(
|
||||||
|
Collections2.transform(getDevices(), Device::getDeviceId),
|
||||||
|
Objects::nonNull));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeviceIds(Collection<Integer> deviceIds) {
|
||||||
|
for (final Integer deviceId : deviceIds) {
|
||||||
|
final var device = this.addExtension(new Device());
|
||||||
|
device.setDeviceId(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import im.conversations.android.xmpp.model.ByteContent;
|
||||||
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
import org.whispersystems.libsignal.ecc.Curve;
|
||||||
|
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||||
|
|
||||||
|
public interface ECPublicKeyContent extends ByteContent {
|
||||||
|
|
||||||
|
default ECPublicKey asECPublicKey() {
|
||||||
|
try {
|
||||||
|
return Curve.decodePoint(asBytes(), 0);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format("%s does not contain a valid ECPublicKey", getClass().getName()),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setContent(final ECPublicKey ecPublicKey) {
|
||||||
|
setContent(ecPublicKey.serialize());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "identityKey")
|
||||||
|
public class IdentityKey extends Extension implements ECPublicKeyContent {
|
||||||
|
|
||||||
|
public IdentityKey() {
|
||||||
|
super(IdentityKey.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "preKeyPublic")
|
||||||
|
public class PreKey extends Extension implements ECPublicKeyContent {
|
||||||
|
|
||||||
|
public PreKey() {
|
||||||
|
super(PreKey.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return Ints.saturatedCast(this.getLongAttribute("preKeyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.setAttribute("id", id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "prekeys")
|
||||||
|
public class PreKeys extends Extension {
|
||||||
|
|
||||||
|
public PreKeys() {
|
||||||
|
super(PreKeys.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "signedPreKeyPublic")
|
||||||
|
public class SignedPreKey extends Extension implements ECPublicKeyContent {
|
||||||
|
|
||||||
|
public SignedPreKey() {
|
||||||
|
super(SignedPreKey.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return Ints.saturatedCast(this.getLongAttribute("signedPreKeyId"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.ByteContent;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(name = "signedPreKeySignature")
|
||||||
|
public class SignedPreKeySignature extends Extension implements ByteContent {
|
||||||
|
|
||||||
|
public SignedPreKeySignature() {
|
||||||
|
super(SignedPreKeySignature.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
@XmlPackage(namespace = Namespace.AXOLOTL)
|
||||||
|
package im.conversations.android.xmpp.model.axolotl;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
|
@ -0,0 +1,20 @@
|
||||||
|
package im.conversations.android.xmpp.model.bookmark;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Conference extends Extension {
|
||||||
|
|
||||||
|
public Conference() {
|
||||||
|
super(Conference.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAutoJoin() {
|
||||||
|
return this.getAttributeAsBoolean("autojoin");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConferenceName() {
|
||||||
|
return this.getAttribute("name");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
@XmlPackage(namespace = Namespace.BOOKMARKS2)
|
||||||
|
package im.conversations.android.xmpp.model.bookmark;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
|
@ -6,7 +6,7 @@ import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
public abstract class Condition extends Extension {
|
public abstract class Condition extends Extension {
|
||||||
|
|
||||||
public Condition(Class<? extends Extension> clazz) {
|
private Condition(Class<? extends Extension> clazz) {
|
||||||
super(clazz);
|
super(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,4 +18,8 @@ public class Error extends Extension {
|
||||||
public void setCondition(final Condition condition) {
|
public void setCondition(final Condition condition) {
|
||||||
this.addExtension(condition);
|
this.addExtension(condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Text getText() {
|
||||||
|
return this.getExtension(Text.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android.xmpp.model.error;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(namespace = Namespace.STANZAS)
|
||||||
|
public class Text extends Extension {
|
||||||
|
|
||||||
|
public Text() {
|
||||||
|
super(Text.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android.xmpp.model.nick;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement(namespace = Namespace.NICK)
|
||||||
|
public class Nick extends Extension {
|
||||||
|
|
||||||
|
public Nick() {
|
||||||
|
super(Nick.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub;
|
||||||
|
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
public interface Item {
|
||||||
|
|
||||||
|
<T extends Extension> T getExtension(final Class<T> clazz);
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Retract;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
public interface Items {
|
||||||
|
|
||||||
|
Collection<? extends Item> getItems();
|
||||||
|
|
||||||
|
String getNode();
|
||||||
|
|
||||||
|
Collection<Retract> getRetractions();
|
||||||
|
|
||||||
|
default <T extends Extension> Map<String, T> getItemMap(final Class<T> clazz) {
|
||||||
|
final ImmutableMap.Builder<String, T> builder = ImmutableMap.builder();
|
||||||
|
for (final Item item : getItems()) {
|
||||||
|
final var id = item.getId();
|
||||||
|
final T extension = item.getExtension(clazz);
|
||||||
|
if (extension == null || Strings.isNullOrEmpty(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
builder.put(id, extension);
|
||||||
|
}
|
||||||
|
return builder.buildKeepingLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T extends Extension> T getItemOrThrow(final String id, final Class<T> clazz) {
|
||||||
|
final var map = getItemMap(clazz);
|
||||||
|
final var item = map.get(id);
|
||||||
|
if (item == null) {
|
||||||
|
throw new NoSuchElementException(
|
||||||
|
String.format("An item with id %s does not exist", id));
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T extends Extension> T getFirstItem(final Class<T> clazz) {
|
||||||
|
final var map = getItemMap(clazz);
|
||||||
|
return Iterables.getFirst(map.values(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T extends Extension> T getOnlyItem(final Class<T> clazz) {
|
||||||
|
final var map = getItemMap(clazz);
|
||||||
|
return Iterables.getOnlyElement(map.values());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Retract;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@XmlElement(name = "pubsub")
|
||||||
|
public class PubSub extends Extension {
|
||||||
|
|
||||||
|
public PubSub() {
|
||||||
|
super(PubSub.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Items getItems() {
|
||||||
|
return this.getExtension(ItemsWrapper.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlElement(name = "items")
|
||||||
|
public static class ItemsWrapper extends Extension implements Items {
|
||||||
|
|
||||||
|
public ItemsWrapper() {
|
||||||
|
super(ItemsWrapper.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNode() {
|
||||||
|
return this.getAttribute("node");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
|
||||||
|
return this.getExtensions(Item.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Retract> getRetractions() {
|
||||||
|
return this.getExtensions(Retract.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNode(String node) {
|
||||||
|
this.setAttribute("node", node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxItems(final int maxItems) {
|
||||||
|
this.setAttribute("max_items", maxItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlElement(name = "item")
|
||||||
|
public static class Item extends Extension
|
||||||
|
implements im.conversations.android.xmpp.model.pubsub.Item {
|
||||||
|
|
||||||
|
public Item() {
|
||||||
|
super(Item.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return this.getAttribute("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String itemId) {
|
||||||
|
this.setAttribute("id", itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub.event;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.Items;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Event extends Extension {
|
||||||
|
|
||||||
|
public Event() {
|
||||||
|
super(Event.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Items getItems() {
|
||||||
|
return this.getExtension(ItemsWrapper.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Purge getPurge() {
|
||||||
|
return this.getExtension(Purge.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlElement(name = "items")
|
||||||
|
public static class ItemsWrapper extends Extension implements Items {
|
||||||
|
|
||||||
|
public ItemsWrapper() {
|
||||||
|
super(ItemsWrapper.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNode() {
|
||||||
|
return this.getAttribute("node");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
|
||||||
|
return this.getExtensions(Item.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Retract> getRetractions() {
|
||||||
|
return this.getExtensions(Retract.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlElement(name = "item")
|
||||||
|
public static class Item extends Extension
|
||||||
|
implements im.conversations.android.xmpp.model.pubsub.Item {
|
||||||
|
|
||||||
|
public Item() {
|
||||||
|
super(Item.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return this.getAttribute("id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub.event;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Purge extends Extension {
|
||||||
|
|
||||||
|
public Purge() {
|
||||||
|
super(Purge.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNode() {
|
||||||
|
return this.getAttribute("node");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package im.conversations.android.xmpp.model.pubsub.event;
|
||||||
|
|
||||||
|
import im.conversations.android.annotation.XmlElement;
|
||||||
|
import im.conversations.android.xmpp.model.Extension;
|
||||||
|
|
||||||
|
@XmlElement
|
||||||
|
public class Retract extends Extension {
|
||||||
|
|
||||||
|
public Retract() {
|
||||||
|
super(Retract.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return this.getAttribute("id");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
@XmlPackage(namespace = Namespace.PUB_SUB_EVENT)
|
||||||
|
package im.conversations.android.xmpp.model.pubsub.event;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
|
@ -0,0 +1,5 @@
|
||||||
|
@XmlPackage(namespace = Namespace.PUB_SUB)
|
||||||
|
package im.conversations.android.xmpp.model.pubsub;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.annotation.XmlPackage;
|
|
@ -5,6 +5,7 @@ import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import im.conversations.android.xmpp.Entity;
|
import im.conversations.android.xmpp.Entity;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import im.conversations.android.xmpp.manager.AxolotlManager;
|
||||||
import im.conversations.android.xmpp.manager.BlockingManager;
|
import im.conversations.android.xmpp.manager.BlockingManager;
|
||||||
import im.conversations.android.xmpp.manager.BookmarkManager;
|
import im.conversations.android.xmpp.manager.BookmarkManager;
|
||||||
import im.conversations.android.xmpp.manager.DiscoManager;
|
import im.conversations.android.xmpp.manager.DiscoManager;
|
||||||
|
@ -47,6 +48,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
|
||||||
|
|
||||||
getManager(BookmarkManager.class).fetch();
|
getManager(BookmarkManager.class).fetch();
|
||||||
|
|
||||||
|
getManager(AxolotlManager.class).publishIfNecessary();
|
||||||
|
|
||||||
getManager(PresenceManager.class).sendPresence();
|
getManager(PresenceManager.class).sendPresence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import android.content.Context;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.manager.CarbonsManager;
|
import im.conversations.android.xmpp.manager.CarbonsManager;
|
||||||
|
import im.conversations.android.xmpp.manager.PubSubManager;
|
||||||
import im.conversations.android.xmpp.model.carbons.Received;
|
import im.conversations.android.xmpp.model.carbons.Received;
|
||||||
import im.conversations.android.xmpp.model.carbons.Sent;
|
import im.conversations.android.xmpp.model.carbons.Sent;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.event.Event;
|
||||||
import im.conversations.android.xmpp.model.stanza.Message;
|
import im.conversations.android.xmpp.model.stanza.Message;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -32,10 +34,17 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
|
||||||
|
|
||||||
if (isRoot && connection.fromServer(message) && message.hasExtension(Received.class)) {
|
if (isRoot && connection.fromServer(message) && message.hasExtension(Received.class)) {
|
||||||
getManager(CarbonsManager.class).handleReceived(message.getExtension(Received.class));
|
getManager(CarbonsManager.class).handleReceived(message.getExtension(Received.class));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRoot && connection.fromServer(message) && message.hasExtension(Sent.class)) {
|
if (isRoot && connection.fromServer(message) && message.hasExtension(Sent.class)) {
|
||||||
getManager(CarbonsManager.class).handleSent(message.getExtension(Sent.class));
|
getManager(CarbonsManager.class).handleSent(message.getExtension(Sent.class));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoot && message.hasExtension(Event.class)) {
|
||||||
|
getManager(PubSubManager.class).handleEvent(message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String body = message.getBody();
|
final String body = message.getBody();
|
||||||
|
|
65
src/test/java/im/conversations/android/xmpp/PubSubTest.java
Normal file
65
src/test/java/im/conversations/android/xmpp/PubSubTest.java
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.XmlElementReader;
|
||||||
|
import im.conversations.android.xmpp.model.bookmark.Conference;
|
||||||
|
import im.conversations.android.xmpp.model.pubsub.PubSub;
|
||||||
|
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.ConscryptMode;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@ConscryptMode(ConscryptMode.Mode.OFF)
|
||||||
|
public class PubSubTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseBookmarkResult() throws IOException {
|
||||||
|
final String xml =
|
||||||
|
"<iq type='result'\n"
|
||||||
|
+ " to='juliet@capulet.lit/balcony'\n"
|
||||||
|
+ " id='retrieve1' xmlns='jabber:client'>\n"
|
||||||
|
+ " <pubsub xmlns='http://jabber.org/protocol/pubsub'>\n"
|
||||||
|
+ " <items node='urn:xmpp:bookmarks:1'>\n"
|
||||||
|
+ " <item id='theplay@conference.shakespeare.lit'>\n"
|
||||||
|
+ " <conference xmlns='urn:xmpp:bookmarks:1'\n"
|
||||||
|
+ " name='The Play's the Thing'\n"
|
||||||
|
+ " autojoin='true'>\n"
|
||||||
|
+ " <nick>JC</nick>\n"
|
||||||
|
+ " </conference>\n"
|
||||||
|
+ " </item>\n"
|
||||||
|
+ " <item id='orchard@conference.shakespeare.lit'>\n"
|
||||||
|
+ " <conference xmlns='urn:xmpp:bookmarks:1'\n"
|
||||||
|
+ " name='The Orcard'\n"
|
||||||
|
+ " autojoin='1'>\n"
|
||||||
|
+ " <nick>JC</nick>\n"
|
||||||
|
+ " <extensions>\n"
|
||||||
|
+ " <state xmlns='http://myclient.example/bookmark/state'"
|
||||||
|
+ " minimized='true'/>\n"
|
||||||
|
+ " </extensions>\n"
|
||||||
|
+ " </conference>\n"
|
||||||
|
+ " </item>\n"
|
||||||
|
+ " </items>\n"
|
||||||
|
+ " </pubsub>\n"
|
||||||
|
+ "</iq>";
|
||||||
|
final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
|
||||||
|
assertThat(element, instanceOf(Iq.class));
|
||||||
|
final Iq iq = (Iq) element;
|
||||||
|
final var pubSub = iq.getExtension(PubSub.class);
|
||||||
|
Assert.assertNotNull(pubSub);
|
||||||
|
final var items = pubSub.getItems();
|
||||||
|
Assert.assertNotNull(items);
|
||||||
|
final var itemMap = items.getItemMap(Conference.class);
|
||||||
|
Assert.assertEquals(2, itemMap.size());
|
||||||
|
final var conference = itemMap.get("orchard@conference.shakespeare.lit");
|
||||||
|
Assert.assertNotNull(conference);
|
||||||
|
Assert.assertEquals("The Orcard", conference.getConferenceName());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue