vietnamese

This commit is contained in:
nnduc
2026-10-24 17:46:20 +07:00
parent 5467b0aa2c
commit a3cf10cb0a
33 changed files with 4306 additions and 94 deletions
+93 -39
View File
@@ -88,6 +88,17 @@ var ChatOmemo = {
const localDeviceId = await store.getLocalRegistrationId();
devicesIds = Object.values(devicesIds);
// A key backup was restored (Account.omemoRestore): publish it with new
// prekeys and drop the device it replaced from our list
let restored = localStorage.getObject(OMEMO_RESTORED_KEY);
if (restored && localDeviceId != undefined) {
localStorage.removeItem(OMEMO_RESTORED_KEY);
devicesIds = devicesIds.filter(id => id != restored.replaced);
ChatOmemo.refreshBundle(devicesIds, false);
ChatOmemo.refreshed = true;
return;
}
if (localDeviceId == undefined) {
ChatOmemo.initiateBundle(devicesIds);
return;
@@ -427,56 +438,99 @@ var ChatOmemo = {
? await sessionCipher.decryptPreKeyWhisperMessage(ciphertext, 'binary')
: await sessionCipher.decryptWhisperMessage(ciphertext, 'binary');
},
searchEncryptedFile: function (plaintext) {
let lines = plaintext.split('\n');
let matches = lines[0].match(AESGCM_REGEX);
// Decrypted files of this page: aesgcm URL → blob URL
decryptedFiles: new Map(),
/**
* Render an aesgcm:// message (XEP-0454) in p: pictures and audio inline,
* other files as a download link. Returns false if it is not one.
* Built with the DOM: the URL comes from the contact.
*/
renderEncryptedFile: function (p, plaintext) {
let encryptedUrl = plaintext.trim();
let matches = encryptedUrl.match(AESGCM_REGEX);
if (!matches) return false;
if (!matches) {
return plaintext;
let [, , filename, extension] = matches;
let type = AESGCM_TYPES[extension.toLowerCase()] ?? '';
p.textContent = '';
if (type.startsWith('image/') || type.startsWith('audio/')) {
let media = document.createElement(type.startsWith('image/') ? 'img' : 'audio');
media.className = 'encrypted_media';
media.title = filename;
if (media.tagName == 'AUDIO') media.controls = true;
p.appendChild(media);
ChatOmemo.decryptFile(encryptedUrl).then(url => {
media.src = url;
if (media.tagName == 'IMG') media.onclick = () => window.open(url, '_blank');
}).catch(e => {
console.log('Cannot decrypt ' + filename, e);
p.textContent = '';
p.appendChild(ChatOmemo.encryptedFileLink(encryptedUrl, filename));
});
} else {
p.appendChild(ChatOmemo.encryptedFileLink(encryptedUrl, filename));
}
let [match, , filename, extension, hash] = matches;
return '<i class="material-symbols">file_download</i> <a href="#" class="encrypted_file" onclick="ChatOmemo.getEncryptedFile(\''
+ lines[0]
+ '\')">'
+ filename
+ '</a>';
return true;
},
encryptedFileLink: function (encryptedUrl, filename) {
let fragment = document.createDocumentFragment();
let icon = document.createElement('i');
icon.className = 'material-symbols';
icon.textContent = 'file_download';
let link = document.createElement('a');
link.href = '#';
link.className = 'encrypted_file';
link.textContent = filename;
link.onclick = e => {
e.preventDefault();
ChatOmemo.getEncryptedFile(encryptedUrl);
};
fragment.append(icon, ' ', link);
return fragment;
},
// Fetch and decrypt an aesgcm:// file, returns a blob URL (cached)
decryptFile: async function (encryptedUrl) {
if (ChatOmemo.decryptedFiles.has(encryptedUrl)) {
return ChatOmemo.decryptedFiles.get(encryptedUrl);
}
let [, url, , extension, hash] = encryptedUrl.match(AESGCM_REGEX);
let response = await fetch('https://' + url);
if (!response.ok) throw new Error('HTTP ' + response.status);
let cipher = await response.arrayBuffer();
const iv = hash.slice(0, hash.length - 64);
const key = hash.slice(hash.length - 64);
const keyObj = await crypto.subtle.importKey('raw', MovimUtils.hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
const algo = {
'name': 'AES-GCM',
'iv': MovimUtils.hexToArrayBuffer(iv),
};
let plainFile = await crypto.subtle.decrypt(algo, keyObj, cipher);
let blobUrl = URL.createObjectURL(new Blob([plainFile], { type: AESGCM_TYPES[extension.toLowerCase()] ?? 'application/octet-stream' }));
ChatOmemo.decryptedFiles.set(encryptedUrl, blobUrl);
return blobUrl;
},
getEncryptedFile: async function (encryptedUrl) {
Chat.enableSending();
let [, url, filename, , hash] = encryptedUrl.match(AESGCM_REGEX);
url = 'https://' + url;
let response;
let [, , filename] = encryptedUrl.match(AESGCM_REGEX);
try {
response = await fetch(url)
} catch (e) {
console.log('Cannot get the following file: ' + url)
return null;
}
if (response.status >= 200 && response.status < 400) {
let cipher = await response.arrayBuffer();
const iv = hash.slice(0, 24);
const key = hash.slice(24);
const keyObj = await crypto.subtle.importKey('raw', MovimUtils.hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
const algo = {
'name': 'AES-GCM',
'iv': MovimUtils.hexToArrayBuffer(iv),
};
let plainFile = await crypto.subtle.decrypt(algo, keyObj, cipher);
const file = new File([plainFile], filename);
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.href = await ChatOmemo.decryptFile(encryptedUrl);
link.download = filename;
link.click();
Chat.disableSending();
URL.revokeObjectURL(link.href)
} catch (e) {
console.log('Cannot get the following file: ' + filename, e);
}
Chat.disableSending();
},
resolveContactFingerprints: function (jid) {
let omemoStorage = new ChatOmemoStorage;
@@ -512,7 +566,7 @@ var ChatOmemo = {
MovimWebsocket.attach(async () => {
var store = new ChatOmemoStorage();
const localDeviceId = await store.getLocalRegistrationId();
if (!localDeviceId) {
if (!localDeviceId || localStorage.getItem(OMEMO_RESTORED_KEY)) {
ChatOmemo_ajaxGetDevicesList(USER_JID);
}
});