SHA256
133 lines
4.4 KiB
JavaScript
133 lines
4.4 KiB
JavaScript
import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from './arweave-file-service.js';
|
|
|
|
const ATTACH_PREFIX = '<SHiNE:attach;';
|
|
const ATTACH_BLOCK_RE = /^<SHiNE:attach;([^>]*)>\n?/u;
|
|
|
|
export function escapeHtml(text) {
|
|
return String(text || '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
export function formatBytes(bytes) {
|
|
const value = Number(bytes || 0);
|
|
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
|
if (value < 1024) return `${value} B`;
|
|
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
|
return `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
|
}
|
|
|
|
function normalizeName(name) {
|
|
const clean = String(name || 'file')
|
|
.replace(/[\u0000-\u001f\u007f]/g, '')
|
|
.replace(/[\\/]/g, '_')
|
|
.trim();
|
|
return clean || 'file';
|
|
}
|
|
|
|
export function normalizeAttachment(input = {}) {
|
|
const txId = String(input.ar || input.txId || '').trim();
|
|
const sha256Hex = String(input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
|
const size = Number(input.size || input.sizeBytes || 0);
|
|
const name = normalizeName(input.name || input.fileName || 'file');
|
|
if (!validateArweaveTxId(txId)) throw new Error('Некорректный Transaction ID Arweave.');
|
|
if (!validateSha256Hex(sha256Hex)) throw new Error('Некорректный SHA-256 файла.');
|
|
if (!Number.isInteger(size) || size <= 0) throw new Error('Некорректный размер файла.');
|
|
return {
|
|
v: 1,
|
|
name,
|
|
size,
|
|
sha256: sha256Hex,
|
|
ar: txId,
|
|
uploadedAtMs: Number(input.uploadedAtMs || 0) || Date.now(),
|
|
};
|
|
}
|
|
|
|
export function buildAttachmentBlock(attachment) {
|
|
const item = normalizeAttachment(attachment);
|
|
const encodedName = encodeURIComponent(item.name);
|
|
return `<SHiNE:attach;v=1;name=${encodedName};size=${item.size};sha256=${item.sha256};ar=${item.ar}>`;
|
|
}
|
|
|
|
export function composeMessageWithAttachments(text, attachments = []) {
|
|
const cleanText = String(text || '').trim();
|
|
const rows = (Array.isArray(attachments) ? attachments : []).map(buildAttachmentBlock);
|
|
if (!rows.length) return cleanText;
|
|
return `${rows.join('\n')}${cleanText ? `\n${cleanText}` : ''}`;
|
|
}
|
|
|
|
function parseFields(rawFields) {
|
|
const out = {};
|
|
String(rawFields || '').split(';').forEach((part) => {
|
|
const eq = part.indexOf('=');
|
|
if (eq <= 0) return;
|
|
const key = part.slice(0, eq).trim();
|
|
const value = part.slice(eq + 1).trim();
|
|
if (key) out[key] = value;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
export function parseMessageAttachments(rawText) {
|
|
let rest = String(rawText || '');
|
|
const attachments = [];
|
|
|
|
while (rest.startsWith(ATTACH_PREFIX)) {
|
|
const match = rest.match(ATTACH_BLOCK_RE);
|
|
if (!match) break;
|
|
const fields = parseFields(match[1]);
|
|
try {
|
|
attachments.push(normalizeAttachment({
|
|
name: decodeURIComponent(String(fields.name || 'file')),
|
|
size: fields.size,
|
|
sha256: fields.sha256,
|
|
ar: fields.ar,
|
|
}));
|
|
} catch {
|
|
// Битый attach-блок скрываем из UI, но не ломаем отображение текста.
|
|
}
|
|
rest = rest.slice(match[0].length);
|
|
}
|
|
|
|
return {
|
|
attachments,
|
|
text: rest.replace(/^\n+/u, ''),
|
|
};
|
|
}
|
|
|
|
export function createAttachmentListElement(attachments = [], { gateway = '', compact = false } = {}) {
|
|
const items = Array.isArray(attachments) ? attachments : [];
|
|
const wrap = document.createElement('div');
|
|
wrap.className = `message-attachments${compact ? ' message-attachments--compact' : ''}`;
|
|
items.forEach((raw) => {
|
|
let item;
|
|
try {
|
|
item = normalizeAttachment(raw);
|
|
} catch {
|
|
return;
|
|
}
|
|
const url = buildArweaveDataUrl({ gateway, txId: item.ar });
|
|
const link = document.createElement('a');
|
|
link.className = 'message-attachment-card';
|
|
link.href = url;
|
|
link.target = '_blank';
|
|
link.rel = 'noopener';
|
|
link.title = 'Открыть файл в Arweave';
|
|
link.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
link.innerHTML = `
|
|
<span class="message-attachment-icon" aria-hidden="true">📎</span>
|
|
<span class="message-attachment-main">
|
|
<span class="message-attachment-name">${escapeHtml(item.name)}</span>
|
|
<span class="message-attachment-meta">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(item.ar)}</span>
|
|
</span>
|
|
`;
|
|
wrap.append(link);
|
|
});
|
|
return wrap;
|
|
}
|