draft code in matrix layer to create room
This commit is contained in:
parent
65dcf8bc36
commit
348de312f9
6 changed files with 216 additions and 7 deletions
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import {Room} from "./room/Room.js";
|
||||
import {ArchivedRoom} from "./room/ArchivedRoom.js";
|
||||
import {RoomStatus} from "./room/RoomStatus.js";
|
||||
import {RoomBeingCreated} from "./room/create";
|
||||
import {Invite} from "./room/Invite.js";
|
||||
import {Pusher} from "./push/Pusher";
|
||||
import { ObservableMap } from "../observable/index.js";
|
||||
|
@ -63,6 +64,7 @@ export class Session {
|
|||
this._activeArchivedRooms = new Map();
|
||||
this._invites = new ObservableMap();
|
||||
this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params);
|
||||
this._roomsBeingCreated = new ObservableMap();
|
||||
this._user = new User(sessionInfo.userId);
|
||||
this._deviceMessageHandler = new DeviceMessageHandler({storage});
|
||||
this._olm = olm;
|
||||
|
@ -421,7 +423,7 @@ export class Session {
|
|||
// load rooms
|
||||
const rooms = await txn.roomSummary.getAll();
|
||||
const roomLoadPromise = Promise.all(rooms.map(async summary => {
|
||||
const room = this.createRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId));
|
||||
const room = this.createJoinedRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId));
|
||||
await log.wrap("room", log => room.load(summary, txn, log));
|
||||
this._rooms.add(room.id, room);
|
||||
}));
|
||||
|
@ -530,7 +532,7 @@ export class Session {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
createRoom(roomId, pendingEvents) {
|
||||
createJoinedRoom(roomId, pendingEvents) {
|
||||
return new Room({
|
||||
roomId,
|
||||
getSyncToken: this._getSyncToken,
|
||||
|
@ -580,6 +582,20 @@ export class Session {
|
|||
});
|
||||
}
|
||||
|
||||
get roomsBeingCreated() {
|
||||
return this._roomsBeingCreated;
|
||||
}
|
||||
|
||||
createRoom(type, isEncrypted, explicitName, topic, invites) {
|
||||
const localId = `room-being-created-${this.platform.random()}`;
|
||||
const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites);
|
||||
this._roomsBeingCreated.set(localId, roomBeingCreated);
|
||||
this._platform.logger.runDetached("create room", async log => {
|
||||
roomBeingCreated.start(this._hsApi, log);
|
||||
});
|
||||
return localId;
|
||||
}
|
||||
|
||||
async obtainSyncLock(syncResponse) {
|
||||
const toDeviceEvents = syncResponse.to_device?.events;
|
||||
if (Array.isArray(toDeviceEvents) && toDeviceEvents.length) {
|
||||
|
@ -667,6 +683,13 @@ export class Session {
|
|||
for (const rs of roomStates) {
|
||||
if (rs.shouldAdd) {
|
||||
this._rooms.add(rs.id, rs.room);
|
||||
for (const roomBeingCreated of this._roomsBeingCreated) {
|
||||
if (roomBeingCreated.roomId === rs.id) {
|
||||
roomBeingCreated.notifyJoinedRoom();
|
||||
this._roomsBeingCreated.delete(roomBeingCreated.localId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (rs.shouldRemove) {
|
||||
this._rooms.remove(rs.id);
|
||||
}
|
||||
|
|
|
@ -392,7 +392,7 @@ export class Sync {
|
|||
// we receive also gets written.
|
||||
// In any case, don't create a room for a rejected invite
|
||||
if (!room && (membership === "join" || (isInitialSync && membership === "leave"))) {
|
||||
room = this._session.createRoom(roomId);
|
||||
room = this._session.createJoinedRoom(roomId);
|
||||
isNewRoom = true;
|
||||
}
|
||||
if (room) {
|
||||
|
|
|
@ -57,3 +57,15 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRoomEncryptionEvent() {
|
||||
return {
|
||||
"type": "m.room.encryption",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"algorithm": MEGOLM_ALGORITHM,
|
||||
"rotation_period_ms": 604800000,
|
||||
"rotation_period_msgs": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -263,20 +263,29 @@ export class HomeServerApi {
|
|||
return this._post(`/logout`, {}, {}, options);
|
||||
}
|
||||
|
||||
getDehydratedDevice(options: IRequestOptions): IHomeServerRequest {
|
||||
getDehydratedDevice(options: IRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._get(`/dehydrated_device`, undefined, undefined, options);
|
||||
}
|
||||
|
||||
createDehydratedDevice(payload: Record<string, any>, options: IRequestOptions): IHomeServerRequest {
|
||||
createDehydratedDevice(payload: Record<string, any>, options: IRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._put(`/dehydrated_device`, {}, payload, options);
|
||||
}
|
||||
|
||||
claimDehydratedDevice(deviceId: string, options: IRequestOptions): IHomeServerRequest {
|
||||
claimDehydratedDevice(deviceId: string, options: IRequestOptions = {}): IHomeServerRequest {
|
||||
options.prefix = DEHYDRATION_PREFIX;
|
||||
return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options);
|
||||
}
|
||||
|
||||
profile(userId: string, options?: IRequestOptions): IHomeServerRequest {
|
||||
return this._get(`/profile/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
createRoom(payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
|
||||
return this._post(`/createRoom`, {}, payload, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
import {Request as MockRequest} from "../../mocks/Request.js";
|
||||
|
|
165
src/matrix/room/create.ts
Normal file
165
src/matrix/room/create.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {calculateRoomName} from "./members/Heroes";
|
||||
import {createRoomEncryptionEvent} from "../e2ee/common";
|
||||
import {EventEmitter} from "../../utils/EventEmitter";
|
||||
|
||||
import type {StateEvent} from "../storage/types";
|
||||
import type {HomeServerApi} from "../net/HomeServerApi";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
|
||||
type CreateRoomPayload = {
|
||||
is_direct?: boolean;
|
||||
preset?: string;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
invite?: string[];
|
||||
initial_state?: StateEvent[]
|
||||
}
|
||||
|
||||
export enum RoomType {
|
||||
DirectMessage,
|
||||
Private,
|
||||
Public
|
||||
}
|
||||
|
||||
function defaultE2EEStatusForType(type: RoomType): boolean {
|
||||
switch (type) {
|
||||
case RoomType.DirectMessage:
|
||||
case RoomType.Private:
|
||||
return true;
|
||||
case RoomType.Public:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function presetForType(type: RoomType): string {
|
||||
switch (type) {
|
||||
case RoomType.DirectMessage:
|
||||
return "trusted_private_chat";
|
||||
case RoomType.Private:
|
||||
return "private_chat";
|
||||
case RoomType.Public:
|
||||
return "public_chat";
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomBeingCreated extends EventEmitter<{change: never, joined: string}> {
|
||||
private _roomId?: string;
|
||||
private profiles: Profile[] = [];
|
||||
|
||||
public readonly isEncrypted: boolean;
|
||||
public readonly name: string;
|
||||
|
||||
constructor(
|
||||
private readonly localId: string,
|
||||
private readonly type: RoomType,
|
||||
isEncrypted: boolean | undefined,
|
||||
private readonly explicitName: string | undefined,
|
||||
private readonly topic: string | undefined,
|
||||
private readonly inviteUserIds: string[] | undefined,
|
||||
log: ILogItem
|
||||
) {
|
||||
super();
|
||||
this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted;
|
||||
if (explicitName) {
|
||||
this.name = explicitName;
|
||||
} else {
|
||||
const summaryData = {
|
||||
joinCount: 1, // ourselves
|
||||
inviteCount: (this.inviteUserIds?.length || 0)
|
||||
};
|
||||
this.name = calculateRoomName(this.profiles, summaryData, log);
|
||||
}
|
||||
}
|
||||
|
||||
public async start(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
await Promise.all([
|
||||
this.loadProfiles(hsApi, log),
|
||||
this.create(hsApi, log),
|
||||
]);
|
||||
}
|
||||
|
||||
private async create(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
const options: CreateRoomPayload = {
|
||||
is_direct: this.type === RoomType.DirectMessage,
|
||||
preset: presetForType(this.type)
|
||||
};
|
||||
if (this.explicitName) {
|
||||
options.name = this.explicitName;
|
||||
}
|
||||
if (this.topic) {
|
||||
options.topic = this.topic;
|
||||
}
|
||||
if (this.inviteUserIds) {
|
||||
options.invite = this.inviteUserIds;
|
||||
}
|
||||
if (this.isEncrypted) {
|
||||
options.initial_state = [createRoomEncryptionEvent()];
|
||||
}
|
||||
|
||||
const response = await hsApi.createRoom(options, {log}).response();
|
||||
this._roomId = response["room_id"];
|
||||
this.emit("change");
|
||||
}
|
||||
|
||||
private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
|
||||
// only load profiles if we need it for the room name and avatar
|
||||
if (!this.explicitName && this.inviteUserIds) {
|
||||
this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log);
|
||||
this.emit("change");
|
||||
}
|
||||
}
|
||||
|
||||
notifyJoinedRoom() {
|
||||
this.emit("joined", this._roomId);
|
||||
}
|
||||
|
||||
get avatarUrl(): string | undefined {
|
||||
return this.profiles[0]?.avatarUrl;
|
||||
}
|
||||
|
||||
get roomId(): string | undefined {
|
||||
return this._roomId;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<Profile[]> {
|
||||
const profiles = await Promise.all(userIds.map(async userId => {
|
||||
const response = await hsApi.profile(userId, {log}).response();
|
||||
return new Profile(userId, response.displayname as string, response.avatar_url as string);
|
||||
}));
|
||||
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return profiles;
|
||||
}
|
||||
|
||||
interface IProfile {
|
||||
get userId(): string;
|
||||
get displayName(): string;
|
||||
get avatarUrl(): string;
|
||||
get name(): string;
|
||||
}
|
||||
|
||||
export class Profile implements IProfile {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly displayName: string,
|
||||
public readonly avatarUrl: string
|
||||
) {}
|
||||
|
||||
get name() { return this.displayName || this.userId; }
|
||||
}
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import {RoomMember} from "./RoomMember.js";
|
||||
|
||||
function calculateRoomName(sortedMembers, summaryData, log) {
|
||||
export function calculateRoomName(sortedMembers, summaryData, log) {
|
||||
const countWithoutMe = summaryData.joinCount + summaryData.inviteCount - 1;
|
||||
if (sortedMembers.length >= countWithoutMe) {
|
||||
if (sortedMembers.length > 1) {
|
||||
|
|
Reference in a new issue