You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
7.8 KiB
Dart
281 lines
7.8 KiB
Dart
import 'dart:math';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:matrix/matrix.dart';
|
|
|
|
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
|
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
|
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
|
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
|
|
|
|
extension SpacesClientExtension on Client {
|
|
Future<String> createPangeaSpace({
|
|
required String name,
|
|
required String introChatName,
|
|
required String announcementsChatName,
|
|
Visibility visibility = Visibility.private,
|
|
JoinRules joinRules = JoinRules.public,
|
|
Uint8List? avatar,
|
|
Uri? avatarUrl,
|
|
}) async {
|
|
final joinCode = await SpaceCodeUtil.generateSpaceCode(this);
|
|
final roomId = await createRoom(
|
|
creationContent: {'type': RoomCreationTypes.mSpace},
|
|
visibility: visibility,
|
|
name: name.trim(),
|
|
powerLevelContentOverride: {'events_default': 100},
|
|
initialState: [
|
|
..._spaceInitialState(
|
|
userID!,
|
|
joinCode,
|
|
joinRules: joinRules,
|
|
),
|
|
if (avatar != null)
|
|
StateEvent(
|
|
type: EventTypes.RoomAvatar,
|
|
content: {'url': avatarUrl.toString()},
|
|
),
|
|
],
|
|
);
|
|
|
|
final space = await _waitForRoom(roomId);
|
|
if (space == null) return roomId;
|
|
|
|
await _addDefaultSpaceChats(
|
|
space: space,
|
|
introductionsName: introChatName,
|
|
announcementsName: announcementsChatName,
|
|
);
|
|
return roomId;
|
|
}
|
|
|
|
Future<Room?> _waitForRoom(String roomId) async {
|
|
final room = getRoomById(roomId);
|
|
if (room != null) return room;
|
|
await waitForRoomInSync(roomId, join: true).timeout(
|
|
const Duration(seconds: 15),
|
|
onTimeout: () {
|
|
throw Exception('Timeout fetching room after creation');
|
|
},
|
|
);
|
|
return getRoomById(roomId);
|
|
}
|
|
|
|
Future<void> _addDefaultSpaceChats({
|
|
required Room space,
|
|
String introductionsName = "Introductions",
|
|
String announcementsName = "Announcements",
|
|
}) async {
|
|
Uri? introChatUploadURL;
|
|
Uri? announcementsChatUploadURL;
|
|
|
|
try {
|
|
final random = Random();
|
|
final introChatIconURL = SpaceConstants
|
|
.introChatIcons[random.nextInt(SpaceConstants.introChatIcons.length)];
|
|
final announcementsChatIconURL = SpaceConstants.announcementChatIcons[
|
|
random.nextInt(SpaceConstants.announcementChatIcons.length)];
|
|
|
|
final introResponse = await http.get(Uri.parse(introChatIconURL));
|
|
final introChatIcon = introResponse.bodyBytes;
|
|
final intoChatIconFilename = Uri.encodeComponent(
|
|
Uri.parse(introChatIconURL).pathSegments.last,
|
|
);
|
|
introChatUploadURL = await uploadContent(
|
|
introChatIcon,
|
|
filename: intoChatIconFilename,
|
|
);
|
|
|
|
final announcementsResponse =
|
|
await http.get(Uri.parse(announcementsChatIconURL));
|
|
final announcementsChatIcon = announcementsResponse.bodyBytes;
|
|
final announcementsChatIconFilename = Uri.encodeComponent(
|
|
Uri.parse(announcementsChatIconURL).pathSegments.last,
|
|
);
|
|
announcementsChatUploadURL = await uploadContent(
|
|
announcementsChatIcon,
|
|
filename: announcementsChatIconFilename,
|
|
);
|
|
} catch (e, s) {
|
|
ErrorHandler.logError(
|
|
e: "Failed to upload space chat icons",
|
|
s: s,
|
|
data: {
|
|
"error": e,
|
|
"spaceId": space.id,
|
|
},
|
|
);
|
|
}
|
|
|
|
final introChatFuture = createRoom(
|
|
preset: CreateRoomPreset.publicChat,
|
|
visibility: Visibility.private,
|
|
name: introductionsName,
|
|
roomAliasName:
|
|
"${SpaceConstants.introductionChatAlias}_${space.id.localpart}",
|
|
initialState: [
|
|
if (introChatUploadURL != null)
|
|
StateEvent(
|
|
type: EventTypes.RoomAvatar,
|
|
content: {'url': introChatUploadURL.toString()},
|
|
),
|
|
RoomDefaults.defaultPowerLevels(userID!),
|
|
StateEvent(
|
|
type: EventTypes.RoomJoinRules,
|
|
content: {
|
|
'join_rule': 'knock_restricted',
|
|
'allow': [
|
|
{
|
|
"type": "m.room_membership",
|
|
"room_id": space.id,
|
|
}
|
|
],
|
|
},
|
|
),
|
|
],
|
|
);
|
|
|
|
final announcementsChatFuture = createRoom(
|
|
preset: CreateRoomPreset.publicChat,
|
|
visibility: Visibility.private,
|
|
name: announcementsName,
|
|
roomAliasName:
|
|
"${SpaceConstants.announcementsChatAlias}_${space.id.localpart}",
|
|
initialState: [
|
|
if (announcementsChatUploadURL != null)
|
|
StateEvent(
|
|
type: EventTypes.RoomAvatar,
|
|
content: {'url': announcementsChatUploadURL.toString()},
|
|
),
|
|
RoomDefaults.restrictedPowerLevels(userID!),
|
|
StateEvent(
|
|
type: EventTypes.RoomJoinRules,
|
|
content: {
|
|
'join_rule': 'knock_restricted',
|
|
'allow': [
|
|
{
|
|
"type": "m.room_membership",
|
|
"room_id": space.id,
|
|
}
|
|
],
|
|
},
|
|
),
|
|
],
|
|
);
|
|
|
|
final List<String> roomIds = await Future.wait([
|
|
introChatFuture,
|
|
announcementsChatFuture,
|
|
]);
|
|
|
|
if (roomIds.length != 2) {
|
|
throw Exception('Failed to create default space chats');
|
|
}
|
|
|
|
for (final roomId in roomIds) {
|
|
final room = getRoomById(roomId);
|
|
if (room == null) {
|
|
await waitForRoomInSync(roomId, join: true);
|
|
}
|
|
}
|
|
|
|
final addIntroChatFuture = space.addToSpace(
|
|
roomIds[0],
|
|
);
|
|
|
|
final addAnnouncementsChatFuture = space.addToSpace(
|
|
roomIds[1],
|
|
);
|
|
|
|
await Future.wait([
|
|
addIntroChatFuture,
|
|
addAnnouncementsChatFuture,
|
|
]);
|
|
}
|
|
|
|
List<StateEvent> _spaceInitialState(
|
|
String userID,
|
|
String joinCode, {
|
|
required JoinRules joinRules,
|
|
}) {
|
|
return [
|
|
RoomDefaults.defaultSpacePowerLevels(userID),
|
|
StateEvent(
|
|
type: EventTypes.RoomJoinRules,
|
|
content: {
|
|
ModelKey.joinRule: joinRules.toString().replaceAll('JoinRules.', ''),
|
|
ModelKey.accessCode: joinCode,
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
/// Keep the room's current join rule state event content (except for what's intentionally replaced)
|
|
/// since space's access codes were stored there. Don't want to accidentally remove them.
|
|
Future<void> pangeaSetJoinRules(
|
|
String roomId,
|
|
String joinRule, {
|
|
List<Map<String, dynamic>>? allow,
|
|
}) async {
|
|
final room = getRoomById(roomId);
|
|
if (room == null) {
|
|
throw Exception('Room not found for user ID: $userID');
|
|
}
|
|
|
|
final currentJoinRule = room
|
|
.getState(
|
|
EventTypes.RoomJoinRules,
|
|
)
|
|
?.content ??
|
|
{};
|
|
|
|
if (currentJoinRule[ModelKey.joinRule] == joinRule &&
|
|
(currentJoinRule['allow'] == allow)) {
|
|
return; // No change needed
|
|
}
|
|
|
|
currentJoinRule[ModelKey.joinRule] = joinRule;
|
|
currentJoinRule['allow'] = allow;
|
|
|
|
await setRoomStateWithKey(
|
|
roomId,
|
|
EventTypes.RoomJoinRules,
|
|
'',
|
|
currentJoinRule,
|
|
);
|
|
}
|
|
|
|
Future<void> setSpaceChildAccess(String roomId) async {
|
|
await pangeaSetJoinRules(
|
|
roomId,
|
|
'knock_restricted',
|
|
allow: [
|
|
{
|
|
"type": "m.room_membership",
|
|
"room_id": id,
|
|
}
|
|
],
|
|
);
|
|
|
|
await setRoomVisibilityOnDirectory(
|
|
roomId,
|
|
visibility: Visibility.private,
|
|
);
|
|
}
|
|
|
|
Future<void> resetSpaceChildAccess(String roomId) async {
|
|
await pangeaSetJoinRules(
|
|
roomId,
|
|
JoinRules.knock.toString().replaceAll('JoinRules.', ''),
|
|
);
|
|
|
|
await setRoomVisibilityOnDirectory(
|
|
roomId,
|
|
visibility: Visibility.private,
|
|
);
|
|
}
|
|
}
|