Work on attachments
This commit is contained in:
parent
ec79a33eab
commit
842c87dc96
28 changed files with 2714 additions and 798 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import axios from "axios";
|
||||
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
|
||||
import imageResize from "image-resize";
|
||||
import { AutoDiscovery } from "matrix-js-sdk";
|
||||
import { AutoDiscovery, Method } from "matrix-js-sdk";
|
||||
import User from "../models/user";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import Hammer from "hammerjs";
|
||||
|
|
@ -12,12 +12,8 @@ import aesjs from "aes-js";
|
|||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import i18n from "./lang";
|
||||
import {
|
||||
toRaw,
|
||||
isRef,
|
||||
isReactive,
|
||||
isProxy,
|
||||
} from 'vue';
|
||||
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";
|
||||
|
|
@ -29,6 +25,9 @@ export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
|
|||
|
||||
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
|
||||
|
||||
const THUMBNAIL_MAX_WIDTH = 160;
|
||||
const THUMBNAIL_MAX_HEIGHT = 160;
|
||||
|
||||
// Install extended localized format
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(duration);
|
||||
|
|
@ -43,32 +42,6 @@ var _browserCanRecordAudioF = function () {
|
|||
};
|
||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||
|
||||
class UploadPromise {
|
||||
aborted = false;
|
||||
onAbort = undefined;
|
||||
|
||||
constructor(wrappedPromise) {
|
||||
this.wrappedPromise = wrappedPromise;
|
||||
}
|
||||
|
||||
abort() {
|
||||
this.aborted = true;
|
||||
if (this.onAbort) {
|
||||
this.onAbort();
|
||||
}
|
||||
}
|
||||
|
||||
then(resolve, reject) {
|
||||
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
|
||||
return this;
|
||||
}
|
||||
|
||||
catch(handler) {
|
||||
this.wrappedPromise = this.wrappedPromise.catch(handler);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class Util {
|
||||
threadMessageType() {
|
||||
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
|
||||
|
|
@ -90,6 +63,7 @@ class Util {
|
|||
}
|
||||
|
||||
getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
|
||||
console.error("GET ATTACHMENT FOR EVENT", event.getId());
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
var url = null;
|
||||
|
|
@ -164,94 +138,54 @@ class Util {
|
|||
});
|
||||
}
|
||||
|
||||
getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = event.getContent();
|
||||
var url = null;
|
||||
var mime = "image/png";
|
||||
var file = null;
|
||||
let decrypt = true;
|
||||
if (content.url != null) {
|
||||
url = matrixClient.mxcUrlToHttp(
|
||||
content.url,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthedMedia
|
||||
);
|
||||
decrypt = false;
|
||||
if (content.info) {
|
||||
mime = content.info.mimetype;
|
||||
}
|
||||
} else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
|
||||
file = content.info.thumbnail_file;
|
||||
// var width = w;
|
||||
// var height = h;
|
||||
// if (content.info.w < w || content.info.h < h) {
|
||||
// width = content.info.w;
|
||||
// height = content.info.h;
|
||||
// }
|
||||
// url = matrixClient.mxcUrlToHttp(
|
||||
// file.url,
|
||||
// width, height,
|
||||
// "scale",
|
||||
// true
|
||||
// );
|
||||
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;
|
||||
async getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
|
||||
console.error("GET THUMB FOR EVENT", event.getId());
|
||||
const content = event.getContent();
|
||||
var url = null;
|
||||
var mime = "image/png";
|
||||
var file = null;
|
||||
let decrypt = true;
|
||||
if (!!content.info && !!content.info.thumbnail_url) {
|
||||
url = matrixClient.mxcUrlToHttp(content.info.thumbnail_url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||
decrypt = false;
|
||||
if (content.info.thumbnail_info) {
|
||||
mime = content.info.thumbnail_info.mimetype;
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
reject("No url found!");
|
||||
return;
|
||||
} else if (content.url != null) {
|
||||
url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
|
||||
decrypt = false;
|
||||
if (content.info) {
|
||||
mime = content.info.mimetype;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
axios
|
||||
if (url == null) {
|
||||
throw new Error("No url found!");
|
||||
}
|
||||
|
||||
const response = await axios
|
||||
.get(url, useAuthedMedia ? {
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
|
||||
},
|
||||
} : { responseType: "arraybuffer" })
|
||||
.then((response) => {
|
||||
return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data });
|
||||
})
|
||||
.then((bytes) => {
|
||||
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: mime })));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Download error: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} : { responseType: "arraybuffer" });
|
||||
const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
|
||||
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
|
||||
}
|
||||
|
||||
b64toBuffer(val) {
|
||||
|
|
@ -430,7 +364,37 @@ class Util {
|
|||
});
|
||||
}
|
||||
|
||||
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) {
|
||||
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);
|
||||
console.error("HASH GENERATED", Buffer.from(hash));
|
||||
|
||||
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) {
|
||||
const uploadPromise = new UploadPromise(undefined);
|
||||
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
|
|
@ -439,131 +403,140 @@ class Util {
|
|||
reject("Aborted");
|
||||
return;
|
||||
}
|
||||
const fileContents = e.target.result;
|
||||
var data = new Uint8Array(fileContents);
|
||||
try {
|
||||
const fileContents = e.target.result;
|
||||
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
var data = new Uint8Array(fileContents);
|
||||
let thumbnailData = undefined;
|
||||
let thumbnailInfo = undefined;
|
||||
|
||||
// If audio, send duration in ms as well
|
||||
if (file.duration) {
|
||||
info.duration = file.duration;
|
||||
}
|
||||
|
||||
var description = file.name;
|
||||
var msgtype = "m.file";
|
||||
if (file.type.startsWith("image/")) {
|
||||
msgtype = "m.image";
|
||||
} else if (file.type.startsWith("audio/")) {
|
||||
msgtype = "m.audio";
|
||||
} else if (file.type.startsWith("video/")) {
|
||||
msgtype = "m.video";
|
||||
}
|
||||
|
||||
const opts = {
|
||||
type: file.type,
|
||||
name: description,
|
||||
progressHandler: onUploadProgress,
|
||||
onlyContentUri: false,
|
||||
};
|
||||
|
||||
var messageContent = {
|
||||
body: description,
|
||||
info: info,
|
||||
msgtype: msgtype,
|
||||
};
|
||||
|
||||
// If thread root (an eventId) is set, add that here
|
||||
if (threadRoot) {
|
||||
messageContent["m.relates_to"] = {
|
||||
rel_type: this.threadMessageType(),
|
||||
event_id: threadRoot,
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
}
|
||||
|
||||
// Set filename for files
|
||||
if (msgtype == "m.file") {
|
||||
messageContent.filename = file.name;
|
||||
}
|
||||
// If audio, send duration in ms as well
|
||||
if (file.duration) {
|
||||
info.duration = file.duration;
|
||||
}
|
||||
|
||||
if (!matrixClient.isRoomEncrypted(roomId)) {
|
||||
// Not encrypted.
|
||||
const promise = matrixClient.uploadContent(data, opts);
|
||||
uploadPromise.onAbort = () => {
|
||||
matrixClient.cancelUpload(promise);
|
||||
};
|
||||
promise
|
||||
.then((response) => {
|
||||
messageContent.url = response.content_uri;
|
||||
return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
|
||||
})
|
||||
.then(() => {
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
||||
})
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
return; // Don't fall through
|
||||
}
|
||||
var description = file.name;
|
||||
var msgtype = "m.file";
|
||||
if (file.type.startsWith("image/")) {
|
||||
msgtype = "m.image";
|
||||
|
||||
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
|
||||
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
|
||||
var encryptedBytes = aesCtr.encrypt(data);
|
||||
data = encryptedBytes;
|
||||
|
||||
// Calculate sha256
|
||||
let hash = await crypto.subtle.digest("SHA-256", data);
|
||||
|
||||
const jwk = {
|
||||
kty: "oct",
|
||||
key_ops: ["encrypt", "decrypt"],
|
||||
alg: "A256CTR",
|
||||
k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
|
||||
ext: true,
|
||||
};
|
||||
|
||||
const encryptedFile = {
|
||||
mimetype: file.type,
|
||||
key: jwk,
|
||||
iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
|
||||
hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
|
||||
v: "v2",
|
||||
};
|
||||
|
||||
messageContent.file = encryptedFile;
|
||||
|
||||
// Encrypted data sent as octet-stream!
|
||||
opts.type = "application/octet-stream";
|
||||
|
||||
const promise = matrixClient.uploadContent(data, opts);
|
||||
uploadPromise.onAbort = () => {
|
||||
matrixClient.cancelUpload(promise);
|
||||
};
|
||||
promise
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
return reject(response.error);
|
||||
// Generate thumbnail?
|
||||
if (dimensions) {
|
||||
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",
|
||||
}).catch(() => {return Promise.resolve(undefined)});
|
||||
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";
|
||||
}
|
||||
|
||||
var messageContent = {
|
||||
body: description,
|
||||
info: info,
|
||||
msgtype: msgtype,
|
||||
};
|
||||
|
||||
// If thread root (an eventId) is set, add that here
|
||||
if (threadRoot) {
|
||||
messageContent["m.relates_to"] = {
|
||||
rel_type: this.threadMessageType(),
|
||||
event_id: threadRoot,
|
||||
};
|
||||
}
|
||||
|
||||
// Set filename for files
|
||||
if (msgtype == "m.file") {
|
||||
messageContent.filename = file.name;
|
||||
}
|
||||
|
||||
const useEncryption = matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
const dataUploadOpts = {
|
||||
type: useEncryption ? "application/octet-stream" : file.type,
|
||||
name: description,
|
||||
progressHandler: onUploadProgress,
|
||||
onlyContentUri: false,
|
||||
};
|
||||
|
||||
if (useEncryption) {
|
||||
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
|
||||
messageContent.file = encryptedFile;
|
||||
data = encryptedBytes;
|
||||
}
|
||||
|
||||
if (thumbnailData) {
|
||||
messageContent.thumbnail_info = thumbnailInfo;
|
||||
if (useEncryption) {
|
||||
console.error("Encrypt thumb thumb");
|
||||
const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(thumbnailData, file.type);
|
||||
messageContent.info.thumbnail_file = encryptedFile;
|
||||
thumbnailData = encryptedBytes;
|
||||
}
|
||||
const thumbnailUploadOpts = {
|
||||
type: useEncryption ? "application/octet-stream" : file.type,
|
||||
name: "thumb:" + description,
|
||||
progressHandler: onUploadProgress,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const dataUploadPromise = matrixClient.uploadContent(data, dataUploadOpts);
|
||||
uploadPromise.onAbort = () => {
|
||||
matrixClient.cancelUpload(dataUploadPromise);
|
||||
};
|
||||
const response = await dataUploadPromise;
|
||||
if (useEncryption) {
|
||||
messageContent.file.url = response.content_uri;
|
||||
return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
|
||||
})
|
||||
.then(() => {
|
||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
||||
})
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
messageContent.url = response.content_uri;
|
||||
}
|
||||
|
||||
// Generate audio waveforms
|
||||
if (msgtype == "m.audio") {
|
||||
this.generateWaveform(fileContents, messageContent);
|
||||
}
|
||||
|
||||
const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
reject(err);
|
||||
|
|
@ -1248,5 +1221,5 @@ class Util {
|
|||
};
|
||||
return objectIterator(sourceObj);
|
||||
}
|
||||
};
|
||||
}
|
||||
export default new Util();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue