keanu-weblite/src/plugins/utils.js

1256 lines
40 KiB
JavaScript
Raw Normal View History

2025-05-09 14:42:37 +02:00
import axios from "axios";
2020-11-21 14:57:43 +01:00
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
import imageResize from "image-resize";
2025-06-09 09:44:37 +02:00
import { AutoDiscovery, Method } from "matrix-js-sdk";
2025-05-09 14:42:37 +02:00
import User from "../models/user";
2025-03-31 16:33:54 +02:00
import prettyBytes from "pretty-bytes";
2025-05-09 14:42:37 +02:00
import { Thread } from "matrix-js-sdk/lib/models/thread";
import { imageSize } from "image-size";
2025-03-31 16:33:54 +02:00
import dayjs from "dayjs";
import aesjs from "aes-js";
2025-05-09 14:42:37 +02:00
import localizedFormat from "dayjs/plugin/localizedFormat";
import duration from "dayjs/plugin/duration";
import i18n from "./lang";
2025-06-09 09:44:37 +02:00
import { toRaw, isRef, isReactive, isProxy } from "vue";
import { UploadPromise } from "../models/attachment";
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
2023-06-28 12:14:44 +00:00
export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default";
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
2023-06-28 12:14:44 +00:00
export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
2024-04-03 09:34:24 +02:00
export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
2023-08-07 14:13:35 +00:00
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
export const CLIENT_EVENT_PROOF_HINT = "im.keanu.proof_hint";
2023-08-07 14:13:35 +00:00
2025-08-27 16:08:52 +02:00
export const THUMBNAIL_MAX_WIDTH = 640;
export const THUMBNAIL_MAX_HEIGHT = 640;
2025-06-09 09:44:37 +02:00
2021-01-20 11:32:21 +01:00
// Install extended localized format
2025-05-09 14:42:37 +02:00
dayjs.extend(localizedFormat);
2021-02-22 16:34:19 +01:00
dayjs.extend(duration);
2021-01-20 11:32:21 +01:00
// Store info about getUserMedia BEFORE we aply polyfill(s)!
2021-03-23 16:20:01 +01:00
var _browserCanRecordAudioF = function () {
2025-05-09 14:42:37 +02:00
var legacyGetUserMedia =
navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
return (
legacyGetUserMedia !== undefined || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia !== undefined)
);
};
var _browserCanRecordAudio = _browserCanRecordAudioF();
2020-11-19 17:08:58 +01:00
class Util {
2025-03-31 16:33:54 +02:00
threadMessageType() {
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
}
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
return new Promise((resolve, reject) => {
const content = event.getContent();
var url = null;
2025-05-13 16:34:12 +02:00
var mime = "image/png";
2025-03-31 16:33:54 +02:00
var file = null;
let decrypt = true;
if (content.url != null) {
url = matrixClient.mxcUrlToHttp(
content.url,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthedMedia
);
2025-03-31 16:33:54 +02:00
decrypt = false;
2025-05-13 16:34:12 +02:00
if (content.info) {
mime = content.info.mimetype;
}
2025-03-31 16:33:54 +02:00
} else if (content.file && content.file.url) {
file = content.file;
url = matrixClient.mxcUrlToHttp(
file.url,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthedMedia
);
2025-05-13 16:34:12 +02:00
mime = file.mimetype;
2025-03-31 16:33:54 +02:00
}
2025-03-31 16:33:54 +02:00
if (url == null) {
reject("No url found!");
}
2025-03-31 16:33:54 +02:00
axios
.get(url, {
headers: {
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
},
signal: abortController ? abortController.signal : undefined,
responseType: "arraybuffer",
onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
if (progressCallback) {
progressCallback(percentCompleted);
}
},
})
.then((response) => {
return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data });
2025-03-31 16:33:54 +02:00
})
.then((bytes) => {
if (asBlob) {
2025-05-13 16:34:12 +02:00
resolve(new Blob([bytes.buffer], { type: mime }));
} else {
2025-05-13 16:34:12 +02:00
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: mime })));
2025-03-31 16:33:54 +02:00
}
})
.catch((err) => {
console.log("Download error: ", err);
reject(err);
})
.finally(() => {
if (progressCallback) {
progressCallback(null);
}
});
2025-03-31 16:33:54 +02:00
});
}
2025-06-09 09:44:37 +02:00
async getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
const content = event.getContent();
var url = null;
var mime = "image/png";
var file = null;
let decrypt = true;
if (!!content.info && !!content.info.thumbnail_url) {
2025-08-20 15:12:04 +02:00
url = matrixClient.mxcUrlToHttp(
content.info.thumbnail_url,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthedMedia
);
2025-06-09 09:44:37 +02:00
decrypt = false;
if (content.info.thumbnail_info) {
mime = content.info.thumbnail_info.mimetype;
}
2025-06-09 09:44:37 +02:00
} else if (content.url != null) {
2025-08-20 15:12:04 +02:00
url = matrixClient.mxcUrlToHttp(
content.url,
undefined,
undefined,
undefined,
undefined,
undefined,
useAuthedMedia
);
2025-06-09 09:44:37 +02:00
decrypt = false;
if (content.info) {
mime = content.info.mimetype;
2025-03-31 16:33:54 +02:00
}
2025-06-09 09:44:37 +02:00
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
file = content.info.thumbnail_file;
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
mime = file.mimetype;
} else if (
content.file &&
content.file.url &&
this.getFileSize(event) > 0 &&
this.getFileSize(event) < config.maxSizeAutoDownloads
) {
// No thumb, use real url
file = content.file;
url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
mime = file.mimetype;
}
2025-06-09 09:44:37 +02:00
if (url == null) {
throw new Error("No url found!");
}
2025-08-20 15:12:04 +02:00
const response = await axios.get(
url,
useAuthedMedia
? {
responseType: "arraybuffer",
headers: {
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
},
}
: { responseType: "arraybuffer" }
);
2025-06-09 09:44:37 +02:00
const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
2025-03-31 16:33:54 +02:00
}
b64toBuffer(val) {
const baseValue = val.replaceAll("-", "+").replaceAll("_", "/");
return Buffer.from(baseValue, "base64");
}
2025-03-31 16:33:54 +02:00
decryptIfNeeded(file, response) {
return new Promise((resolve, reject) => {
const key = this.b64toBuffer(file.key.k);
const iv = this.b64toBuffer(file.iv);
const originalHash = this.b64toBuffer(file.hashes.sha256);
2025-03-31 16:33:54 +02:00
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
2025-03-31 16:33:54 +02:00
const data = new Uint8Array(response.data);
crypto.subtle
.digest("SHA-256", data)
.then((hash) => {
// Calculate sha256 and compare hashes
if (Buffer.compare(Buffer.from(hash), originalHash) != 0) {
reject("Hashes don't match!");
return;
}
var decryptedBytes = aesCtr.decrypt(data);
resolve(decryptedBytes);
})
.catch((err) => {
reject("Failed to calculate hash value");
});
2025-03-31 16:33:54 +02:00
});
}
2025-08-20 15:12:04 +02:00
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) {
2025-09-11 16:00:18 +02:00
var content = ContentHelpers.makeTextMessage(text ?? "");
2025-03-31 16:33:54 +02:00
if (editedEvent) {
content["m.relates_to"] = {
rel_type: "m.replace",
event_id: editedEvent.getId(),
};
content["m.new_content"] = {
body: content.body,
msgtype: content.msgtype,
};
} else if (replyToEvent) {
content["m.relates_to"] = {
"m.in_reply_to": {
event_id: replyToEvent.getId(),
},
};
let senderContent = replyToEvent.getContent();
const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, "body")
? senderContent.body
: Object.values(senderContent)[0].question.body;
// Prefix the content with reply info (seems to be a legacy thing)
const prefix = senderContentBody
.split("\n")
.map((item, index) => {
return "> " + (index == 0 ? "<" + replyToEvent.getSender() + "> " : "") + item;
})
.join("\n");
content.body = prefix + "\n\n" + content.body;
}
2025-08-20 15:12:04 +02:00
return this.sendMessage(matrixClient, roomId, "m.room.message", content, txnId);
2025-03-31 16:33:54 +02:00
}
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
const content = {
"m.relates_to": Object.assign(extraData, {
key: emoji,
rel_type: "m.annotation",
event_id: event.getId(),
}),
};
return this.sendMessage(matrixClient, roomId, "m.reaction", content);
}
createPoll(matrixClient, roomId, question, answers, isDisclosed) {
var idx = 0;
let answerData = answers.map((a) => {
idx++;
return { id: "" + idx, "org.matrix.msc1767.text": a.text };
});
const content = {
"org.matrix.msc3381.poll.start": {
question: {
"org.matrix.msc1767.text": question,
body: question,
},
kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
max_selections: 1,
answers: answerData,
},
};
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
}
closePoll(matrixClient, roomId, event) {
const content = {
"m.relates_to": {
rel_type: "m.reference",
event_id: event.getId(),
},
"org.matrix.msc3381.poll.end": {},
};
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
}
sendPollAnswer(matrixClient, roomId, answers, event) {
const content = {
"m.relates_to": {
rel_type: "m.reference",
event_id: event.getId(),
},
"org.matrix.msc3381.poll.response": {
answers: answers,
},
};
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
}
2025-08-20 15:12:04 +02:00
sendMessage(matrixClient, roomId, eventType, content, txnId) {
2025-03-31 16:33:54 +02:00
return new Promise((resolve, reject) => {
matrixClient
2025-08-20 15:12:04 +02:00
.sendEvent(roomId, eventType, content, txnId, undefined)
2025-03-31 16:33:54 +02:00
.then((result) => {
console.log("Message sent: ", result);
resolve(result.event_id);
})
.catch((err) => {
console.log("Send error: ", err);
if (err && err.name == "UnknownDeviceError") {
console.log("Unknown devices. Mark as known before retrying.");
var setAsKnownPromises = [];
for (var user of Object.keys(err.devices)) {
const userDevices = err.devices[user];
for (var deviceId of Object.keys(userDevices)) {
const deviceInfo = userDevices[deviceId];
if (!deviceInfo.known) {
setAsKnownPromises.push(matrixClient.setDeviceKnown(user, deviceId, true));
}
}
2020-11-19 17:08:58 +01:00
}
2025-03-31 16:33:54 +02:00
Promise.all(setAsKnownPromises).then(() => {
// All devices now marked as "known", try to resend
let event = err.event;
if (!event) {
// Seems event is no longer send in the UnknownDevices error...
const room = matrixClient.getRoom(roomId);
if (room) {
event = room
.getLiveTimeline()
.getEvents()
.find((e) => {
// Find the exact match (= object equality)
return e.error === err;
});
}
}
matrixClient
.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
.then((result) => {
console.log("Message sent: ", result);
resolve(result.event_id);
2020-11-19 17:08:58 +01:00
})
2025-03-31 16:33:54 +02:00
.catch((err) => {
// Still error, abort
reject(err.toLocaleString());
2020-11-19 17:08:58 +01:00
});
2025-03-31 16:33:54 +02:00
});
} else {
reject(err.toLocaleString());
}
2020-11-19 17:08:58 +01:00
});
2025-03-31 16:33:54 +02:00
});
}
2025-06-09 09:44:37 +02:00
async encryptFileAndGenerateInfo(data, mime) {
let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.
// Encrypt
let aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
let encryptedBytes = aesCtr.encrypt(data);
// Calculate sha256
let hash = await crypto.subtle.digest("SHA-256", encryptedBytes);
const jwk = {
kty: "oct",
key_ops: ["encrypt", "decrypt"],
alg: "A256CTR",
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
ext: true,
};
const encryptedFile = {
mimetype: mime,
key: jwk,
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
v: "v2",
};
return [encryptedBytes, encryptedFile];
}
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot, dimensions, thumbnail, proofHintFlags) {
2025-03-31 16:33:54 +02:00
const uploadPromise = new UploadPromise(undefined);
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
var reader = new FileReader();
2025-05-13 16:34:12 +02:00
reader.onload = async (e) => {
2025-03-31 16:33:54 +02:00
if (uploadPromise.aborted) {
reject("Aborted");
return;
}
2025-06-09 09:44:37 +02:00
try {
const fileContents = e.target.result;
2025-06-09 09:44:37 +02:00
var data = new Uint8Array(fileContents);
let thumbnailData = undefined;
let thumbnailInfo = undefined;
2020-11-21 14:57:43 +01:00
2025-06-09 09:44:37 +02:00
const info = {
mimetype: file.type,
size: file.size,
};
2020-11-25 14:42:50 +01:00
2025-06-09 09:44:37 +02:00
// If audio, send duration in ms as well
if (file.duration) {
info.duration = file.duration;
}
2025-06-09 09:44:37 +02:00
var description = file.name;
var msgtype = "m.file";
2025-08-20 15:12:04 +02:00
2025-07-10 09:46:07 +02:00
if (thumbnail) {
thumbnailData = thumbnail.data;
thumbnailInfo = {
mimetype: thumbnail.mimetype,
size: thumbnail.size,
w: thumbnail.w,
h: thumbnail.h,
};
2025-08-20 15:12:04 +02:00
}
2025-06-09 09:44:37 +02:00
if (file.type.startsWith("image/")) {
msgtype = "m.image";
// Generate thumbnail?
2025-07-10 09:46:07 +02:00
if (dimensions && !thumbnail) {
2025-06-09 09:44:37 +02:00
const w = dimensions.width;
const h = dimensions.height;
if (w > THUMBNAIL_MAX_WIDTH || h > THUMBNAIL_MAX_HEIGHT) {
var aspect = w / h;
var newWidth = parseInt((w > h ? THUMBNAIL_MAX_WIDTH : THUMBNAIL_MAX_HEIGHT * aspect).toFixed());
var newHeight = parseInt((w > h ? THUMBNAIL_MAX_WIDTH / aspect : THUMBNAIL_MAX_HEIGHT).toFixed());
const scaled = await imageResize(file, {
format: "webp",
width: newWidth,
height: newHeight,
outputType: "blob",
2025-08-20 15:12:04 +02:00
}).catch(() => {
return Promise.resolve(undefined);
});
2025-06-09 09:44:37 +02:00
if (scaled && file.size > scaled.size) {
thumbnailData = new Uint8Array(await scaled.arrayBuffer());
thumbnailInfo = {
mimetype: scaled.type,
size: scaled.size,
h: newHeight,
w: newWidth,
};
}
}
}
} else if (file.type.startsWith("audio/")) {
msgtype = "m.audio";
} else if (file.type.startsWith("video/")) {
msgtype = "m.video";
}
let messageContent = {
2025-06-09 09:44:37 +02:00
body: description,
info: info,
msgtype: msgtype,
2025-03-31 16:33:54 +02:00
};
// if (proofHintFlags) {
// messageContent[CLIENT_EVENT_PROOF_HINT] = JSON.stringify(proofHintFlags);
// }
2025-06-09 09:44:37 +02:00
// If thread root (an eventId) is set, add that here
if (threadRoot) {
messageContent["m.relates_to"] = {
rel_type: this.threadMessageType(),
event_id: threadRoot,
};
}
2020-11-25 14:42:50 +01:00
2025-06-09 09:44:37 +02:00
// Set filename for files
if (msgtype == "m.file") {
messageContent.filename = file.name;
}
2023-06-25 17:12:29 +03:00
2025-08-20 15:12:04 +02:00
let totalBytes = 0;
let thumbBytes = 0;
2025-06-09 09:44:37 +02:00
const useEncryption = matrixClient.isRoomEncrypted(roomId);
2025-06-09 09:44:37 +02:00
const dataUploadOpts = {
type: useEncryption ? "application/octet-stream" : file.type,
name: description,
2025-08-20 15:12:04 +02:00
progressHandler: ({ loaded, total }) => {
if (onUploadProgress) {
onUploadProgress({ loaded: loaded + thumbBytes, total: totalBytes });
}
},
2025-06-09 09:44:37 +02:00
onlyContentUri: false,
};
2025-06-09 09:44:37 +02:00
if (useEncryption) {
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
messageContent.file = encryptedFile;
data = encryptedBytes;
}
2025-08-20 15:12:04 +02:00
totalBytes = data.length;
2025-06-09 09:44:37 +02:00
if (thumbnailData) {
messageContent.thumbnail_info = thumbnailInfo;
if (useEncryption) {
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(thumbnailData, file.type);
messageContent.info.thumbnail_file = encryptedFile;
thumbnailData = encryptedBytes;
}
2025-08-20 15:12:04 +02:00
thumbBytes = thumbnailData.length;
totalBytes += thumbnailData.length;
2025-06-09 09:44:37 +02:00
const thumbnailUploadOpts = {
type: useEncryption ? "application/octet-stream" : file.type,
name: "thumb:" + description,
2025-08-20 15:12:04 +02:00
progressHandler: ({ loaded, total }) => {
if (onUploadProgress) {
onUploadProgress({ loaded: loaded, total: totalBytes });
}
},
2025-06-09 09:44:37 +02:00
onlyContentUri: false,
};
const thumbUploadPromise = matrixClient.uploadContent(thumbnailData, thumbnailUploadOpts);
uploadPromise.onAbort = () => {
matrixClient.cancelUpload(thumbUploadPromise);
};
const thumbnailResponse = await thumbUploadPromise;
if (useEncryption) {
messageContent.info.thumbnail_file.url = thumbnailResponse.content_uri;
} else {
messageContent.info.thumbnail_url = thumbnailResponse.content_uri;
}
}
2023-06-28 12:14:44 +00:00
2025-06-09 09:44:37 +02:00
const dataUploadPromise = matrixClient.uploadContent(data, dataUploadOpts);
uploadPromise.onAbort = () => {
matrixClient.cancelUpload(dataUploadPromise);
};
const response = await dataUploadPromise;
if (useEncryption) {
messageContent.file.url = response.content_uri;
} else {
messageContent.url = response.content_uri;
}
2025-06-09 09:44:37 +02:00
// Generate audio waveforms
if (msgtype == "m.audio") {
2025-09-10 18:06:41 +02:00
await this.generateWaveform(fileContents, messageContent);
2025-06-09 09:44:37 +02:00
}
2020-11-25 14:42:50 +01:00
2025-06-09 09:44:37 +02:00
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
resolve(result);
} catch (error) {
reject(error);
}
2025-03-31 16:33:54 +02:00
};
reader.onerror = (err) => {
reject(err);
};
reader.readAsArrayBuffer(file);
});
return uploadPromise;
}
2025-09-10 18:06:41 +02:00
async generateWaveform(data, messageContent) {
2025-03-31 16:33:54 +02:00
if (!(window.AudioContext || window.webkitAudioContext)) {
return; // No support
2020-11-21 14:57:43 +01:00
}
2025-09-10 18:06:41 +02:00
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx) {
const audioBuffer = await audioCtx.decodeAudioData(data);
if (audioBuffer) {
const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
const samples = 1000; // Number of samples
const blockSize = Math.floor(rawData.length / samples);
let filteredData = [];
for (let i = 0; i < samples; i++) {
let blockStart = blockSize * i; // the location of the first sample in the block
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
}
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
2025-03-31 16:33:54 +02:00
}
2025-09-10 18:06:41 +02:00
// Normalize
const multiplier = Math.pow(Math.max(...filteredData), -1);
filteredData = filteredData.map((n) => n * multiplier);
// Integerize
filteredData = filteredData.map((n) => parseInt((n * 255).toFixed()));
// Generate SVG of waveform
let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
svg += `<path d="`;
filteredData.forEach((d, i) => {
const delta = d / 2;
svg += `M${i} ${128 - delta}V${128 + delta}`;
});
svg += `" style="fill:none;stroke:green;stroke-width:1" />`;
svg += "</svg>";
messageContent.format = "org.matrix.custom.html";
messageContent.formatted_body = svg;
// if duration is not set, do that here, since we have it
if (!messageContent.info.duration) {
messageContent.info.duration = parseInt((1000 * audioBuffer.duration).toFixed());
}
}
}
} catch (error) {
return;
2025-03-31 16:33:54 +02:00
}
}
/**
* Return what "mode" to use for the given room.
*
* The default value is given by the room itself (as state events, see roomTypeMixin).
* This method just returns if the user has overridden this in room settings (this
* fact will be persisted as a user specific tag on the room). Note: currently override
* is disabled in the UI...
*/
roomDisplayTypeOverride(roomOrNull) {
if (roomOrNull) {
const room = roomOrNull;
// Have we changed our local view mode of this room?
const tags = room.tags;
if (tags && tags["ui_options"]) {
if (tags["ui_options"]["voice_mode"] === 1) {
return ROOM_TYPE_VOICE_MODE;
} else if (tags["ui_options"]["file_mode"] === 1) {
return ROOM_TYPE_FILE_MODE;
} else if (tags["ui_options"]["file_mode"] === 0 && tags["ui_options"]["file_mode"] === 0) {
// Explicitly set to "default"
return ROOM_TYPE_DEFAULT;
2022-07-01 08:55:26 +00:00
}
2025-03-31 16:33:54 +02:00
}
}
2025-03-31 16:33:54 +02:00
return null;
}
/**
* Return the room type for the current room
* @param {*} roomOrNull
*/
roomDisplayTypeToQueryParam(roomOrNull, roomDisplayType) {
const roomType = this.roomDisplayTypeOverride(roomOrNull) || roomDisplayType;
if (roomType === ROOM_TYPE_FILE_MODE) {
// Send "file" here, so the receiver of the invite link knows to display the "file drop" join page
// instead of the standard one.
return "file";
} else if (roomType === ROOM_TYPE_VOICE_MODE) {
// No need to return "voice" here. The invite page looks the same for default and voice mode,
// so currently no point in cluttering the invite link with it. The corrent mode will be picked up from
// room creation flags once the user joins.
return undefined;
}
2025-03-31 16:33:54 +02:00
return undefined; // Default, just return undefined
}
/** Generate a random user name */
randomUser(prefix) {
var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, "") : null;
if (!pfx || pfx.length == 0) {
pfx = "weblite-";
}
2025-03-31 16:33:54 +02:00
return pfx + this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
}
/**
* Generate random 12 char password
*/
randomPass() {
return this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#_-*+");
}
// From here: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
randomString(length, characters) {
var result = "";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
2025-03-31 16:33:54 +02:00
return result;
}
2025-03-31 16:33:54 +02:00
sanitizeRoomId(roomId) {
if (roomId && roomId.match(/^(!|#).+$/)) {
return roomId;
}
2025-03-31 16:33:54 +02:00
return null;
}
2025-03-31 16:33:54 +02:00
sanitizeUserId(userId) {
if (userId && userId.match(/^([0-9a-z-.=_/]+|@[0-9a-z-.=_/]+:.+)$/)) {
return userId;
}
2025-03-31 16:33:54 +02:00
return null;
}
2025-03-31 16:33:54 +02:00
invalidUserIdChars() {
return /[^0-9a-z-.=_/]+/g;
}
2025-03-31 16:33:54 +02:00
getFirstVisibleElement(parentNode, where) {
let visible = this.findVisibleElements(parentNode);
if (visible) {
visible = visible.filter(where);
}
2025-03-31 16:33:54 +02:00
if (visible && visible.length > 0) {
return visible[0];
}
2025-03-31 16:33:54 +02:00
return null;
}
2025-03-31 16:33:54 +02:00
getLastVisibleElement(parentNode, where) {
let visible = this.findVisibleElements(parentNode);
if (visible) {
visible = visible.filter(where);
}
2025-03-31 16:33:54 +02:00
if (visible && visible.length > 0) {
return visible[visible.length - 1];
}
2025-03-31 16:33:54 +02:00
return null;
}
findVisibleElements(parentNode) {
const middle = this.findOneVisibleElement(parentNode);
if (middle) {
var nodes = [parentNode.children[middle]];
var i = middle - 1;
while (i >= 0 && this.isChildVisible(parentNode, parentNode.children[i])) {
nodes.splice(0, 0, parentNode.children[i]);
i -= 1;
}
i = middle + 1;
while (i < parentNode.children.length && this.isChildVisible(parentNode, parentNode.children[i])) {
nodes.push(parentNode.children[i]);
i += 1;
}
return nodes;
2021-01-20 09:42:13 +01:00
}
2025-03-31 16:33:54 +02:00
return null; // No visible found
}
isChildVisible(parentNode, childNode) {
const rect1 = parentNode.getBoundingClientRect();
const rect2 = childNode.getBoundingClientRect();
var overlap = !(
rect1.right <= rect2.left ||
rect1.left >= rect2.right ||
rect1.bottom <= rect2.top ||
rect1.top >= rect2.bottom
);
return overlap;
}
findOneVisibleElement(parentNode) {
let start = 0;
let end = parentNode && parentNode.children ? parentNode.children.length - 1 : -1;
while (start <= end) {
let middle = Math.floor((start + end) / 2);
let childNode = parentNode.children[middle];
if (this.isChildVisible(parentNode, childNode)) {
// found the key
return middle;
} else if (childNode.getBoundingClientRect().top <= parentNode.getBoundingClientRect().top) {
// continue searching to the right
start = middle + 1;
} else {
// search searching to the left
end = middle - 1;
}
2021-01-20 09:42:13 +01:00
}
2025-03-31 16:33:54 +02:00
// key wasn't found
return null;
}
_importAll(r) {
var images = [];
r.keys().forEach((res) => {
console.log("Avatar", res);
// // Remove"./"
var name = res.split("_")[1];
name = name.slice(0, name.indexOf("."));
name = name.charAt(0).toUpperCase() + name.slice(1);
const image = r(res);
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
images.push({ id: res, image: image, name: "Guest " + name + " " + randomNumber });
});
return images;
}
getDefaultAvatars() {
var images = [];
2025-05-12 17:15:11 +02:00
const modules = import.meta.glob("@/assets/avatars/*.{jpeg,jpg,png}", {
query: "?url",
import: "default",
eager: true,
});
2025-03-31 16:33:54 +02:00
Object.keys(modules).map((path) => {
var name = path.split("_")[1];
name = name.slice(0, name.indexOf("."));
name = name.charAt(0).toUpperCase() + name.slice(1);
const image = modules[path];
2025-03-31 16:33:54 +02:00
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
const title = "Guest " + name + " " + randomNumber;
images.push({ id: path, image: image, name: title, title: title });
2025-03-31 16:33:54 +02:00
});
return images;
}
setAvatar(matrix, file, onUploadProgress) {
return new Promise((resolve, reject) => {
axios
.get(file, { responseType: "arraybuffer" })
.then((response) => {
const opts = {
type: response.headers["content-type"].split(";")[0],
name: "Avatar",
progressHandler: onUploadProgress,
onlyContentUri: false,
};
var avatarUri;
matrix.matrixClient
.uploadContent(response.data, opts)
.then((response) => {
avatarUri = response.content_uri;
return matrix.matrixClient.setAvatarUrl(avatarUri);
})
.then((result) => {
matrix.userAvatar = avatarUri;
resolve(result);
})
.catch((err) => {
reject(err);
});
2021-01-20 09:42:13 +01:00
})
2025-03-31 16:33:54 +02:00
.catch((err) => {
reject(err);
2021-03-23 16:20:01 +01:00
});
2025-03-31 16:33:54 +02:00
});
}
setRoomAvatar(matrixClient, roomId, file, onUploadProgress) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = (e) => {
const fileContents = e.target.result;
var data = new Uint8Array(fileContents);
const info = {
mimetype: file.type,
size: file.size,
};
2021-01-20 11:32:21 +01:00
2025-03-31 16:33:54 +02:00
const opts = {
type: file.type,
name: "Room Avatar",
progressHandler: onUploadProgress,
onlyContentUri: false,
};
2025-03-31 16:33:54 +02:00
var messageContent = {
body: file.name,
info: info,
};
2021-02-22 16:34:19 +01:00
2025-03-31 16:33:54 +02:00
matrixClient
.uploadContent(data, opts)
.then((response) => {
messageContent.url = response.content_uri;
return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent);
})
.then((result) => {
resolve(matrixClient.mxcUrlToHttp(messageContent.url, 80, 80, "scale", undefined, undefined, true));
// resolve(result);
2025-03-31 16:33:54 +02:00
})
.catch((err) => {
reject(err);
});
};
reader.onerror = (err) => {
reject(err);
};
reader.readAsArrayBuffer(file);
});
}
loadAvatarFromFile(event, onLoad) {
if (event.target.files && event.target.files[0]) {
var reader = new FileReader();
reader.onload = (e) => {
const file = event.target.files[0];
if (file.type.startsWith("image/")) {
try {
var image = e.target.result;
const buffer = Uint8Array.from(window.atob(e.target.result.replace(/^data[^,]+,/, "")), (v) =>
v.charCodeAt(0)
);
var dimens = imageSize(buffer);
2025-03-31 16:33:54 +02:00
// Need to resize?
const w = dimens.width;
const h = dimens.height;
if (w > 640 || h > 640) {
var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
imageResize(image, {
2025-03-31 16:33:54 +02:00
format: "png",
width: newWidth,
height: newHeight,
outputType: "blob",
})
2025-03-31 16:33:54 +02:00
.then((img) => {
var resizedImageFile = new File([img], file.name, {
type: img.type,
lastModified: Date.now(),
});
var reader2 = new FileReader();
reader2.onload = (e) => {
onLoad(e.target.result);
};
reader2.readAsDataURL(resizedImageFile);
})
.catch((err) => {
console.error("Resize failed:", err);
});
} else {
onLoad(image);
}
} catch (error) {
console.error("Failed to get image dimensions: " + error);
}
2023-06-25 17:12:29 +03:00
}
2025-03-31 16:33:54 +02:00
};
reader.readAsDataURL(event.target.files[0]);
2023-06-25 17:12:29 +03:00
}
2025-03-31 16:33:54 +02:00
}
/**
* Return number of whole days between the timestamps, at end of that day.
* @param {*} ts1
* @param {*} ts2
*/
dayDiff(ts1, ts2) {
var t1 = dayjs(ts1).endOf("day");
var t2 = dayjs(ts2).endOf("day");
return t2.diff(t1, "day");
}
dayDiffToday(timestamp) {
var then = dayjs(timestamp).endOf("day");
var now = dayjs().endOf("day");
return now.diff(then, "day");
}
formatDay(timestamp) {
var then = dayjs(timestamp).endOf("day");
return then.format("L");
}
formatTime(time) {
const date = new Date();
date.setTime(time);
const today = new Date();
if (
date.getDate() == today.getDate() &&
date.getMonth() == today.getMonth() &&
date.getFullYear() == today.getFullYear()
) {
// For today, skip the date part
return date.toLocaleTimeString();
2021-02-22 16:34:19 +01:00
}
2025-03-31 16:33:54 +02:00
return date.toLocaleString();
}
2021-02-22 16:34:19 +01:00
2025-03-31 16:33:54 +02:00
formatRecordDuration(ms) {
return dayjs.duration(ms).format("HH:mm:ss");
}
2021-05-07 11:30:24 +02:00
2025-03-31 16:33:54 +02:00
formatDuration(ms) {
if (ms >= 60 * 60000) {
return dayjs.duration(ms).format("H:mm:ss");
2021-02-22 16:34:19 +01:00
}
2025-03-31 16:33:54 +02:00
return dayjs.duration(ms).format("m:ss");
}
2025-03-31 16:33:54 +02:00
formatRecordStartTime(timestamp) {
var then = dayjs(timestamp);
return then.format("lll");
}
2025-03-31 16:33:54 +02:00
browserCanRecordAudio() {
return _browserCanRecordAudio;
}
2023-08-07 15:47:52 +00:00
2025-03-31 16:33:54 +02:00
getRoomNameFromAlias(alias) {
if (alias && alias.startsWith("#") && alias.indexOf(":") > 0) {
return alias.slice(1).split(":")[0];
2023-11-06 15:28:26 +00:00
}
2025-03-31 16:33:54 +02:00
return undefined;
}
getUniqueAliasForRoomName(matrixClient, roomName, defaultMatrixDomainPart, iterationCount) {
return new Promise((resolve, reject) => {
var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart;
matrixClient
.getRoomIdForAlias(tryAlias)
.then((ignoredid) => {
// We got a response, this means the tryAlias already exists.
// Try again, with appended random chars
if (iterationCount) {
// Not the first time around. Reached max tries?
if (iterationCount == 5) {
reject("Failed to get unique room alias");
return;
}
// Else, strip random chars from end so we can try again
roomName = roomName.substring(0, roomName.length - 5);
2024-07-30 18:44:46 +02:00
}
2025-03-31 16:33:54 +02:00
const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789");
resolve(
this.getUniqueAliasForRoomName(
matrixClient,
roomName + "-" + randomChars,
defaultMatrixDomainPart,
(iterationCount || 0) + 1
)
);
2023-08-07 15:47:52 +00:00
})
.catch((err) => {
2025-03-31 16:33:54 +02:00
if (err.errcode == "M_NOT_FOUND") {
resolve(preferredAlias);
} else {
reject(err);
}
2023-08-07 15:47:52 +00:00
});
2025-03-31 16:33:54 +02:00
});
}
downloadableTypes() {
return ["m.video", "m.audio", "m.image", "m.file"];
}
download(matrixClient, useAuthedMedia, event) {
console.log("DOWNLOAD");
this.getAttachment(matrixClient, useAuthedMedia, event)
.then((url) => {
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
if (!this.isFileTypePDF(event)) {
// PDFs are shown inline, not downloaded
link.download = event.getContent().body || this.$t("fallbacks.download_name");
2023-12-01 08:20:03 +00:00
}
2025-03-31 16:33:54 +02:00
console.log("LINK", link);
document.body.appendChild(link);
link.click();
setTimeout(function () {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 200);
})
.catch((err) => {
console.log("Failed to fetch attachment: ", err);
});
}
getMatrixBaseUrl(user, config) {
if (user) {
const domain = User.domainPart(user.user_id);
if (domain) {
const endpoint = config.getMatrixDomainPartMapping(domain);
if (endpoint) {
console.log("Mapped to", endpoint);
return Promise.resolve(endpoint);
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return AutoDiscovery.findClientConfig(domain)
.then((clientConfig) => {
const hs = clientConfig["m.homeserver"];
if (hs && !hs.error && hs.base_url) {
console.log("Use home server returned from well-known", hs.base_url);
return hs.base_url;
}
console.log("Fallback to default server");
return config.defaultBaseUrl;
})
.catch((err) => {
console.error("Failed well-known lookup", err);
return config.defaultBaseUrl;
});
}
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return Promise.resolve(config.defaultBaseUrl);
}
getMimeType(event) {
const content = event.getContent();
return content.info && content.info.mimetype
? content.info.mimetype
: content.file && content.file.mimetype
? content.file.mimetype
: "";
}
getFileName(event) {
const content = event.getContent();
return (content.body || content.filename || "").toLowerCase();
}
getFileExtension(event) {
const fileName = this.getFileName(event);
const parts = fileName.split(".");
if (parts.length > 1) {
return "." + parts[parts.length - 1].toLowerCase();
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return "";
}
2023-12-04 11:29:23 +01:00
2025-03-31 16:33:54 +02:00
getFileSize(event) {
const content = event.getContent();
if (content.info) {
return content.info.size;
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return 0;
}
2023-12-04 11:29:23 +01:00
2025-03-31 16:33:54 +02:00
getFileSizeFormatted(event) {
2025-05-13 16:34:12 +02:00
const size = this.getFileSize(event);
const res = prettyBytes(size);
return res;
2025-03-31 16:33:54 +02:00
}
2024-01-14 13:06:37 +02:00
2025-03-31 16:33:54 +02:00
isFileTypeAPK(event) {
const mime = this.getMimeType(event);
if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) {
return true;
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return false;
}
2023-12-04 11:29:23 +01:00
2025-03-31 16:33:54 +02:00
isFileTypeIPA(event) {
if (this.getFileName(event).endsWith(".ipa")) {
return true;
2023-12-04 11:29:23 +01:00
}
2025-03-31 16:33:54 +02:00
return false;
}
2025-03-31 16:33:54 +02:00
isFileTypePDF(event) {
const mime = this.getMimeType(event);
if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) {
return true;
}
2025-03-31 16:33:54 +02:00
return false;
}
isFileTypeZip(event) {
const mime = this.getMimeType(event);
if (
["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) ||
this.getFileName(event).endsWith(".zip")
) {
return true;
}
2025-03-31 16:33:54 +02:00
return false;
}
isMobileOrTabletBrowser() {
// Regular expression to match common mobile and tablet browser user agent strings
const mobileTabletPattern = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Tablet|Mobile|CriOS/i;
const userAgent = navigator.userAgent;
return mobileTabletPattern.test(userAgent);
}
2025-05-09 14:42:37 +02:00
/**
* Possibly convert numerals to local representation (currently only for "bo" locale)
* @param str String in which to convert numerals [0-9]
* @returns converted string
2025-05-12 17:15:11 +02:00
*/
2025-05-09 14:42:37 +02:00
toLocalNumbers = (str) => {
if (i18n.locale == "my") {
// Translate to burmese numerals
var result = "";
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
if (c >= 48 && c <= 57) {
result += String.fromCharCode(c + 0x1040 - 48);
} else {
result += String.fromCharCode(c);
}
}
return result;
} else if (i18n.locale == "bo") {
// Translate to tibetan numerals
result = "";
for (i = 0; i < str.length; i++) {
c = str.charCodeAt(i);
if (c >= 48 && c <= 57) {
result += String.fromCharCode(c + 0x0f20 - 48);
} else {
result += String.fromCharCode(c);
}
}
return result;
}
return str;
};
2025-05-12 17:15:11 +02:00
deepToRaw(sourceObj) {
const objectIterator = (input) => {
if (Array.isArray(input)) {
return input.map((item) => objectIterator(item));
}
if (isRef(input) || isReactive(input) || isProxy(input)) {
return objectIterator(toRaw(input));
}
if (input && typeof input === "object") {
return Object.keys(input).reduce((acc, key) => {
acc[key] = objectIterator(input[key]);
return acc;
}, {});
}
return input;
};
return objectIterator(sourceObj);
}
2025-06-09 09:44:37 +02:00
}
2020-11-19 17:08:58 +01:00
export default new Util();