Merge branch 'soru/replay-to-device-messages' into 'main'

feat: Replay last sent olm message on olm session recovery from other device

See merge request famedly/famedlysdk!616
This commit is contained in:
Sorunome 2021-01-20 12:50:47 +00:00
commit 1171720bf2
37 changed files with 270 additions and 57 deletions

View File

@ -93,6 +93,11 @@ class Encryption {
// them in the background
unawaited(runInRoot(() => keyManager.handleToDeviceEvent(event)));
}
if (event.type == EventTypes.Dummy) {
// the previous device just had to create a new olm session, due to olm session
// corruption. We want to try to send it the last message we just sent it, if possible
unawaited(runInRoot(() => olmManager.handleToDeviceEvent(event)));
}
if (event.type.startsWith('m.key.verification.')) {
// some key verification event. No need to handle it now, we can easily
// do this in the background
@ -336,12 +341,6 @@ class Encryption {
return encryptedPayload;
}
Future<Map<String, dynamic>> encryptToDeviceMessagePayload(
DeviceKeys device, String type, Map<String, dynamic> payload) async {
return await olmManager.encryptToDeviceMessagePayload(
device, type, payload);
}
Future<Map<String, dynamic>> encryptToDeviceMessage(
List<DeviceKeys> deviceKeys,
String type,

View File

@ -444,7 +444,7 @@ class OlmManager {
}
_restoredOlmSessionsTime[mapKey] = DateTime.now();
await startOutgoingOlmSessions([device]);
await client.sendToDeviceEncrypted([device], 'm.dummy', {});
await client.sendToDeviceEncrypted([device], EventTypes.Dummy, {});
}
Future<ToDeviceEvent> decryptToDeviceEvent(ToDeviceEvent event) async {
@ -542,6 +542,16 @@ class OlmManager {
};
final encryptResult = sess.first.session.encrypt(json.encode(fullPayload));
storeOlmSession(sess.first);
if (client.database != null) {
unawaited(client.database.setLastSentMessageUserDeviceKey(
json.encode({
'type': type,
'content': payload,
}),
client.id,
device.userId,
device.deviceId));
}
final encryptedBody = <String, dynamic>{
'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2,
'sender_key': identityKey,
@ -587,6 +597,35 @@ class OlmManager {
return data;
}
Future<void> handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == EventTypes.Dummy) {
// We receive dan encrypted m.dummy. This means that the other end was not able to
// decrypt our last message. So, we re-send it.
if (event.encryptedContent == null || client.database == null) {
return;
}
final device = client.getUserDeviceKeysByCurve25519Key(
event.encryptedContent.tryGet<String>('sender_key', ''));
if (device == null) {
return; // device not found
}
Logs().v(
'[OlmManager] Device ${device.userId}:${device.deviceId} generated a new olm session, replaying last sent message...');
final lastSentMessageRes = await client.database
.getLastSentMessageUserDeviceKey(
client.id, device.userId, device.deviceId)
.get();
if (lastSentMessageRes.isEmpty ||
(lastSentMessageRes.first?.isEmpty ?? true)) {
return;
}
final lastSentMessage = json.decode(lastSentMessageRes.first);
// okay, time to send the message!
await client.sendToDeviceEncrypted(
[device], lastSentMessage['type'], lastSentMessage['content']);
}
}
void dispose() {
for (final sessions in olmSessions.values) {
for (final sess in sessions) {

View File

@ -70,7 +70,7 @@ class Database extends _$Database {
Database.connect(DatabaseConnection connection) : super.connect(connection);
@override
int get schemaVersion => 9;
int get schemaVersion => 10;
int get maxFileSize => 1 * 1024 * 1024;
@ -164,6 +164,12 @@ class Database extends _$Database {
userDeviceKeysKey, userDeviceKeysKey.lastActive);
from++;
}
if (from == 9) {
await m.addColumnIfNotExists(
userDeviceKeysKey, userDeviceKeysKey.lastSentMessage);
await m.createIndexIfNotExists(olmSessionsIdentityIndex);
from++;
}
} catch (e, s) {
api.Logs().e('Database migration failed', e, s);
onError.add(SdkError(exception: e, stackTrace: s));

View File

@ -769,6 +769,7 @@ class DbUserDeviceKeysKey extends DataClass
final bool verified;
final bool blocked;
final int lastActive;
final String lastSentMessage;
DbUserDeviceKeysKey(
{@required this.clientId,
@required this.userId,
@ -776,7 +777,8 @@ class DbUserDeviceKeysKey extends DataClass
@required this.content,
this.verified,
this.blocked,
this.lastActive});
this.lastActive,
this.lastSentMessage});
factory DbUserDeviceKeysKey.fromData(
Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
@ -799,6 +801,8 @@ class DbUserDeviceKeysKey extends DataClass
boolType.mapFromDatabaseResponse(data['${effectivePrefix}blocked']),
lastActive: intType
.mapFromDatabaseResponse(data['${effectivePrefix}last_active']),
lastSentMessage: stringType
.mapFromDatabaseResponse(data['${effectivePrefix}last_sent_message']),
);
}
@override
@ -825,6 +829,9 @@ class DbUserDeviceKeysKey extends DataClass
if (!nullToAbsent || lastActive != null) {
map['last_active'] = Variable<int>(lastActive);
}
if (!nullToAbsent || lastSentMessage != null) {
map['last_sent_message'] = Variable<String>(lastSentMessage);
}
return map;
}
@ -850,6 +857,9 @@ class DbUserDeviceKeysKey extends DataClass
lastActive: lastActive == null && nullToAbsent
? const Value.absent()
: Value(lastActive),
lastSentMessage: lastSentMessage == null && nullToAbsent
? const Value.absent()
: Value(lastSentMessage),
);
}
@ -864,6 +874,7 @@ class DbUserDeviceKeysKey extends DataClass
verified: serializer.fromJson<bool>(json['verified']),
blocked: serializer.fromJson<bool>(json['blocked']),
lastActive: serializer.fromJson<int>(json['last_active']),
lastSentMessage: serializer.fromJson<String>(json['last_sent_message']),
);
}
@override
@ -877,6 +888,7 @@ class DbUserDeviceKeysKey extends DataClass
'verified': serializer.toJson<bool>(verified),
'blocked': serializer.toJson<bool>(blocked),
'last_active': serializer.toJson<int>(lastActive),
'last_sent_message': serializer.toJson<String>(lastSentMessage),
};
}
@ -887,7 +899,8 @@ class DbUserDeviceKeysKey extends DataClass
String content,
bool verified,
bool blocked,
int lastActive}) =>
int lastActive,
String lastSentMessage}) =>
DbUserDeviceKeysKey(
clientId: clientId ?? this.clientId,
userId: userId ?? this.userId,
@ -896,6 +909,7 @@ class DbUserDeviceKeysKey extends DataClass
verified: verified ?? this.verified,
blocked: blocked ?? this.blocked,
lastActive: lastActive ?? this.lastActive,
lastSentMessage: lastSentMessage ?? this.lastSentMessage,
);
@override
String toString() {
@ -906,7 +920,8 @@ class DbUserDeviceKeysKey extends DataClass
..write('content: $content, ')
..write('verified: $verified, ')
..write('blocked: $blocked, ')
..write('lastActive: $lastActive')
..write('lastActive: $lastActive, ')
..write('lastSentMessage: $lastSentMessage')
..write(')'))
.toString();
}
@ -920,8 +935,12 @@ class DbUserDeviceKeysKey extends DataClass
deviceId.hashCode,
$mrjc(
content.hashCode,
$mrjc(verified.hashCode,
$mrjc(blocked.hashCode, lastActive.hashCode)))))));
$mrjc(
verified.hashCode,
$mrjc(
blocked.hashCode,
$mrjc(lastActive.hashCode,
lastSentMessage.hashCode))))))));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
@ -932,7 +951,8 @@ class DbUserDeviceKeysKey extends DataClass
other.content == this.content &&
other.verified == this.verified &&
other.blocked == this.blocked &&
other.lastActive == this.lastActive);
other.lastActive == this.lastActive &&
other.lastSentMessage == this.lastSentMessage);
}
class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
@ -943,6 +963,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
final Value<bool> verified;
final Value<bool> blocked;
final Value<int> lastActive;
final Value<String> lastSentMessage;
const UserDeviceKeysKeyCompanion({
this.clientId = const Value.absent(),
this.userId = const Value.absent(),
@ -951,6 +972,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
this.verified = const Value.absent(),
this.blocked = const Value.absent(),
this.lastActive = const Value.absent(),
this.lastSentMessage = const Value.absent(),
});
UserDeviceKeysKeyCompanion.insert({
@required int clientId,
@ -960,6 +982,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
this.verified = const Value.absent(),
this.blocked = const Value.absent(),
this.lastActive = const Value.absent(),
this.lastSentMessage = const Value.absent(),
}) : clientId = Value(clientId),
userId = Value(userId),
deviceId = Value(deviceId),
@ -972,6 +995,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
Expression<bool> verified,
Expression<bool> blocked,
Expression<int> lastActive,
Expression<String> lastSentMessage,
}) {
return RawValuesInsertable({
if (clientId != null) 'client_id': clientId,
@ -981,6 +1005,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
if (verified != null) 'verified': verified,
if (blocked != null) 'blocked': blocked,
if (lastActive != null) 'last_active': lastActive,
if (lastSentMessage != null) 'last_sent_message': lastSentMessage,
});
}
@ -991,7 +1016,8 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
Value<String> content,
Value<bool> verified,
Value<bool> blocked,
Value<int> lastActive}) {
Value<int> lastActive,
Value<String> lastSentMessage}) {
return UserDeviceKeysKeyCompanion(
clientId: clientId ?? this.clientId,
userId: userId ?? this.userId,
@ -1000,6 +1026,7 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
verified: verified ?? this.verified,
blocked: blocked ?? this.blocked,
lastActive: lastActive ?? this.lastActive,
lastSentMessage: lastSentMessage ?? this.lastSentMessage,
);
}
@ -1027,6 +1054,9 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
if (lastActive.present) {
map['last_active'] = Variable<int>(lastActive.value);
}
if (lastSentMessage.present) {
map['last_sent_message'] = Variable<String>(lastSentMessage.value);
}
return map;
}
@ -1039,7 +1069,8 @@ class UserDeviceKeysKeyCompanion extends UpdateCompanion<DbUserDeviceKeysKey> {
..write('content: $content, ')
..write('verified: $verified, ')
..write('blocked: $blocked, ')
..write('lastActive: $lastActive')
..write('lastActive: $lastActive, ')
..write('lastSentMessage: $lastSentMessage')
..write(')'))
.toString();
}
@ -1108,9 +1139,27 @@ class UserDeviceKeysKey extends Table
$customConstraints: '');
}
final VerificationMeta _lastSentMessageMeta =
const VerificationMeta('lastSentMessage');
GeneratedTextColumn _lastSentMessage;
GeneratedTextColumn get lastSentMessage =>
_lastSentMessage ??= _constructLastSentMessage();
GeneratedTextColumn _constructLastSentMessage() {
return GeneratedTextColumn('last_sent_message', $tableName, true,
$customConstraints: '');
}
@override
List<GeneratedColumn> get $columns =>
[clientId, userId, deviceId, content, verified, blocked, lastActive];
List<GeneratedColumn> get $columns => [
clientId,
userId,
deviceId,
content,
verified,
blocked,
lastActive,
lastSentMessage
];
@override
UserDeviceKeysKey get asDslTable => this;
@override
@ -1161,6 +1210,12 @@ class UserDeviceKeysKey extends Table
lastActive.isAcceptableOrUnknown(
data['last_active'], _lastActiveMeta));
}
if (data.containsKey('last_sent_message')) {
context.handle(
_lastSentMessageMeta,
lastSentMessage.isAcceptableOrUnknown(
data['last_sent_message'], _lastSentMessageMeta));
}
return context;
}
@ -6136,6 +6191,10 @@ abstract class _$Database extends GeneratedDatabase {
Index get olmSessionsIndex => _olmSessionsIndex ??= Index(
'olm_sessions_index',
'CREATE INDEX olm_sessions_index ON olm_sessions(client_id);');
Index _olmSessionsIdentityIndex;
Index get olmSessionsIdentityIndex => _olmSessionsIdentityIndex ??= Index(
'olm_sessions_identity_index',
'CREATE INDEX olm_sessions_identity_index ON olm_sessions(client_id, identity_key);');
OutboundGroupSessions _outboundGroupSessions;
OutboundGroupSessions get outboundGroupSessions =>
_outboundGroupSessions ??= OutboundGroupSessions(this);
@ -6574,6 +6633,35 @@ abstract class _$Database extends GeneratedDatabase {
);
}
Future<int> setLastSentMessageUserDeviceKey(String last_sent_message,
int client_id, String user_id, String device_id) {
return customUpdate(
'UPDATE user_device_keys_key SET last_sent_message = :last_sent_message WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id',
variables: [
Variable.withString(last_sent_message),
Variable.withInt(client_id),
Variable.withString(user_id),
Variable.withString(device_id)
],
updates: {userDeviceKeysKey},
updateKind: UpdateKind.update,
);
}
Selectable<String> getLastSentMessageUserDeviceKey(
int client_id, String user_id, String device_id) {
return customSelect(
'SELECT last_sent_message FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id',
variables: [
Variable.withInt(client_id),
Variable.withString(user_id),
Variable.withString(device_id)
],
readsFrom: {
userDeviceKeysKey
}).map((QueryRow row) => row.readString('last_sent_message'));
}
Future<int> setVerifiedUserCrossSigningKey(
bool verified, int client_id, String user_id, String public_key) {
return customUpdate(
@ -7068,6 +7156,7 @@ abstract class _$Database extends GeneratedDatabase {
userCrossSigningKeysIndex,
olmSessions,
olmSessionsIndex,
olmSessionsIdentityIndex,
outboundGroupSessions,
outboundGroupSessionsIndex,
inboundGroupSessions,

View File

@ -29,6 +29,7 @@ CREATE TABLE user_device_keys_key (
verified BOOLEAN DEFAULT false,
blocked BOOLEAN DEFAULT false,
last_active BIGINT,
last_sent_message TEXT,
UNIQUE(client_id, user_id, device_id)
) as DbUserDeviceKeysKey;
CREATE INDEX user_device_keys_key_index ON user_device_keys_key(client_id);
@ -53,6 +54,7 @@ CREATE TABLE olm_sessions (
UNIQUE(client_id, identity_key, session_id)
) AS DbOlmSessions;
CREATE INDEX olm_sessions_index ON olm_sessions(client_id);
CREATE INDEX olm_sessions_identity_index ON olm_sessions(client_id, identity_key);
CREATE TABLE outbound_group_sessions (
client_id INTEGER NOT NULL REFERENCES clients(client_id),
@ -204,6 +206,8 @@ setBlockedUserDeviceKey: UPDATE user_device_keys_key SET blocked = :blocked WHER
storeUserDeviceKey: INSERT OR REPLACE INTO user_device_keys_key (client_id, user_id, device_id, content, verified, blocked, last_active) VALUES (:client_id, :user_id, :device_id, :content, :verified, :blocked, :last_active);
removeUserDeviceKey: DELETE FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
setLastActiveUserDeviceKey: UPDATE user_device_keys_key SET last_active = :last_active WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
setLastSentMessageUserDeviceKey: UPDATE user_device_keys_key SET last_sent_message = :last_sent_message WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
getLastSentMessageUserDeviceKey: SELECT last_sent_message FROM user_device_keys_key WHERE client_id = :client_id AND user_id = :user_id AND device_id = :device_id;
setVerifiedUserCrossSigningKey: UPDATE user_cross_signing_keys SET verified = :verified WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
setBlockedUserCrossSigningKey: UPDATE user_cross_signing_keys SET blocked = :blocked WHERE client_id = :client_id AND user_id = :user_id AND public_key = :public_key;
storeUserCrossSigningKey: INSERT OR REPLACE INTO user_cross_signing_keys (client_id, user_id, public_key, content, verified, blocked) VALUES (:client_id, :user_id, :public_key, :content, :verified, :blocked);

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
@ -82,12 +82,6 @@ void main() {
.encryptToDeviceMessage([device], 'm.to_device', {'hello': 'foxies'});
});
test('encryptToDeviceMessagePayload', () async {
// just a hard test if nothing errors
await otherClient.encryption.encryptToDeviceMessagePayload(
device, 'm.to_device', {'hello': 'foxies'});
});
test('decryptToDeviceEvent', () async {
final encryptedEvent = ToDeviceEvent(
sender: '@othertest:fakeServer.notExisting',

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,6 +1,6 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Copyright (C) 2020 Famedly GmbH
* Famedly Matrix SDK
* Copyright (C) 2020, 2021 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -126,6 +126,88 @@ void main() {
true);
});
test('replay to_device events', () async {
final userId = '@alice:example.com';
final deviceId = 'JLAFKJWSCS';
final senderKey = 'L+4+JCl8MD63dgo8z5Ta+9QAHXiANyOVSfgbHA5d3H8';
FakeMatrixApi.calledEndpoints.clear();
await client.database.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
client.id,
userId,
deviceId);
var event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: {
'sender_key': senderKey,
},
);
await client.encryption.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
true);
// fail scenarios
// not encrypted
FakeMatrixApi.calledEndpoints.clear();
await client.database.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
client.id,
userId,
deviceId);
event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: null,
);
await client.encryption.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
// device not found
FakeMatrixApi.calledEndpoints.clear();
await client.database.setLastSentMessageUserDeviceKey(
json.encode({
'type': 'm.foxies',
'content': {
'floof': 'foxhole',
},
}),
client.id,
userId,
deviceId);
event = ToDeviceEvent(
sender: userId,
type: 'm.dummy',
content: {},
encryptedContent: {
'sender_key': 'invalid',
},
);
await client.encryption.olmManager.handleToDeviceEvent(event);
expect(
FakeMatrixApi.calledEndpoints.keys.any(
(k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
false);
});
test('dispose client', () async {
await client.dispose(closeDatabase: true);
});

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,5 @@
/*
* Ansible inventory script used at Famedly GmbH for managing many hosts
* Famedly Matrix SDK
* Copyright (C) 2019, 2020 Famedly GmbH
*
* This program is free software: you can redistribute it and/or modify