Browse Source

Merge pull request #980 from nextcloud/feature/nextcloud/spreed

feat(nextcloud): Add spreed
pull/1061/head
Kate 1 year ago committed by GitHub
parent
commit
c5772fabba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      .cspell/nextcloud.txt
  2. 4
      .cspell/tools.txt
  3. 3
      .gitmodules
  4. 7
      CONTRIBUTING.md
  5. 1
      external/nextcloud-spreed
  6. 3
      packages/nextcloud/lib/ids.dart
  7. 14
      packages/nextcloud/lib/spreed.dart
  8. 436
      packages/nextcloud/lib/src/api/core.openapi.dart
  9. 15730
      packages/nextcloud/lib/src/api/core.openapi.g.dart
  10. 184
      packages/nextcloud/lib/src/api/core.openapi.json
  11. 25107
      packages/nextcloud/lib/src/api/spreed.openapi.dart
  12. 52860
      packages/nextcloud/lib/src/api/spreed.openapi.g.dart
  13. 16787
      packages/nextcloud/lib/src/api/spreed.openapi.json
  14. 203
      packages/nextcloud/lib/src/helpers/spreed.dart
  15. 88
      packages/nextcloud/lib/src/patches/spreed/compatibility.json
  16. 12
      packages/nextcloud/lib/src/patches/spreed/oneof-workaround.json
  17. 10
      packages/nextcloud/test/core_test.dart
  18. 9
      packages/nextcloud/test/dashboard_test.dart
  19. 8
      packages/nextcloud/test/helper.dart
  20. 2
      packages/nextcloud/test/provisioning_api_test.dart
  21. 423
      packages/nextcloud/test/spreed_test.dart
  22. 4
      tool/Dockerfile.dev
  23. 19
      tool/generate-specs.sh
  24. 2
      tool/update-cspell-dictionaries.sh

14
.cspell/nextcloud.txt

@ -10,11 +10,14 @@ commenters
csapi csapi
datetime datetime
deletedshares deletedshares
dialin
dialout
displayname displayname
etag etag
fediverse fediverse
getapppassword getapppassword
groupid groupid
hostedsignalingserver
hovercard hovercard
iscustomavatar iscustomavatar
itemsperpage itemsperpage
@ -24,6 +27,7 @@ lastmod
licence licence
logfile logfile
logoheader logoheader
matterbridge
mimetypes mimetypes
mountpoint mountpoint
navigations navigations
@ -34,12 +38,14 @@ organisation
prio prio
productname productname
publicpreview publicpreview
publicshare
publicshareauth
replyable
requesttrial
reshares reshares
resharing resharing
rgdnvw rgdnvw
r'sharebymail setsip
r'updatenotification
r'uppush
shareapi shareapi
sharebymail sharebymail
sharee sharee
@ -47,6 +53,7 @@ shareesapi
shareinfo shareinfo
statuscode statuscode
stime stime
stunservers
stylesheet stylesheet
subadmin subadmin
subadmins subadmins
@ -57,6 +64,7 @@ textprocessing
totalitems totalitems
transferownership transferownership
trashbin trashbin
turnservers
undelete undelete
unifiedpush unifiedpush
unsharing unsharing

4
.cspell/tools.txt

@ -30,6 +30,7 @@ icudtl
inkscape inkscape
intellij intellij
jetbrains jetbrains
jsonpatch
jvmargs jvmargs
klass klass
lcov lcov
@ -43,12 +44,15 @@ libsqlite
mipmap mipmap
ndebug ndebug
nproc nproc
openrelay
openrelayprojectsecret
plantuml plantuml
precache precache
puml puml
rpath rpath
signoff signoff
startuml startuml
staticauth
stdlib stdlib
strconcat strconcat
strdupv strdupv

3
.gitmodules vendored

@ -10,3 +10,6 @@
[submodule "external/flathub-shared-modules"] [submodule "external/flathub-shared-modules"]
path = external/flathub-shared-modules path = external/flathub-shared-modules
url = https://github.com/flathub/shared-modules.git url = https://github.com/flathub/shared-modules.git
[submodule "external/nextcloud-spreed"]
path = external/nextcloud-spreed
url = https://github.com/nextcloud/spreed

7
CONTRIBUTING.md

@ -11,11 +11,16 @@ Currently included are:
To set up all these tools run the `./tool/setup.sh` script. To set up all these tools run the `./tool/setup.sh` script.
Note that you need to have Dart installed and [`~/.pub-cache/bin/` needs to be in your PATH](https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path) before running the script. Note that you need to have Dart installed and [`~/.pub-cache/bin/` needs to be in your PATH](https://dart.dev/tools/pub/cmd/pub-global#running-a-script-from-your-path) before running the script.
You will need to have the following packages installed: You will need to have the following dependencies installed to get the app running:
- [yq](https://github.com/kislyuk/yq) - [yq](https://github.com/kislyuk/yq)
- [sqlite3](https://pub.dev/packages/sqflite_common_ffi#getting-started) - [sqlite3](https://pub.dev/packages/sqflite_common_ffi#getting-started)
- [appindicator3](https://pub.dev/packages/tray_manager#quick-start) - [appindicator3](https://pub.dev/packages/tray_manager#quick-start)
For working with lower levels like generating the OpenAPI specifications a few more dependencies are required:
- [jsonpatch](https://pypi.org/project/jsonpatch)
- [PHP](https://www.php.net)
- [composer](https://getcomposer.org)
## Picking an issue ## Picking an issue
You may wish to start with our list of [good first issues](https://github.com/nextcloud/neon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) You may wish to start with our list of [good first issues](https://github.com/nextcloud/neon/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)

1
external/nextcloud-spreed vendored

@ -0,0 +1 @@
Subproject commit ec8eb42fe3ef5d0f0247a3fd078099e49d0140c3

3
packages/nextcloud/lib/ids.dart

@ -48,6 +48,9 @@ final class AppIDs {
/// ID for the sharebymail app. /// ID for the sharebymail app.
static const sharebymail = 'sharebymail'; static const sharebymail = 'sharebymail';
/// ID for the spreed app.
static const spreed = 'spreed';
/// ID for the theming app. /// ID for the theming app.
static const theming = 'theming'; static const theming = 'theming';

14
packages/nextcloud/lib/spreed.dart

@ -0,0 +1,14 @@
// coverage:ignore-file
import 'package:nextcloud/src/api/spreed.openapi.dart';
import 'package:nextcloud/src/client.dart';
export 'src/api/spreed.openapi.dart';
export 'src/helpers/spreed.dart';
// ignore: public_member_api_docs
extension SpreedExtension on NextcloudClient {
static final _spreed = Expando<Client>();
/// Client for the spreed APIs
Client get spreed => _spreed[this] ??= Client.fromClient(this);
}

436
packages/nextcloud/lib/src/api/core.openapi.dart

@ -7405,6 +7405,392 @@ abstract class SharebymailCapabilities
static Serializer<SharebymailCapabilities> get serializer => _$sharebymailCapabilitiesSerializer; static Serializer<SharebymailCapabilities> get serializer => _$sharebymailCapabilitiesSerializer;
} }
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_AttachmentsInterface {
bool get allowed;
String? get folder;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Attachments
implements
SpreedPublicCapabilities0_Spreed_Config_AttachmentsInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Attachments,
SpreedPublicCapabilities0_Spreed_Config_AttachmentsBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Attachments([
final void Function(SpreedPublicCapabilities0_Spreed_Config_AttachmentsBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Attachments;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Attachments._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Attachments.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Attachments> get serializer =>
_$spreedPublicCapabilities0SpreedConfigAttachmentsSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_CallInterface {
bool get enabled;
@BuiltValueField(wireName: 'breakout-rooms')
bool get breakoutRooms;
bool get recording;
@BuiltValueField(wireName: 'recording-consent')
int? get recordingConsent;
@BuiltValueField(wireName: 'supported-reactions')
BuiltList<String> get supportedReactions;
@BuiltValueField(wireName: 'predefined-backgrounds')
BuiltList<String> get predefinedBackgrounds;
@BuiltValueField(wireName: 'can-upload-background')
bool get canUploadBackground;
@BuiltValueField(wireName: 'sip-enabled')
bool? get sipEnabled;
@BuiltValueField(wireName: 'sip-dialout-enabled')
bool? get sipDialoutEnabled;
@BuiltValueField(wireName: 'can-enable-sip')
bool? get canEnableSip;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Call
implements
SpreedPublicCapabilities0_Spreed_Config_CallInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Call, SpreedPublicCapabilities0_Spreed_Config_CallBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Call([
final void Function(SpreedPublicCapabilities0_Spreed_Config_CallBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Call;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Call._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Call.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Call> get serializer =>
_$spreedPublicCapabilities0SpreedConfigCallSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_ChatInterface {
@BuiltValueField(wireName: 'max-length')
int get maxLength;
@BuiltValueField(wireName: 'read-privacy')
int get readPrivacy;
@BuiltValueField(wireName: 'has-translation-providers')
bool? get hasTranslationProviders;
@BuiltValueField(wireName: 'typing-privacy')
int get typingPrivacy;
BuiltList<String>? get translations;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Chat
implements
SpreedPublicCapabilities0_Spreed_Config_ChatInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Chat, SpreedPublicCapabilities0_Spreed_Config_ChatBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Chat([
final void Function(SpreedPublicCapabilities0_Spreed_Config_ChatBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Chat;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Chat._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Chat.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Chat> get serializer =>
_$spreedPublicCapabilities0SpreedConfigChatSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_ConversationsInterface {
@BuiltValueField(wireName: 'can-create')
bool get canCreate;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Conversations
implements
SpreedPublicCapabilities0_Spreed_Config_ConversationsInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Conversations,
SpreedPublicCapabilities0_Spreed_Config_ConversationsBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Conversations([
final void Function(SpreedPublicCapabilities0_Spreed_Config_ConversationsBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Conversations;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Conversations._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Conversations.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Conversations> get serializer =>
_$spreedPublicCapabilities0SpreedConfigConversationsSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_PreviewsInterface {
@BuiltValueField(wireName: 'max-gif-size')
int get maxGifSize;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Previews
implements
SpreedPublicCapabilities0_Spreed_Config_PreviewsInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Previews,
SpreedPublicCapabilities0_Spreed_Config_PreviewsBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Previews([
final void Function(SpreedPublicCapabilities0_Spreed_Config_PreviewsBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Previews;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Previews._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Previews.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Previews> get serializer =>
_$spreedPublicCapabilities0SpreedConfigPreviewsSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_Config_SignalingInterface {
@BuiltValueField(wireName: 'session-ping-limit')
int get sessionPingLimit;
@BuiltValueField(wireName: 'hello-v2-token-key')
String? get helloV2TokenKey;
}
abstract class SpreedPublicCapabilities0_Spreed_Config_Signaling
implements
SpreedPublicCapabilities0_Spreed_Config_SignalingInterface,
Built<SpreedPublicCapabilities0_Spreed_Config_Signaling,
SpreedPublicCapabilities0_Spreed_Config_SignalingBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config_Signaling([
final void Function(SpreedPublicCapabilities0_Spreed_Config_SignalingBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config_Signaling;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config_Signaling._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config_Signaling.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config_Signaling> get serializer =>
_$spreedPublicCapabilities0SpreedConfigSignalingSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_Spreed_ConfigInterface {
SpreedPublicCapabilities0_Spreed_Config_Attachments get attachments;
SpreedPublicCapabilities0_Spreed_Config_Call get call;
SpreedPublicCapabilities0_Spreed_Config_Chat get chat;
SpreedPublicCapabilities0_Spreed_Config_Conversations get conversations;
SpreedPublicCapabilities0_Spreed_Config_Previews get previews;
SpreedPublicCapabilities0_Spreed_Config_Signaling get signaling;
}
abstract class SpreedPublicCapabilities0_Spreed_Config
implements
SpreedPublicCapabilities0_Spreed_ConfigInterface,
Built<SpreedPublicCapabilities0_Spreed_Config, SpreedPublicCapabilities0_Spreed_ConfigBuilder> {
factory SpreedPublicCapabilities0_Spreed_Config([
final void Function(SpreedPublicCapabilities0_Spreed_ConfigBuilder)? b,
]) = _$SpreedPublicCapabilities0_Spreed_Config;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed_Config._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed_Config.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed_Config> get serializer =>
_$spreedPublicCapabilities0SpreedConfigSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0_SpreedInterface {
BuiltList<String> get features;
SpreedPublicCapabilities0_Spreed_Config get config;
String get version;
}
abstract class SpreedPublicCapabilities0_Spreed
implements
SpreedPublicCapabilities0_SpreedInterface,
Built<SpreedPublicCapabilities0_Spreed, SpreedPublicCapabilities0_SpreedBuilder> {
factory SpreedPublicCapabilities0_Spreed([final void Function(SpreedPublicCapabilities0_SpreedBuilder)? b]) =
_$SpreedPublicCapabilities0_Spreed;
// coverage:ignore-start
const SpreedPublicCapabilities0_Spreed._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0_Spreed.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0_Spreed> get serializer => _$spreedPublicCapabilities0SpreedSerializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilities0Interface {
SpreedPublicCapabilities0_Spreed get spreed;
}
abstract class SpreedPublicCapabilities0
implements SpreedPublicCapabilities0Interface, Built<SpreedPublicCapabilities0, SpreedPublicCapabilities0Builder> {
factory SpreedPublicCapabilities0([final void Function(SpreedPublicCapabilities0Builder)? b]) =
_$SpreedPublicCapabilities0;
// coverage:ignore-start
const SpreedPublicCapabilities0._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities0.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
static Serializer<SpreedPublicCapabilities0> get serializer => _$spreedPublicCapabilities0Serializer;
}
@BuiltValue(instantiable: false)
abstract interface class SpreedPublicCapabilitiesInterface {
SpreedPublicCapabilities0? get spreedPublicCapabilities0;
BuiltList<JsonObject>? get builtListJsonObject;
}
abstract class SpreedPublicCapabilities
implements SpreedPublicCapabilitiesInterface, Built<SpreedPublicCapabilities, SpreedPublicCapabilitiesBuilder> {
factory SpreedPublicCapabilities([final void Function(SpreedPublicCapabilitiesBuilder)? b]) =
_$SpreedPublicCapabilities;
// coverage:ignore-start
const SpreedPublicCapabilities._();
// coverage:ignore-end
// coverage:ignore-start
factory SpreedPublicCapabilities.fromJson(final Map<String, dynamic> json) =>
_jsonSerializers.deserializeWith(serializer, json)!;
// coverage:ignore-end
// coverage:ignore-start
Map<String, dynamic> toJson() => _jsonSerializers.serializeWith(serializer, this)! as Map<String, dynamic>;
// coverage:ignore-end
@BuiltValueSerializer(custom: true)
static Serializer<SpreedPublicCapabilities> get serializer => _$SpreedPublicCapabilitiesSerializer();
JsonObject get data;
@BuiltValueHook(finalizeBuilder: true)
static void _validate(final SpreedPublicCapabilitiesBuilder b) {
// When this is rebuild from another builder
if (b._data == null) {
return;
}
final match = [b._spreedPublicCapabilities0, b._builtListJsonObject].firstWhereOrNull((final x) => x != null);
if (match == null) {
throw StateError("Need at least one of 'spreedPublicCapabilities0', 'builtListJsonObject' for ${b._data}");
}
}
}
class _$SpreedPublicCapabilitiesSerializer implements PrimitiveSerializer<SpreedPublicCapabilities> {
@override
final Iterable<Type> types = const [SpreedPublicCapabilities, _$SpreedPublicCapabilities];
@override
final String wireName = 'SpreedPublicCapabilities';
@override
Object serialize(
final Serializers serializers,
final SpreedPublicCapabilities object, {
final FullType specifiedType = FullType.unspecified,
}) =>
object.data.value;
@override
SpreedPublicCapabilities deserialize(
final Serializers serializers,
final Object data, {
final FullType specifiedType = FullType.unspecified,
}) {
final result = SpreedPublicCapabilitiesBuilder()..data = JsonObject(data);
try {
final value = _jsonSerializers.deserialize(data, specifiedType: const FullType(SpreedPublicCapabilities0))!
as SpreedPublicCapabilities0;
result.spreedPublicCapabilities0.replace(value);
} catch (_) {}
try {
final value = _jsonSerializers.deserialize(
data,
specifiedType: const FullType(BuiltList, [FullType(JsonObject)]),
)! as BuiltList<JsonObject>;
result.builtListJsonObject.replace(value);
} catch (_) {}
return result.build();
}
}
@BuiltValue(instantiable: false) @BuiltValue(instantiable: false)
abstract interface class ThemingPublicCapabilities_ThemingInterface { abstract interface class ThemingPublicCapabilities_ThemingInterface {
String get name; String get name;
@ -7604,6 +7990,7 @@ abstract interface class OcsGetCapabilitiesResponseApplicationJson_Ocs_Data_Capa
NotificationsCapabilities? get notificationsCapabilities; NotificationsCapabilities? get notificationsCapabilities;
ProvisioningApiCapabilities? get provisioningApiCapabilities; ProvisioningApiCapabilities? get provisioningApiCapabilities;
SharebymailCapabilities? get sharebymailCapabilities; SharebymailCapabilities? get sharebymailCapabilities;
SpreedPublicCapabilities? get spreedPublicCapabilities;
ThemingPublicCapabilities? get themingPublicCapabilities; ThemingPublicCapabilities? get themingPublicCapabilities;
UserStatusCapabilities? get userStatusCapabilities; UserStatusCapabilities? get userStatusCapabilities;
WeatherStatusCapabilities? get weatherStatusCapabilities; WeatherStatusCapabilities? get weatherStatusCapabilities;
@ -7654,13 +8041,14 @@ abstract class OcsGetCapabilitiesResponseApplicationJson_Ocs_Data_Capabilities
b._notificationsCapabilities, b._notificationsCapabilities,
b._provisioningApiCapabilities, b._provisioningApiCapabilities,
b._sharebymailCapabilities, b._sharebymailCapabilities,
b._spreedPublicCapabilities,
b._themingPublicCapabilities, b._themingPublicCapabilities,
b._userStatusCapabilities, b._userStatusCapabilities,
b._weatherStatusCapabilities, b._weatherStatusCapabilities,
].firstWhereOrNull((final x) => x != null); ].firstWhereOrNull((final x) => x != null);
if (match == null) { if (match == null) {
throw StateError( throw StateError(
"Need at least one of 'commentsCapabilities', 'davCapabilities', 'filesCapabilities', 'filesSharingCapabilities', 'filesTrashbinCapabilities', 'filesVersionsCapabilities', 'notesCapabilities', 'notificationsCapabilities', 'provisioningApiCapabilities', 'sharebymailCapabilities', 'themingPublicCapabilities', 'userStatusCapabilities', 'weatherStatusCapabilities' for ${b._data}", "Need at least one of 'commentsCapabilities', 'davCapabilities', 'filesCapabilities', 'filesSharingCapabilities', 'filesTrashbinCapabilities', 'filesVersionsCapabilities', 'notesCapabilities', 'notificationsCapabilities', 'provisioningApiCapabilities', 'sharebymailCapabilities', 'spreedPublicCapabilities', 'themingPublicCapabilities', 'userStatusCapabilities', 'weatherStatusCapabilities' for ${b._data}",
); );
} }
} }
@ -7742,6 +8130,11 @@ class _$OcsGetCapabilitiesResponseApplicationJson_Ocs_Data_CapabilitiesSerialize
as SharebymailCapabilities; as SharebymailCapabilities;
result.sharebymailCapabilities.replace(value); result.sharebymailCapabilities.replace(value);
} catch (_) {} } catch (_) {}
try {
final value = _jsonSerializers.deserialize(data, specifiedType: const FullType(SpreedPublicCapabilities))!
as SpreedPublicCapabilities;
result.spreedPublicCapabilities.replace(value);
} catch (_) {}
try { try {
final value = _jsonSerializers.deserialize(data, specifiedType: const FullType(ThemingPublicCapabilities))! final value = _jsonSerializers.deserialize(data, specifiedType: const FullType(ThemingPublicCapabilities))!
as ThemingPublicCapabilities; as ThemingPublicCapabilities;
@ -10132,6 +10525,47 @@ final Serializers _serializers = (Serializers().toBuilder()
SharebymailCapabilities_FilesSharing_Sharebymail_ExpireDate.new, SharebymailCapabilities_FilesSharing_Sharebymail_ExpireDate.new,
) )
..add(SharebymailCapabilities_FilesSharing_Sharebymail_ExpireDate.serializer) ..add(SharebymailCapabilities_FilesSharing_Sharebymail_ExpireDate.serializer)
..addBuilderFactory(const FullType(SpreedPublicCapabilities), SpreedPublicCapabilities.new)
..add(SpreedPublicCapabilities.serializer)
..addBuilderFactory(const FullType(SpreedPublicCapabilities0), SpreedPublicCapabilities0.new)
..add(SpreedPublicCapabilities0.serializer)
..addBuilderFactory(const FullType(SpreedPublicCapabilities0_Spreed), SpreedPublicCapabilities0_Spreed.new)
..add(SpreedPublicCapabilities0_Spreed.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config),
SpreedPublicCapabilities0_Spreed_Config.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Attachments),
SpreedPublicCapabilities0_Spreed_Config_Attachments.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Attachments.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Call),
SpreedPublicCapabilities0_Spreed_Config_Call.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Call.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Chat),
SpreedPublicCapabilities0_Spreed_Config_Chat.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Chat.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Conversations),
SpreedPublicCapabilities0_Spreed_Config_Conversations.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Conversations.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Previews),
SpreedPublicCapabilities0_Spreed_Config_Previews.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Previews.serializer)
..addBuilderFactory(
const FullType(SpreedPublicCapabilities0_Spreed_Config_Signaling),
SpreedPublicCapabilities0_Spreed_Config_Signaling.new,
)
..add(SpreedPublicCapabilities0_Spreed_Config_Signaling.serializer)
..addBuilderFactory(const FullType(ThemingPublicCapabilities), ThemingPublicCapabilities.new) ..addBuilderFactory(const FullType(ThemingPublicCapabilities), ThemingPublicCapabilities.new)
..add(ThemingPublicCapabilities.serializer) ..add(ThemingPublicCapabilities.serializer)
..addBuilderFactory(const FullType(ThemingPublicCapabilities_Theming), ThemingPublicCapabilities_Theming.new) ..addBuilderFactory(const FullType(ThemingPublicCapabilities_Theming), ThemingPublicCapabilities_Theming.new)

15730
packages/nextcloud/lib/src/api/core.openapi.g.dart

File diff suppressed because it is too large Load Diff

184
packages/nextcloud/lib/src/api/core.openapi.json

@ -1066,6 +1066,187 @@
} }
} }
}, },
"SpreedPublicCapabilities": {
"anyOf": [
{
"type": "object",
"required": [
"spreed"
],
"properties": {
"spreed": {
"type": "object",
"required": [
"features",
"config",
"version"
],
"properties": {
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"config": {
"type": "object",
"required": [
"attachments",
"call",
"chat",
"conversations",
"previews",
"signaling"
],
"properties": {
"attachments": {
"type": "object",
"required": [
"allowed"
],
"properties": {
"allowed": {
"type": "boolean"
},
"folder": {
"type": "string"
}
}
},
"call": {
"type": "object",
"required": [
"enabled",
"breakout-rooms",
"recording",
"supported-reactions",
"predefined-backgrounds",
"can-upload-background"
],
"properties": {
"enabled": {
"type": "boolean"
},
"breakout-rooms": {
"type": "boolean"
},
"recording": {
"type": "boolean"
},
"recording-consent": {
"type": "integer",
"format": "int64"
},
"supported-reactions": {
"type": "array",
"items": {
"type": "string"
}
},
"predefined-backgrounds": {
"type": "array",
"items": {
"type": "string"
}
},
"can-upload-background": {
"type": "boolean"
},
"sip-enabled": {
"type": "boolean"
},
"sip-dialout-enabled": {
"type": "boolean"
},
"can-enable-sip": {
"type": "boolean"
}
}
},
"chat": {
"type": "object",
"required": [
"max-length",
"read-privacy",
"typing-privacy"
],
"properties": {
"max-length": {
"type": "integer",
"format": "int64"
},
"read-privacy": {
"type": "integer",
"format": "int64"
},
"has-translation-providers": {
"type": "boolean"
},
"typing-privacy": {
"type": "integer",
"format": "int64"
},
"translations": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"conversations": {
"type": "object",
"required": [
"can-create"
],
"properties": {
"can-create": {
"type": "boolean"
}
}
},
"previews": {
"type": "object",
"required": [
"max-gif-size"
],
"properties": {
"max-gif-size": {
"type": "integer",
"format": "int64"
}
}
},
"signaling": {
"type": "object",
"required": [
"session-ping-limit"
],
"properties": {
"session-ping-limit": {
"type": "integer",
"format": "int64"
},
"hello-v2-token-key": {
"type": "string"
}
}
}
}
},
"version": {
"type": "string"
}
}
}
}
},
{
"type": "array",
"maxLength": 0
}
]
},
"ThemingPublicCapabilities": { "ThemingPublicCapabilities": {
"type": "object", "type": "object",
"required": [ "required": [
@ -2191,6 +2372,9 @@
{ {
"$ref": "#/components/schemas/SharebymailCapabilities" "$ref": "#/components/schemas/SharebymailCapabilities"
}, },
{
"$ref": "#/components/schemas/SpreedPublicCapabilities"
},
{ {
"$ref": "#/components/schemas/ThemingPublicCapabilities" "$ref": "#/components/schemas/ThemingPublicCapabilities"
}, },

25107
packages/nextcloud/lib/src/api/spreed.openapi.dart

File diff suppressed because it is too large Load Diff

52860
packages/nextcloud/lib/src/api/spreed.openapi.g.dart

File diff suppressed because it is too large Load Diff

16787
packages/nextcloud/lib/src/api/spreed.openapi.json

File diff suppressed because it is too large Load Diff

203
packages/nextcloud/lib/src/helpers/spreed.dart

@ -0,0 +1,203 @@
import 'package:nextcloud/nextcloud.dart';
import 'package:nextcloud/src/api/core.openapi.dart' as core;
import 'package:nextcloud/src/api/spreed.openapi.dart' as spreed;
import 'package:version/version.dart';
/// The version of the spreed app that is supported.
const supportedVersion = 17;
/// Extension for checking whether spreed is supported.
extension SpreedVersionSupported on spreed.Client {
/// Checks whether the spreed app installed on the server is supported by this client.
///
/// Also returns the supported version number.
VersionSupported<int> isSupported(final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities) {
final version = capabilities.capabilities.spreedPublicCapabilities?.spreedPublicCapabilities0?.spreed.version;
return (
isSupported: version != null && Version.parse(version).major == supportedVersion,
minimumVersion: supportedVersion,
);
}
}
/// Conversation types.
///
/// Use [value] to get the integer representation that is used in the API.
/// See https://github.com/nextcloud/spreed/blob/master/lib/Room.php.
enum RoomType {
/// Room between two participants.
oneToOne,
/// Room with multiple participants.
group,
/// Public room with multiple participants.
public,
/// Room with the changelog bots that posts a message when a new Talk release was installed.
changelog,
/// Room that previously was a [oneToOne]. The user has been deleted on the server or removed from all rooms.
oneToOneFormer,
/// Room to send messages to yourself for note keeping.
noteToSelf;
/// Integer representation of the [ParticipantType].
int get value => index + 1;
}
/// Types of chat messages.
///
/// Use [name] to get the string representation that is used in the API.
/// See https://github.com/nextcloud/spreed/blob/master/lib/Chat/ChatManager.php.
enum MessageType {
/// Message.
comment,
/// Message from the system.
system,
/// An object shared to the room.
// ignore: constant_identifier_names
object_shared,
/// Message from a command.
command,
/// Deleted message.
// ignore: constant_identifier_names
comment_deleted,
/// Emoji reaction.
reaction,
/// Deleted emoji reaction.
// ignore: constant_identifier_names
reaction_deleted;
}
/// Actor types of chat messages.
///
/// Use [name] to get the string representation that is used in the API.
/// See https://github.com/nextcloud/spreed/blob/master/lib/Model/Attendee.php.
enum ActorType {
/// Logged-in users.
users,
/// Groups.
groups,
/// Guest users.
guests,
/// E-mails.
emails,
/// Circles.
circles,
/// Users whose messages are bridged in by the Matterbridge integration.
bridged,
/// Used by commands and the changelog conversation.
bots,
/// Users from other instances.
// ignore: constant_identifier_names
federated_users,
/// Users from SIP.
phones;
}
/// Participant types.
///
/// Use [value] to get the integer representation that is used in the API.
/// See https://github.com/nextcloud/spreed/blob/master/lib/Participant.php.
enum ParticipantType {
/// Owner.
owner,
/// Moderator.
moderator,
/// User.
user,
/// Guest.
guest,
/// User following a public link.
userFollowingPublicLink,
/// Guest with moderator permissions.
guestWithModeratorPermissions;
/// Integer representation of the [ParticipantType].
int get value => index + 1;
}
/// Attendee permissions.
///
/// Use [fromValue] to convert the integer representation into this enum representation.
/// Use [value] to get the integer representation that is used in the API.
/// Use [ParticipantPermissionsValue.value] to convert multiple [ParticipantPermission] into the integer representation.
///
/// See https://github.com/nextcloud/spreed/blob/master/lib/Model/Attendee.php.
enum ParticipantPermission {
/// Default permissions.
$default,
/// Custom permissions.
custom,
/// Start call.
startCall,
/// Join call.
joinCall,
/// Can ignore lobby.
canIgnoreLobby,
/// Can publish audio stream.
canPublishAudio,
/// Can publish video stream.
canPublishVideo,
/// Can publish screen sharing stream.
canScreenShare,
/// Can post chat message, share items and do reactions.
canSendMessageAndShareAndReact;
/// Integer representation of the [ParticipantPermission].
int get value => index == 0 ? 0 : 1 << (index - 1);
/// Converts the integer representation of multiple [ParticipantPermission]s to the corresponding [ParticipantPermission]s.
static Set<ParticipantPermission> fromValue(final int value) {
final permissions = <ParticipantPermission>{};
var v = value;
for (var i = 1; i <= ParticipantPermission.values.length - 1; i++) {
if (v.isOdd) {
permissions.add(ParticipantPermission.values[i]);
}
v = v >> 1;
}
if (permissions.isEmpty) {
permissions.add(ParticipantPermission.$default);
}
return permissions;
}
}
/// Extension for the integer representation of multiple [ParticipantPermission]s.
extension ParticipantPermissionsValue on Set<ParticipantPermission> {
/// Gets the integer representation of multiple [ParticipantPermission]s.
int get value => map((final p) => p.value).reduce((final a, final b) => a | b);
}

88
packages/nextcloud/lib/src/patches/spreed/compatibility.json

@ -0,0 +1,88 @@
[
{
"op": "replace",
"path": "/components/schemas/PublicCapabilities/oneOf/0/properties/spreed/properties/config/properties/call/required",
"value": [
"enabled",
"breakout-rooms",
"recording",
"supported-reactions",
"predefined-backgrounds",
"can-upload-background"
]
},
{
"op": "replace",
"path": "/components/schemas/PublicCapabilities/oneOf/0/properties/spreed/properties/config/properties/chat/required",
"value": [
"max-length",
"read-privacy",
"typing-privacy"
]
},
{
"op": "add",
"path": "/components/schemas/PublicCapabilities/oneOf/0/properties/spreed/properties/config/properties/chat/properties/translations",
"value": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"op": "replace",
"path": "/components/schemas/Room/required",
"value": [
"actorId",
"actorType",
"attendeeId",
"attendeePermissions",
"attendeePin",
"avatarVersion",
"breakoutRoomMode",
"breakoutRoomStatus",
"callFlag",
"callPermissions",
"callRecording",
"callStartTime",
"canDeleteConversation",
"canEnableSIP",
"canLeaveConversation",
"canStartCall",
"defaultPermissions",
"description",
"displayName",
"hasCall",
"hasPassword",
"id",
"isCustomAvatar",
"isFavorite",
"lastActivity",
"lastCommonReadMessage",
"lastMessage",
"lastPing",
"lastReadMessage",
"listable",
"lobbyState",
"lobbyTimer",
"messageExpiration",
"name",
"notificationCalls",
"notificationLevel",
"objectId",
"objectType",
"participantFlags",
"participantType",
"permissions",
"readOnly",
"sessionId",
"sipEnabled",
"token",
"type",
"unreadMention",
"unreadMentionDirect",
"unreadMessages"
]
}
]

12
packages/nextcloud/lib/src/patches/spreed/oneof-workaround.json

@ -0,0 +1,12 @@
[
{
"op": "move",
"from": "/components/schemas/PublicCapabilities/oneOf",
"path": "/components/schemas/PublicCapabilities/anyOf"
},
{
"op": "move",
"from": "/components/schemas/Room/properties/lastMessage/oneOf",
"path": "/components/schemas/Room/properties/lastMessage/anyOf"
}
]

10
packages/nextcloud/test/core_test.dart

@ -70,6 +70,7 @@ void main() {
expect(response.body.ocs.data.capabilities.notificationsCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.notificationsCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.provisioningApiCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.provisioningApiCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.sharebymailCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.sharebymailCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.spreedPublicCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.themingPublicCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.themingPublicCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.userStatusCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.userStatusCapabilities, isNotNull);
expect(response.body.ocs.data.capabilities.weatherStatusCapabilities, isNotNull); expect(response.body.ocs.data.capabilities.weatherStatusCapabilities, isNotNull);
@ -81,14 +82,15 @@ void main() {
final response = await client.core.navigation.getAppsNavigation(); final response = await client.core.navigation.getAppsNavigation();
expect(response.statusCode, 200); expect(response.statusCode, 200);
expect(() => response.headers, isA<void>()); expect(() => response.headers, isA<void>());
expect(response.body.ocs.data, hasLength(6)); expect(response.body.ocs.data, hasLength(7));
expect(response.body.ocs.data[0].id, 'dashboard'); expect(response.body.ocs.data[0].id, 'dashboard');
expect(response.body.ocs.data[1].id, 'files'); expect(response.body.ocs.data[1].id, 'files');
expect(response.body.ocs.data[2].id, 'photos'); expect(response.body.ocs.data[2].id, 'photos');
expect(response.body.ocs.data[3].id, 'activity'); expect(response.body.ocs.data[3].id, 'activity');
expect(response.body.ocs.data[4].id, 'notes'); expect(response.body.ocs.data[4].id, 'spreed');
expect(response.body.ocs.data[5].id, 'news'); expect(response.body.ocs.data[5].id, 'notes');
expect(response.body.ocs.data[6].id, 'news');
}); });
}); });
@ -151,7 +153,7 @@ void main() {
expect(response.statusCode, 200); expect(response.statusCode, 200);
expect(() => response.headers, isA<void>()); expect(() => response.headers, isA<void>());
expect(response.body.ocs.data, hasLength(14)); expect(response.body.ocs.data, hasLength(17));
}); });
test('Search', () async { test('Search', () async {

9
packages/nextcloud/test/dashboard_test.dart

@ -20,15 +20,16 @@ void main() {
test('Get widgets', () async { test('Get widgets', () async {
final response = await client.dashboard.dashboardApi.getWidgets(); final response = await client.dashboard.dashboardApi.getWidgets();
expect(response.body.ocs.data.keys, equals(['activity', 'notes', 'recommendations', 'user_status'])); expect(response.body.ocs.data.keys, equals(['activity', 'notes', 'recommendations', 'spreed', 'user_status']));
}); });
group('Get widget items', () { group('Get widget items', () {
test('v1', () async { test('v1', () async {
final response = await client.dashboard.dashboardApi.getWidgetItems(); final response = await client.dashboard.dashboardApi.getWidgetItems();
expect(response.body.ocs.data.keys, equals(['recommendations'])); final items = response.body.ocs.data;
final items = response.body.ocs.data['recommendations']!; expect(items.keys, equals(['recommendations', 'spreed']));
expect(items, hasLength(7)); expect(items['recommendations'], hasLength(7));
expect(items['spreed'], hasLength(0));
}); });
test('v2', () async { test('v2', () async {

8
packages/nextcloud/test/helper.dart

@ -140,12 +140,16 @@ Future<TestNextcloudClient> getTestClient(
try { try {
await client.core.getStatus(); await client.core.getStatus();
break; break;
} on DynamiteApiException catch (error) { } catch (error) {
if (error is HttpException || error is DynamiteApiException) {
i++; i++;
await Future<void>.delayed(const Duration(milliseconds: 100)); await Future<void>.delayed(const Duration(milliseconds: 100));
if (i >= 30) { if (i >= 300) {
throw TimeoutException('Failed to get the status of the Server. $error'); throw TimeoutException('Failed to get the status of the Server. $error');
} }
} else {
rethrow;
}
} }
} }

2
packages/nextcloud/test/provisioning_api_test.dart

@ -50,7 +50,7 @@ void main() {
final response = await client.provisioningApi.apps.getApps(); final response = await client.provisioningApi.apps.getApps();
expect(response.statusCode, 200); expect(response.statusCode, 200);
expect(() => response.headers, isA<void>()); expect(() => response.headers, isA<void>());
expect(response.body.ocs.data.apps, hasLength(40)); expect(response.body.ocs.data.apps, hasLength(41));
for (final id in response.body.ocs.data.apps) { for (final id in response.body.ocs.data.apps) {
final app = await client.provisioningApi.apps.getAppInfo(app: id); final app = await client.provisioningApi.apps.getAppInfo(app: id);

423
packages/nextcloud/test/spreed_test.dart

@ -0,0 +1,423 @@
import 'dart:async';
import 'dart:convert';
import 'package:built_value/json_object.dart';
import 'package:nextcloud/core.dart' as core;
import 'package:nextcloud/spreed.dart' as spreed;
import 'package:test/test.dart';
import 'helper.dart';
void main() {
group(
'spreed',
() {
late DockerImage image;
setUpAll(() async => image = await getDockerImage());
late DockerContainer container;
late TestNextcloudClient client1;
setUp(() async {
container = await getDockerContainer(
image,
useApache: true,
);
client1 = await getTestClient(container);
});
tearDown(() => container.destroy());
Future<spreed.Room> createTestRoom() async => (await client1.spreed.room.createRoom(
roomType: spreed.RoomType.public.value,
roomName: 'Test',
))
.body
.ocs
.data;
group('Helpers', () {
test('Is supported', () async {
final response = await client1.core.ocs.getCapabilities();
expect(response.statusCode, 200);
expect(() => response.headers, isA<void>());
final result = client1.spreed.isSupported(response.body.ocs.data);
expect(result.isSupported, isTrue);
});
test('Participant permissions', () async {
expect(spreed.ParticipantPermission.$default.value, 0);
expect(spreed.ParticipantPermission.fromValue(0), {spreed.ParticipantPermission.$default});
expect({spreed.ParticipantPermission.$default}.value, 0);
expect(spreed.ParticipantPermission.custom.value, 1);
expect(spreed.ParticipantPermission.canSendMessageAndShareAndReact.value, 128);
expect(spreed.ParticipantPermission.fromValue(129), {
spreed.ParticipantPermission.custom,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
});
expect(
{
spreed.ParticipantPermission.custom,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
}.value,
129,
);
});
});
group('Room', () {
test('Get rooms', () async {
final response = await client1.spreed.room.getRooms();
expect(response.body.ocs.data, hasLength(1));
expect(response.body.ocs.data[0].id, 1);
expect(response.body.ocs.data[0].token, isNotEmpty);
expect(response.body.ocs.data[0].type, spreed.RoomType.changelog.value);
expect(response.body.ocs.data[0].name, 'user1');
expect(response.body.ocs.data[0].displayName, 'Talk updates ✅');
expect(response.body.ocs.data[0].participantType, spreed.ParticipantType.user.value);
expect(spreed.ParticipantPermission.fromValue(response.body.ocs.data[0].permissions), {
spreed.ParticipantPermission.startCall,
spreed.ParticipantPermission.joinCall,
spreed.ParticipantPermission.canPublishAudio,
spreed.ParticipantPermission.canPublishVideo,
spreed.ParticipantPermission.canScreenShare,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
});
});
test('Session', () async {
var room = await createTestRoom();
expect(room.sessionId, '0');
final response = await client1.spreed.room.joinRoom(token: room.token);
expect(response.body.ocs.data.id, room.id);
expect(response.body.ocs.data.sessionId, isNot(room.sessionId));
room = (await client1.spreed.room.getSingleRoom(token: room.token)).body.ocs.data;
expect(room.sessionId, response.body.ocs.data.sessionId);
await client1.spreed.room.leaveRoom(token: room.token);
room = (await client1.spreed.room.getSingleRoom(token: room.token)).body.ocs.data;
expect(room.sessionId, '0');
});
group('Create room', () {
test('One-to-One', () async {
final response = await client1.spreed.room.createRoom(
roomType: spreed.RoomType.oneToOne.value,
invite: 'user2',
);
expect(response.body.ocs.data.id, 1);
expect(response.body.ocs.data.token, isNotEmpty);
expect(response.body.ocs.data.type, spreed.RoomType.oneToOne.value);
expect(response.body.ocs.data.name, 'user2');
expect(response.body.ocs.data.displayName, 'User Two');
expect(response.body.ocs.data.participantType, spreed.ParticipantType.owner.value);
expect(spreed.ParticipantPermission.fromValue(response.body.ocs.data.permissions), {
spreed.ParticipantPermission.startCall,
spreed.ParticipantPermission.joinCall,
spreed.ParticipantPermission.canIgnoreLobby,
spreed.ParticipantPermission.canPublishAudio,
spreed.ParticipantPermission.canPublishVideo,
spreed.ParticipantPermission.canScreenShare,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
});
});
test('Group', () async {
final response = await client1.spreed.room.createRoom(
roomType: spreed.RoomType.group.value,
invite: 'admin',
);
expect(response.body.ocs.data.id, 1);
expect(response.body.ocs.data.token, isNotEmpty);
expect(response.body.ocs.data.type, spreed.RoomType.group.value);
expect(response.body.ocs.data.name, 'admin');
expect(response.body.ocs.data.displayName, 'admin');
expect(response.body.ocs.data.participantType, spreed.ParticipantType.owner.value);
expect(spreed.ParticipantPermission.fromValue(response.body.ocs.data.permissions), {
spreed.ParticipantPermission.startCall,
spreed.ParticipantPermission.joinCall,
spreed.ParticipantPermission.canIgnoreLobby,
spreed.ParticipantPermission.canPublishAudio,
spreed.ParticipantPermission.canPublishVideo,
spreed.ParticipantPermission.canScreenShare,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
});
});
test('Public', () async {
final response = await client1.spreed.room.createRoom(
roomType: spreed.RoomType.public.value,
roomName: 'abc',
);
expect(response.body.ocs.data.id, 1);
expect(response.body.ocs.data.token, isNotEmpty);
expect(response.body.ocs.data.type, spreed.RoomType.public.value);
expect(response.body.ocs.data.name, 'abc');
expect(response.body.ocs.data.displayName, 'abc');
expect(response.body.ocs.data.participantType, spreed.ParticipantType.owner.value);
expect(spreed.ParticipantPermission.fromValue(response.body.ocs.data.permissions), {
spreed.ParticipantPermission.startCall,
spreed.ParticipantPermission.joinCall,
spreed.ParticipantPermission.canIgnoreLobby,
spreed.ParticipantPermission.canPublishAudio,
spreed.ParticipantPermission.canPublishVideo,
spreed.ParticipantPermission.canScreenShare,
spreed.ParticipantPermission.canSendMessageAndShareAndReact,
});
});
});
});
group('Chat', () {
test('Send message', () async {
final startTime = DateTime.now();
final room = (await client1.spreed.room.createRoom(
roomType: spreed.RoomType.oneToOne.value,
invite: 'user2',
))
.body
.ocs
.data;
final response = await client1.spreed.chat.sendMessage(
token: room.token,
message: 'bla',
);
expect(response.body.ocs.data!.id, 2);
expect(response.body.ocs.data!.actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data!.actorId, 'user1');
expect(response.body.ocs.data!.actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data!.timestamp * 1000),
startTime,
);
expect(response.body.ocs.data!.message, 'bla');
expect(response.body.ocs.data!.messageType, spreed.MessageType.comment.name);
});
group('Get messages', () {
test('Directly', () async {
final startTime = DateTime.now();
final room = (await client1.spreed.room.createRoom(
roomType: spreed.RoomType.oneToOne.value,
invite: 'user2',
))
.body
.ocs
.data;
await client1.spreed.chat.sendMessage(
token: room.token,
message: '123',
replyTo: (await client1.spreed.chat.sendMessage(
token: room.token,
message: 'bla',
))
.body
.ocs
.data!
.id,
);
final response = await client1.spreed.chat.receiveMessages(
token: room.token,
lookIntoFuture: 0,
);
expect(response.headers.xChatLastGiven, '1');
expect(response.headers.xChatLastCommonRead, '1');
expect(response.body.ocs.data, hasLength(3));
expect(response.body.ocs.data[0].id, 3);
expect(response.body.ocs.data[0].actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data[0].actorId, 'user1');
expect(response.body.ocs.data[0].actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].timestamp * 1000),
startTime,
);
expect(response.body.ocs.data[0].message, '123');
expect(response.body.ocs.data[0].messageType, spreed.MessageType.comment.name);
expect(response.body.ocs.data[0].parent!.id, 2);
expect(response.body.ocs.data[0].parent!.actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data[0].parent!.actorId, 'user1');
expect(response.body.ocs.data[0].parent!.actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].parent!.timestamp * 1000),
startTime,
);
expect(response.body.ocs.data[0].parent!.message, 'bla');
expect(response.body.ocs.data[0].parent!.messageType, spreed.MessageType.comment.name);
expect(response.body.ocs.data[1].id, 2);
expect(response.body.ocs.data[1].actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data[1].actorId, 'user1');
expect(response.body.ocs.data[1].actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[1].timestamp * 1000),
startTime,
);
expect(response.body.ocs.data[1].message, 'bla');
expect(response.body.ocs.data[1].messageType, spreed.MessageType.comment.name);
expect(response.body.ocs.data[2].id, 1);
expect(response.body.ocs.data[2].actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data[2].actorId, 'user1');
expect(response.body.ocs.data[2].actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[2].timestamp * 1000),
startTime,
);
expect(response.body.ocs.data[2].message, 'You created the conversation');
expect(response.body.ocs.data[2].systemMessage, 'conversation_created');
expect(response.body.ocs.data[2].messageType, spreed.MessageType.system.name);
});
test('Polling', () async {
final startTime = DateTime.now();
final room = await createTestRoom();
final message = (await client1.spreed.chat.sendMessage(
token: room.token,
message: 'bla',
))
.body
.ocs
.data!;
unawaited(
Future<void>.delayed(const Duration(seconds: 1)).then((final _) async {
await client1.spreed.chat.sendMessage(
token: room.token,
message: '123',
);
}),
);
final response = await client1.spreed.chat.receiveMessages(
token: room.token,
lookIntoFuture: 1,
timeout: 3,
lastKnownMessageId: message.id,
);
expect(response.body.ocs.data, hasLength(1));
expect(response.body.ocs.data[0].id, 3);
expect(response.body.ocs.data[0].actorType, spreed.ActorType.users.name);
expect(response.body.ocs.data[0].actorId, 'user1');
expect(response.body.ocs.data[0].actorDisplayName, 'User One');
expectDateInReasonableTimeRange(
DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].timestamp * 1000),
startTime,
);
expect(response.body.ocs.data[0].message, '123');
expect(response.body.ocs.data[0].messageType, spreed.MessageType.comment.name);
});
});
});
group('Call', () {
test('Start and end call', () async {
var room = await createTestRoom();
expect(room.hasCall, isFalse);
room = (await client1.spreed.room.joinRoom(token: room.token)).body.ocs.data;
await client1.spreed.call.joinCall(token: room.token);
room = (await client1.spreed.room.getSingleRoom(token: room.token)).body.ocs.data;
expect(room.hasCall, isTrue);
await client1.spreed.call.leaveCall(token: room.token);
room = (await client1.spreed.room.getSingleRoom(token: room.token)).body.ocs.data;
expect(room.hasCall, isFalse);
});
});
group('Signaling', () {
test('Get settings', () async {
final room = await createTestRoom();
final response = await client1.spreed.signaling.getSettings(token: room.token);
expect(response.body.ocs.data.signalingMode, 'internal');
expect(response.body.ocs.data.userId, 'user1');
expect(response.body.ocs.data.hideWarning, false);
expect(response.body.ocs.data.server, '');
expect(response.body.ocs.data.ticket, contains(':user1:'));
expect(response.body.ocs.data.helloAuthParams.$10.userid, 'user1');
expect(response.body.ocs.data.helloAuthParams.$10.ticket, contains(':user1:'));
expect(response.body.ocs.data.helloAuthParams.$20.token.split('').where((final x) => x == '.'), hasLength(2));
expect(response.body.ocs.data.stunservers, hasLength(1));
expect(response.body.ocs.data.stunservers[0].urls, hasLength(1));
expect(response.body.ocs.data.stunservers[0].urls[0], 'stun:stun.nextcloud.com:443');
expect(response.body.ocs.data.turnservers, hasLength(1));
expect(response.body.ocs.data.turnservers[0].urls, hasLength(4));
expect(
response.body.ocs.data.turnservers[0].urls[0],
'turn:staticauth.openrelay.metered.ca:443?transport=udp',
);
expect(
response.body.ocs.data.turnservers[0].urls[1],
'turn:staticauth.openrelay.metered.ca:443?transport=tcp',
);
expect(
response.body.ocs.data.turnservers[0].urls[2],
'turns:staticauth.openrelay.metered.ca:443?transport=udp',
);
expect(
response.body.ocs.data.turnservers[0].urls[3],
'turns:staticauth.openrelay.metered.ca:443?transport=tcp',
);
expect(response.body.ocs.data.turnservers[0].username, isNotEmpty);
expect((response.body.ocs.data.turnservers[0].credential as StringJsonObject).asString, isNotEmpty);
expect(response.body.ocs.data.sipDialinInfo, '');
});
test('Send and receive messages', () async {
final room = (await client1.spreed.room.createRoom(
roomType: spreed.RoomType.oneToOne.value,
invite: 'user2',
))
.body
.ocs
.data;
final room1 = (await client1.spreed.room.joinRoom(token: room.token)).body.ocs.data;
await client1.spreed.call.joinCall(token: room.token);
final client2 = await getTestClient(
container,
username: 'user2',
);
final room2 = (await client2.spreed.room.joinRoom(token: room.token)).body.ocs.data;
await client2.spreed.call.joinCall(token: room.token);
await client1.spreed.signaling.sendMessages(
token: room.token,
messages: json.encode([
{
'ev': 'message',
'sessionId': room1.sessionId,
'fn': json.encode({
'to': room2.sessionId,
}),
},
]),
);
await Future<void>.delayed(const Duration(seconds: 1));
final messages = (await client2.spreed.signaling.pullMessages(token: room.token)).body.ocs.data;
expect(messages, hasLength(2));
expect(messages[0].type, 'message');
expect(json.decode(messages[0].data.string!), {'to': room2.sessionId, 'from': room1.sessionId});
expect(messages[1].type, 'usersInRoom');
expect(messages[1].data.builtListSignalingSession, hasLength(2));
expect(messages[1].data.builtListSignalingSession![0].userId, 'user1');
expect(messages[1].data.builtListSignalingSession![1].userId, 'user2');
});
});
},
retry: retryCount,
timeout: timeout,
);
}

4
tool/Dockerfile.dev

@ -25,6 +25,10 @@ RUN install_app_version notes https://github.com/nextcloud-releases/notes/releas
ARG UPPUSH_VERSION=1.4.0 ARG UPPUSH_VERSION=1.4.0
RUN install_app_version uppush https://codeberg.org/NextPush/uppush/archive/$UPPUSH_VERSION.tar.gz RUN install_app_version uppush https://codeberg.org/NextPush/uppush/archive/$UPPUSH_VERSION.tar.gz
ARG SPREED_VERSION=17.1.2
RUN install_app_version spreed https://github.com/nextcloud-releases/spreed/releases/download/v$SPREED_VERSION/spreed-v$SPREED_VERSION.tar.gz
RUN ./occ talk:turn:add turn,turns staticauth.openrelay.metered.ca:443 udp,tcp --secret openrelayprojectsecret
RUN ./occ app:enable password_policy RUN ./occ app:enable password_policy
RUN (sh /entrypoint.sh php -S 0.0.0.0:8080 &) && \ RUN (sh /entrypoint.sh php -S 0.0.0.0:8080 &) && \
until curl -s -o /dev/null http://localhost:8080/status.php; do true; done && \ until curl -s -o /dev/null http://localhost:8080/status.php; do true; done && \

19
tool/generate-specs.sh

@ -11,10 +11,10 @@ function generate_spec() {
composer exec generate-spec -- "$path" "../../packages/nextcloud/lib/src/api/$codename.openapi.json" --first-content-type --openapi-version 3.1.0 composer exec generate-spec -- "$path" "../../packages/nextcloud/lib/src/api/$codename.openapi.json" --first-content-type --openapi-version 3.1.0
} }
for dir in external/nextcloud-server external/nextcloud-notifications; do for dir in external/nextcloud-server external/nextcloud-notifications external/nextcloud-spreed; do
( (
cd "$dir" cd "$dir"
composer update composer install
composer install --no-dev composer install --no-dev
git checkout . # Remove changed files git checkout . # Remove changed files
) )
@ -49,6 +49,21 @@ done
cd external/nextcloud-notifications cd external/nextcloud-notifications
generate_spec "." "notifications" generate_spec "." "notifications"
) )
(
cd external/nextcloud-spreed
generate_spec "." "spreed"
)
for spec in packages/nextcloud/lib/src/api/*.openapi.json; do
name="$(basename "$spec" | cut -d "." -f 1)"
dir="packages/nextcloud/lib/src/patches/$name"
if [ -d "$dir" ]; then
for patch in "$dir/"*; do
cp "$spec" "/tmp/nextcloud-neon/$name.json"
jsonpatch --indent 4 "/tmp/nextcloud-neon/$name.json" "$patch" > "$spec"
done
fi
done
( (
cd external/nextcloud-server cd external/nextcloud-server

2
tool/update-cspell-dictionaries.sh

@ -4,5 +4,5 @@ cd "$(dirname "$0")/.."
for file in .cspell/*; do for file in .cspell/*; do
rm "$file" rm "$file"
cspell lint --quiet --unique --words-only . | tr '[:upper:]' '[:lower:]' | sort -f | uniq > "$file" || true cspell lint --quiet --unique --words-only . | tr '[:upper:]' '[:lower:]' | sed "s/^r'//" | sort -f | uniq > "$file" || true
done done

Loading…
Cancel
Save