SHA256
НЕ ПРОВЕРЕНО: откат DM-вложений, оставлены ревизии и удаление
This commit is contained in:
@@ -917,7 +917,6 @@ async function init() {
|
||||
unread: isIncomingForCurrent,
|
||||
rawBlobB64: blobB64,
|
||||
revisionTimeMs: Number(parsed.revisionTimeMs || 0),
|
||||
attachments: Array.isArray(parsed.bodyAttachments) ? parsed.bodyAttachments : [],
|
||||
deleted: Boolean(parsed.deleted),
|
||||
});
|
||||
if (added) {
|
||||
|
||||
+10
-132
@@ -166,35 +166,6 @@ function resolveDeliveryStatus(msg) {
|
||||
return '…';
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
const value = Number(bytes || 0);
|
||||
if (!Number.isFinite(value) || value < 1024) return `${Math.max(0, Math.trunc(value))} B`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
||||
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function messagePlainText(msg) {
|
||||
return String(msg?.text || '').trim();
|
||||
}
|
||||
|
||||
async function downloadAttachment(attachment) {
|
||||
const fileName = String(attachment?.fileName || 'file.bin');
|
||||
const mime = String(attachment?.mime || 'application/octet-stream');
|
||||
const plainBytes = await authService.downloadAndDecryptDmAttachment(attachment, state.entrySettings.shineServerHttp);
|
||||
const blob = new Blob([plainBytes], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
try {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} finally {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToLatestMessage(list) {
|
||||
if (!list) return;
|
||||
const apply = () => {
|
||||
@@ -228,35 +199,10 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
|
||||
const bubbleKind = String(msg?.kind || '').trim();
|
||||
bubble.className = `bubble ${msg.from}${bubbleKind ? ` ${bubbleKind}` : ''}`;
|
||||
|
||||
const plainText = messagePlainText(msg);
|
||||
if (plainText) {
|
||||
const textNode = document.createElement('div');
|
||||
textNode.className = 'bubble-text';
|
||||
textNode.textContent = plainText;
|
||||
bubble.append(textNode);
|
||||
}
|
||||
|
||||
const attachments = Array.isArray(msg?.attachments) ? msg.attachments : [];
|
||||
if (attachments.length) {
|
||||
const attachmentsNode = document.createElement('div');
|
||||
attachmentsNode.className = 'stack';
|
||||
attachments.forEach((attachment) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'secondary-btn';
|
||||
btn.textContent = `${attachment?.fileName || 'file'} • ${formatFileSize(attachment?.origSize || attachment?.encSize || 0)}`;
|
||||
btn.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await downloadAttachment(attachment);
|
||||
} catch (error) {
|
||||
showToast(`Не удалось скачать файл: ${error?.message || 'unknown'}`, { kind: 'error', timeoutMs: 1600 });
|
||||
}
|
||||
});
|
||||
attachmentsNode.append(btn);
|
||||
});
|
||||
bubble.append(attachmentsNode);
|
||||
}
|
||||
const textNode = document.createElement('div');
|
||||
textNode.className = 'bubble-text';
|
||||
textNode.textContent = msg.text || '';
|
||||
bubble.append(textNode);
|
||||
|
||||
const metaNode = document.createElement('div');
|
||||
metaNode.className = 'bubble-meta';
|
||||
@@ -388,64 +334,17 @@ export function render({ navigate, route }) {
|
||||
const form = document.createElement('form');
|
||||
form.className = 'chat-input dm-chat-input';
|
||||
form.innerHTML = `
|
||||
<input type="file" id="chat-file-input" multiple hidden />
|
||||
<div class="stack" id="chat-attachments-preview"></div>
|
||||
<textarea class="input dm-input" name="message" rows="1" placeholder="Введите сообщение" maxlength="12000"></textarea>
|
||||
<div class="dm-actions-col">
|
||||
<button class="ghost-btn dm-voice-btn" type="button" id="chat-file-pick" title="Вложить файлы">📎</button>
|
||||
<button class="ghost-btn dm-voice-btn" type="button" id="chat-voice-input" title="Голосовой ввод">🎤</button>
|
||||
<button class="primary-btn dm-send-btn dm-send-icon-btn" type="submit" title="Отправить">➤</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fileInput = form.querySelector('#chat-file-input');
|
||||
const attachmentsPreview = form.querySelector('#chat-attachments-preview');
|
||||
let pendingFiles = [];
|
||||
|
||||
const renderPendingFiles = () => {
|
||||
if (!attachmentsPreview) return;
|
||||
attachmentsPreview.innerHTML = '';
|
||||
pendingFiles.forEach((file, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'meta-muted';
|
||||
row.textContent = `${file.name} • ${formatFileSize(file.size)}`;
|
||||
row.addEventListener('click', () => {
|
||||
pendingFiles = pendingFiles.filter((_, current) => current !== index);
|
||||
renderPendingFiles();
|
||||
});
|
||||
attachmentsPreview.append(row);
|
||||
});
|
||||
};
|
||||
|
||||
const buildMessagePayloadText = (plainText, preparedAttachments) => {
|
||||
const parts = [];
|
||||
const text = String(plainText || '').trim();
|
||||
if (text) parts.push(text);
|
||||
preparedAttachments.forEach((item) => {
|
||||
parts.push(`<<file:file-format(1.0):${item.type}|${item.fileName}|${item.origSize}|${item.origHashB64u}|${item.encHashB64u}|${item.encSize}|${item.keyB64u}|${item.nonceB64u}>>`);
|
||||
});
|
||||
return parts.join('\n');
|
||||
};
|
||||
|
||||
const ensureUploads = async (preparedAttachments) => {
|
||||
for (const item of preparedAttachments) {
|
||||
const exists = await authService.headDmFile(item.encHashB64u, state.entrySettings.shineServerHttp);
|
||||
if (!exists) {
|
||||
await authService.uploadDmFileCiphertext({
|
||||
encHashB64u: item.encHashB64u,
|
||||
encSize: item.encSize,
|
||||
ciphertextBytes: item.ciphertextBytes,
|
||||
serverHttpBase: state.entrySettings.shineServerHttp,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendTextMessage = async (rawText) => {
|
||||
const text = String(rawText || '').trim();
|
||||
if (!text && pendingFiles.length === 0) return;
|
||||
const tempLabel = text || `Файлы: ${pendingFiles.length}`;
|
||||
const tempId = addOutgoingPendingMessage(chatId, tempLabel);
|
||||
if (!text) return;
|
||||
const tempId = addOutgoingPendingMessage(chatId, text);
|
||||
renderLog(log, chatId, {
|
||||
onOpenActions: (msg) => openMessageActionsModal({
|
||||
messageText: msg?.text || '',
|
||||
@@ -460,23 +359,12 @@ export function render({ navigate, route }) {
|
||||
});
|
||||
|
||||
try {
|
||||
const filesToSend = pendingFiles.slice(0, 12);
|
||||
const preparedAttachments = [];
|
||||
for (const file of filesToSend) {
|
||||
preparedAttachments.push(await authService.prepareEncryptedDmAttachment(file));
|
||||
}
|
||||
await ensureUploads(preparedAttachments);
|
||||
const messagePayloadText = buildMessagePayloadText(text, preparedAttachments);
|
||||
const result = await authService.sendDirectMessageWithAttachments({
|
||||
const result = await authService.sendDirectMessage({
|
||||
login: state.session.login,
|
||||
toLogin: chatId,
|
||||
text: messagePayloadText,
|
||||
text,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
attachments: preparedAttachments,
|
||||
});
|
||||
pendingFiles = [];
|
||||
if (fileInput) fileInput.value = '';
|
||||
renderPendingFiles();
|
||||
markOutgoingSent(tempId, {
|
||||
messageKey: result?.outgoingKey || '',
|
||||
baseKey: result?.baseKey || result?.localBaseKey || '',
|
||||
@@ -494,7 +382,6 @@ export function render({ navigate, route }) {
|
||||
unread: false,
|
||||
rawBlobB64: result.localOutgoingBlobB64,
|
||||
revisionTimeMs: Number(parsed?.revisionTimeMs || 0),
|
||||
attachments: Array.isArray(parsed?.bodyAttachments) ? parsed.bodyAttachments : [],
|
||||
deleted: Boolean(parsed?.deleted),
|
||||
});
|
||||
} catch {
|
||||
@@ -551,15 +438,6 @@ export function render({ navigate, route }) {
|
||||
};
|
||||
|
||||
const input = form.elements.message;
|
||||
form.querySelector('#chat-file-pick')?.addEventListener('click', () => fileInput?.click());
|
||||
fileInput?.addEventListener('change', () => {
|
||||
const selected = Array.from(fileInput.files || []);
|
||||
if (selected.length > 12) {
|
||||
showToast('Можно приложить не больше 12 файлов', { kind: 'error', timeoutMs: 1400 });
|
||||
}
|
||||
pendingFiles = selected.slice(0, 12);
|
||||
renderPendingFiles();
|
||||
});
|
||||
autoResizeComposer(input);
|
||||
input?.addEventListener('input', () => autoResizeComposer(input));
|
||||
input?.addEventListener('focus', () => {
|
||||
@@ -584,7 +462,7 @@ export function render({ navigate, route }) {
|
||||
}
|
||||
event.preventDefault();
|
||||
const text = String(input.value || '').trim();
|
||||
if (!text && pendingFiles.length === 0) return;
|
||||
if (!text) return;
|
||||
input.value = '';
|
||||
autoResizeComposer(input);
|
||||
await sendTextMessage(text);
|
||||
@@ -608,7 +486,7 @@ export function render({ navigate, route }) {
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const text = input.value.trim();
|
||||
if (!text && pendingFiles.length === 0) return;
|
||||
if (!text) return;
|
||||
input.value = '';
|
||||
autoResizeComposer(input);
|
||||
await sendTextMessage(text);
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { WsJsonClient } from './ws-client.js';
|
||||
import {
|
||||
base64ToBytes,
|
||||
base64UrlToBytes,
|
||||
bytesToBase64,
|
||||
bytesToBase64Url,
|
||||
decryptBytesAesGcm,
|
||||
deriveEd25519FromMasterSecret,
|
||||
deriveMasterSecretFromPassword,
|
||||
encryptBytesAesGcm,
|
||||
exportEd25519PublicKeyB64,
|
||||
exportPkcs8B64,
|
||||
generateEd25519Pair,
|
||||
importPkcs8Ed25519,
|
||||
publicKeyB64FromPkcs8Ed25519,
|
||||
randomBytes,
|
||||
randomBase64,
|
||||
sha256Bytes,
|
||||
signBytes,
|
||||
@@ -212,7 +207,6 @@ const DM2_TYPE_READ_INCOMING = 3;
|
||||
const DM2_TYPE_READ_OUTGOING_COPY = 4;
|
||||
const DM_FORMAT_VERSION_MAJOR = 1;
|
||||
const DM_FORMAT_VERSION_MINOR = 0;
|
||||
const DM_MAX_ATTACHMENTS = 12;
|
||||
const DM_MAX_ENCRYPTED_BODY_BYTES = 16384;
|
||||
|
||||
function ensureAsciiBytes(value, field, min = 1, max = 60) {
|
||||
@@ -248,51 +242,6 @@ function buildReadReceiptPayloadBytes({ refToLogin, refFromLogin, refTimeMs, ref
|
||||
);
|
||||
}
|
||||
|
||||
function buildDmAttachmentsSectionBytes(attachments = []) {
|
||||
const list = Array.isArray(attachments) ? attachments : [];
|
||||
if (list.length > DM_MAX_ATTACHMENTS) {
|
||||
throw new Error(`Вложений должно быть не больше ${DM_MAX_ATTACHMENTS}`);
|
||||
}
|
||||
const parts = [uint8Bytes(list.length)];
|
||||
list.forEach((item, index) => {
|
||||
const hashB64u = String(item?.encHashB64u || '').trim();
|
||||
const hashBytes = base64UrlToBytes(hashB64u);
|
||||
if (hashBytes.length !== 32) throw new Error(`Некорректный encHash у вложения #${index + 1}`);
|
||||
const encSize = Number(item?.encSize ?? item?.encFileSize ?? 0);
|
||||
if (!Number.isFinite(encSize) || encSize < 0) throw new Error(`Некорректный encSize у вложения #${index + 1}`);
|
||||
parts.push(hashBytes, uint64Bytes(encSize));
|
||||
});
|
||||
return concatBytes(...parts);
|
||||
}
|
||||
|
||||
function parseDmTextAttachments(text) {
|
||||
const raw = String(text || '');
|
||||
const regex = /<<file:file-format\(1\.0\):([^>|]+)\|([^>|]+)\|(\d+)\|([^>|]+)\|([^>|]+)\|(\d+)\|([^>|]+)\|([^>|]+)>>/g;
|
||||
const attachments = [];
|
||||
let cleaned = '';
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = regex.exec(raw)) !== null) {
|
||||
cleaned += raw.slice(lastIndex, match.index);
|
||||
lastIndex = match.index + match[0].length;
|
||||
attachments.push({
|
||||
type: String(match[1] || 'file'),
|
||||
fileName: String(match[2] || ''),
|
||||
origSize: Number(match[3] || 0),
|
||||
origHashB64u: String(match[4] || ''),
|
||||
encHashB64u: String(match[5] || ''),
|
||||
encSize: Number(match[6] || 0),
|
||||
keyB64u: String(match[7] || ''),
|
||||
nonceB64u: String(match[8] || ''),
|
||||
});
|
||||
}
|
||||
cleaned += raw.slice(lastIndex);
|
||||
return {
|
||||
text: cleaned.replace(/\n{3,}/g, '\n\n').trim(),
|
||||
attachments,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSignedMessageBlockBytes(bytes) {
|
||||
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
let o = 0;
|
||||
@@ -348,17 +297,7 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
const messageType = readU16();
|
||||
const revisionTimeMs = readU64();
|
||||
const attachmentsCount = readU8();
|
||||
if (attachmentsCount > DM_MAX_ATTACHMENTS) throw new Error('TOO_MANY_ATTACHMENTS');
|
||||
const attachments = [];
|
||||
for (let i = 0; i < attachmentsCount; i += 1) {
|
||||
const hashBytes = read(32);
|
||||
const encSize = readU64();
|
||||
attachments.push({
|
||||
encHashBytes: hashBytes,
|
||||
encHashB64u: bytesToBase64Url(hashBytes),
|
||||
encSize,
|
||||
});
|
||||
}
|
||||
if (attachmentsCount !== 0) throw new Error('ATTACHMENTS_DISABLED');
|
||||
const encryptedBodyLen = readU32();
|
||||
const encryptedBodyBytes = read(encryptedBodyLen);
|
||||
const signatureBytes = read(64);
|
||||
@@ -367,7 +306,6 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
const baseKey = dm2BaseKey({ toLogin, fromLogin, timeMs, nonce });
|
||||
const messageKey = dm2MessageKey({ toLogin, fromLogin, timeMs, nonce, messageType });
|
||||
const bodyText = new TextDecoder().decode(encryptedBodyBytes);
|
||||
const parsedBody = parseDmTextAttachments(bodyText);
|
||||
return {
|
||||
toLogin,
|
||||
fromLogin,
|
||||
@@ -377,11 +315,10 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
revisionTimeMs,
|
||||
formatVersionMajor,
|
||||
formatVersionMinor,
|
||||
attachments,
|
||||
encryptedBodyBytes,
|
||||
encryptedBodyText: bodyText,
|
||||
text: parsedBody.text,
|
||||
bodyAttachments: parsedBody.attachments,
|
||||
text: bodyText,
|
||||
bodyAttachments: [],
|
||||
payloadBytes: encryptedBodyBytes,
|
||||
signatureBytes,
|
||||
signedBody,
|
||||
@@ -418,7 +355,6 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
nonce,
|
||||
messageType,
|
||||
revisionTimeMs: 0,
|
||||
attachments: [],
|
||||
encryptedBodyBytes: payloadBytes,
|
||||
encryptedBodyText: new TextDecoder().decode(payloadBytes),
|
||||
text: new TextDecoder().decode(payloadBytes),
|
||||
@@ -1981,7 +1917,6 @@ export class AuthService {
|
||||
nonce,
|
||||
messageType,
|
||||
revisionTimeMs = 0,
|
||||
attachments = [],
|
||||
encryptedBodyBytes = new Uint8Array(0),
|
||||
}) {
|
||||
const cleanFromLogin = String(login || '').trim();
|
||||
@@ -1999,7 +1934,6 @@ export class AuthService {
|
||||
|
||||
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
|
||||
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
|
||||
const attachmentsSection = buildDmAttachmentsSectionBytes(attachments);
|
||||
const preimage = concatBytes(
|
||||
DM_PREFIX_V1,
|
||||
uint8Bytes(DM_FORMAT_VERSION_MAJOR),
|
||||
@@ -2010,7 +1944,7 @@ export class AuthService {
|
||||
uint32Bytes(nonce),
|
||||
uint16Bytes(messageType),
|
||||
uint64Bytes(revisionTimeMs),
|
||||
attachmentsSection,
|
||||
uint8Bytes(0),
|
||||
uint32Bytes(encryptedBodyBytes.length),
|
||||
encryptedBodyBytes,
|
||||
);
|
||||
@@ -2073,25 +2007,13 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async sendDirectMessage({ login, toLogin, text, storagePwd }) {
|
||||
return this.sendDirectMessageWithAttachments({ login, toLogin, text, storagePwd, attachments: [] });
|
||||
}
|
||||
|
||||
async sendDirectMessageWithAttachments({
|
||||
login,
|
||||
toLogin,
|
||||
text,
|
||||
storagePwd,
|
||||
attachments = [],
|
||||
timeMs = Date.now(),
|
||||
nonce = Math.floor(Math.random() * 0x100000000),
|
||||
revisionTimeMs = 0,
|
||||
}) {
|
||||
const cleanFromLogin = String(login || '').trim();
|
||||
const cleanToLogin = String(toLogin || '').trim();
|
||||
const cleanText = String(text || '');
|
||||
const normalizedAttachments = Array.isArray(attachments) ? attachments : [];
|
||||
if (!cleanFromLogin || !cleanToLogin) throw new Error('Не передан login/toLogin');
|
||||
if (!cleanText && normalizedAttachments.length === 0) throw new Error('Пустое сообщение');
|
||||
if (!cleanText) throw new Error('Пустое сообщение');
|
||||
const timeMs = Date.now();
|
||||
const nonce = Math.floor(Math.random() * 0x100000000);
|
||||
const encryptedBodyBytes = utf8Bytes(cleanText);
|
||||
|
||||
const incomingBlock = await this.buildSignedDmV1Block({
|
||||
@@ -2101,8 +2023,6 @@ export class AuthService {
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_INCOMING,
|
||||
revisionTimeMs,
|
||||
attachments: normalizedAttachments,
|
||||
encryptedBodyBytes,
|
||||
});
|
||||
const outgoingBlock = await this.buildSignedDmV1Block({
|
||||
@@ -2112,8 +2032,6 @@ export class AuthService {
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_OUTGOING_COPY,
|
||||
revisionTimeMs,
|
||||
attachments: normalizedAttachments,
|
||||
encryptedBodyBytes,
|
||||
});
|
||||
|
||||
@@ -2164,90 +2082,6 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
getHttpBaseUrl(serverHttpBase = '') {
|
||||
const explicit = String(serverHttpBase || '').trim();
|
||||
if (explicit) return explicit.replace(/\/$/, '');
|
||||
try {
|
||||
const parsed = new URL(this.serverUrl);
|
||||
parsed.protocol = parsed.protocol === 'wss:' ? 'https:' : 'http:';
|
||||
parsed.pathname = '';
|
||||
parsed.search = '';
|
||||
parsed.hash = '';
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
buildDmFileUrl(encHashB64u, serverHttpBase = '') {
|
||||
const base = this.getHttpBaseUrl(serverHttpBase);
|
||||
return `${base}/f/${encodeURIComponent(String(encHashB64u || '').trim())}`;
|
||||
}
|
||||
|
||||
async headDmFile(encHashB64u, serverHttpBase = '') {
|
||||
const url = this.buildDmFileUrl(encHashB64u, serverHttpBase);
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
return response.status === 200;
|
||||
}
|
||||
|
||||
async uploadDmFileCiphertext({ encHashB64u, encSize, ciphertextBytes, serverHttpBase = '' }) {
|
||||
const base = this.getHttpBaseUrl(serverHttpBase);
|
||||
const url = `${base}/upload?hash=${encodeURIComponent(String(encHashB64u || '').trim())}&size=${encodeURIComponent(String(encSize || 0))}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: ciphertextBytes,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`upload_failed_${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async prepareEncryptedDmAttachment(file) {
|
||||
if (!(file instanceof File)) throw new Error('Ожидался File');
|
||||
const fileName = String(file.name || 'file.bin');
|
||||
if (!fileName || /[|:>\n\r]/.test(fileName)) {
|
||||
throw new Error('Имя файла содержит запрещённые символы для DM-протокола');
|
||||
}
|
||||
const plainBytes = new Uint8Array(await file.arrayBuffer());
|
||||
const origHashBytes = await sha256Bytes(plainBytes);
|
||||
const aesKeyBytes = randomBytes(32);
|
||||
const ivBytes = randomBytes(12);
|
||||
const cipherBytes = await encryptBytesAesGcm(plainBytes, aesKeyBytes, ivBytes);
|
||||
const encHashBytes = await sha256Bytes(cipherBytes);
|
||||
const type = String(file.type || '').startsWith('image/')
|
||||
? 'photo'
|
||||
: (String(file.type || '').startsWith('video/')
|
||||
? 'video'
|
||||
: (String(file.type || '').startsWith('audio/') ? 'audio' : 'file'));
|
||||
return {
|
||||
type,
|
||||
mime: String(file.type || 'application/octet-stream'),
|
||||
fileName,
|
||||
origSize: plainBytes.length,
|
||||
origHashB64u: bytesToBase64Url(origHashBytes),
|
||||
encHashB64u: bytesToBase64Url(encHashBytes),
|
||||
encSize: cipherBytes.length,
|
||||
keyB64u: bytesToBase64Url(aesKeyBytes),
|
||||
nonceB64u: bytesToBase64Url(ivBytes),
|
||||
ciphertextBytes: cipherBytes,
|
||||
};
|
||||
}
|
||||
|
||||
async downloadAndDecryptDmAttachment(attachment, serverHttpBase = '') {
|
||||
const encHashB64u = String(attachment?.encHashB64u || '').trim();
|
||||
if (!encHashB64u) throw new Error('Не указан encHashB64u');
|
||||
const response = await fetch(this.buildDmFileUrl(encHashB64u, serverHttpBase));
|
||||
if (!response.ok) throw new Error(`download_failed_${response.status}`);
|
||||
const cipherBytes = new Uint8Array(await response.arrayBuffer());
|
||||
const keyBytes = base64UrlToBytes(String(attachment?.keyB64u || '').trim());
|
||||
const nonceBytes = base64UrlToBytes(String(attachment?.nonceB64u || '').trim());
|
||||
return decryptBytesAesGcm(cipherBytes, keyBytes, nonceBytes);
|
||||
}
|
||||
|
||||
|
||||
async callInviteBroadcast({ toLogin, callId, type = 100 }) {
|
||||
const response = await this.ws.request('CallInviteBroadcast', { toLogin, callId, type });
|
||||
|
||||
@@ -384,7 +384,6 @@ function persistMessageRecord(chatId, row) {
|
||||
secondTick: Boolean(row.secondTick),
|
||||
readReceiptSent: Boolean(row.readReceiptSent),
|
||||
refBaseKey: String(row.refBaseKey || ''),
|
||||
attachments: Array.isArray(row.attachments) ? row.attachments : [],
|
||||
ts: resolvedTs > 0 ? resolvedTs : Date.now(),
|
||||
}).catch(() => {});
|
||||
}
|
||||
@@ -420,7 +419,6 @@ export async function hydrateMessagesFromStore() {
|
||||
secondTick: Boolean(row.secondTick),
|
||||
readReceiptSent: Boolean(row.readReceiptSent),
|
||||
refBaseKey: String(row.refBaseKey || ''),
|
||||
attachments: Array.isArray(row.attachments) ? row.attachments : [],
|
||||
createdAtMs: Number(row.ts || 0),
|
||||
});
|
||||
});
|
||||
@@ -575,7 +573,6 @@ export function addSignedMessageToChat({
|
||||
rawBlobB64 = '',
|
||||
refBaseKey = '',
|
||||
revisionTimeMs = 0,
|
||||
attachments = [],
|
||||
deleted = false,
|
||||
} = {}) {
|
||||
const id = String(messageKey || '').trim();
|
||||
@@ -603,7 +600,6 @@ export function addSignedMessageToChat({
|
||||
row.messageType = Number(messageType || 0);
|
||||
row.rawBlobB64 = String(rawBlobB64 || '');
|
||||
row.revisionTimeMs = Number(revisionTimeMs || 0);
|
||||
row.attachments = Array.isArray(attachments) ? attachments : [];
|
||||
row.unread = row.from === 'in' ? Boolean(unread) : false;
|
||||
row.refBaseKey = String(refBaseKey || '');
|
||||
row.firstTick = row.from === 'out';
|
||||
|
||||
Reference in New Issue
Block a user