SHA256
Сократить служебные теги SHiNE и сохранить обратную совместимость
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from './arweave-file-service.js';
|
||||
|
||||
const ATTACH_PREFIX = '<SHiNE:attach;';
|
||||
const ATTACH_BLOCK_RE = /^<SHiNE:attach;([^>]*)>\n?/u;
|
||||
const ATTACH_BLOCK_RE = /^<(?:SHiNE|S):(attach|att);([^>]*)>\n?/u;
|
||||
export const MAX_MESSAGE_ATTACHMENTS = 10;
|
||||
const RECENT_UNAVAILABLE_MS = 20 * 60 * 1000;
|
||||
const IMAGE_EXTENSIONS = new Set(['apng', 'avif', 'bmp', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp']);
|
||||
@@ -33,8 +32,8 @@ function normalizeName(name) {
|
||||
}
|
||||
|
||||
function normalizePreview(input = {}) {
|
||||
const previewTxId = String(input.previewAr || input.ar || input.txId || '').trim();
|
||||
const previewSha256Hex = String(input.previewSha256 || input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
const previewTxId = String(input.preAr || input.previewAr || input.ar || input.txId || '').trim();
|
||||
const previewSha256Hex = String(input.preSha256 || input.previewSha256 || input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
if (!previewTxId || !previewSha256Hex) return null;
|
||||
if (!validateArweaveTxId(previewTxId)) return null;
|
||||
if (!validateSha256Hex(previewSha256Hex)) return null;
|
||||
@@ -47,9 +46,11 @@ function normalizePreview(input = {}) {
|
||||
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');
|
||||
const size = Number(input.sz || input.size || input.sizeBytes || 0);
|
||||
const name = normalizeName(input.nm || input.name || input.fileName || 'file');
|
||||
const preview = normalizePreview(input.preview || {
|
||||
preAr: input.preAr,
|
||||
preSha256: input.preSha256,
|
||||
previewAr: input.previewAr,
|
||||
previewSha256: input.previewSha256,
|
||||
});
|
||||
@@ -75,9 +76,9 @@ export function buildAttachmentBlock(attachment) {
|
||||
const item = normalizeAttachment(attachment);
|
||||
const encodedName = encodeURIComponent(item.name);
|
||||
const previewFields = item.preview
|
||||
? `;previewAr=${item.preview.ar};previewSha256=${item.preview.sha256}`
|
||||
? `;preAr=${item.preview.ar};preSha256=${item.preview.sha256}`
|
||||
: '';
|
||||
return `<SHiNE:attach;v=${item.preview ? 2 : 1};name=${encodedName};size=${item.size};sha256=${item.sha256};ar=${item.ar}${previewFields}>`;
|
||||
return `<S:att;v=1;nm=${encodedName};sz=${item.size};sha256=${item.sha256};ar=${item.ar}${previewFields}>`;
|
||||
}
|
||||
|
||||
export function composeMessageWithAttachments(text, attachments = []) {
|
||||
@@ -105,16 +106,22 @@ export function parseMessageAttachments(rawText) {
|
||||
let rest = String(rawText || '');
|
||||
const attachments = [];
|
||||
|
||||
while (rest.startsWith(ATTACH_PREFIX)) {
|
||||
while (true) {
|
||||
const match = rest.match(ATTACH_BLOCK_RE);
|
||||
if (!match) break;
|
||||
const fields = parseFields(match[1]);
|
||||
const fields = parseFields(match[2]);
|
||||
try {
|
||||
const version = Number(fields.v || 0);
|
||||
if (version !== 1 && version !== 2) {
|
||||
throw new Error('unsupported attachment version');
|
||||
}
|
||||
attachments.push(normalizeAttachment({
|
||||
name: decodeURIComponent(String(fields.name || 'file')),
|
||||
size: fields.size,
|
||||
nm: decodeURIComponent(String(fields.nm || fields.name || 'file')),
|
||||
sz: fields.sz || fields.size,
|
||||
sha256: fields.sha256,
|
||||
ar: fields.ar,
|
||||
preAr: fields.preAr,
|
||||
preSha256: fields.preSha256,
|
||||
previewAr: fields.previewAr,
|
||||
previewSha256: fields.previewSha256,
|
||||
}));
|
||||
|
||||
@@ -795,7 +795,7 @@ function composeChannelMetaText({ title = '', description = '', avatar = null }
|
||||
const cleanTitle = validateChannelMetaTitle(title);
|
||||
const cleanDescription = normalizeChannelMetaDescription(description);
|
||||
const rows = [];
|
||||
if (cleanTitle) rows.push(`<SHiNE:title;v=1;${cleanTitle}>`);
|
||||
if (cleanTitle) rows.push(`<S:title;v=1;${cleanTitle}>`);
|
||||
if (avatar?.ar) {
|
||||
const size = Number(avatar.size || 0);
|
||||
const sha256 = String(avatar.sha256 || '').trim().toLowerCase();
|
||||
@@ -803,7 +803,7 @@ function composeChannelMetaText({ title = '', description = '', avatar = null }
|
||||
if (!Number.isInteger(size) || size <= 0) throw new Error('Некорректный размер аватара.');
|
||||
if (!/^[0-9a-f]{64}$/u.test(sha256)) throw new Error('Некорректный SHA-256 аватара.');
|
||||
if (!/^[A-Za-z0-9_-]{43}$/u.test(ar)) throw new Error('Некорректный Arweave txId аватара.');
|
||||
rows.push(`<SHiNE:avatar;v=1;size=${size};sha256=${sha256};ar=${ar}>`);
|
||||
rows.push(`<S:ava;v=1;sz=${size};sha256=${sha256};ar=${ar}>`);
|
||||
}
|
||||
if (cleanDescription) rows.push(cleanDescription);
|
||||
return rows.join('\n');
|
||||
|
||||
@@ -11,6 +11,16 @@ function defaultParsed(rawText = '') {
|
||||
};
|
||||
}
|
||||
|
||||
function hasTechPrefix(text = '', offset = 0) {
|
||||
return String(text || '').startsWith('<SHiNE:', offset) || String(text || '').startsWith('<S:', offset);
|
||||
}
|
||||
|
||||
function getTechPrefixLength(text = '', offset = 0) {
|
||||
if (String(text || '').startsWith('<SHiNE:', offset)) return 7;
|
||||
if (String(text || '').startsWith('<S:', offset)) return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseReplyId(value = '') {
|
||||
const parts = String(value || '').split('|');
|
||||
if (parts.length !== 4) return null;
|
||||
@@ -57,41 +67,45 @@ function buildCallDisplayText(callSummary) {
|
||||
|
||||
export function sanitizeUserDmTextForSend(rawText = '') {
|
||||
const text = String(rawText || '');
|
||||
return text.startsWith('<SHiNE:') ? `< SHiNE:${text.slice(7)}` : text;
|
||||
if (text.startsWith('<SHiNE:')) return `< SHiNE:${text.slice(7)}`;
|
||||
if (text.startsWith('<S:')) return `< S:${text.slice(3)}`;
|
||||
return text;
|
||||
}
|
||||
|
||||
export function buildDmCallTechBlock({ status = '', durationSec = 0, reason = '' } = {}) {
|
||||
const cleanStatus = String(status || '').trim().toLowerCase();
|
||||
if (cleanStatus === 'completed') {
|
||||
return `<SHiNE:call;v=1;status=completed;duration=${Math.max(0, Math.floor(Number(durationSec || 0)))}>`;
|
||||
return `<S:call;v=1;status=completed;duration=${Math.max(0, Math.floor(Number(durationSec || 0)))}>`;
|
||||
}
|
||||
const cleanReason = String(reason || '').trim().toLowerCase();
|
||||
return `<SHiNE:call;v=1;status=failed;reason=${cleanReason || 'connect_failed'}>`;
|
||||
return `<S:call;v=1;status=failed;reason=${cleanReason || 'connect_failed'}>`;
|
||||
}
|
||||
|
||||
export function buildDmReplyTechBlock({ baseKey = '' } = {}) {
|
||||
const cleanBaseKey = String(baseKey || '').trim();
|
||||
if (!cleanBaseKey) return '';
|
||||
return `<SHiNE:reply;v=1;id=${cleanBaseKey}>`;
|
||||
return `<S:reply;v=1;id=${cleanBaseKey}>`;
|
||||
}
|
||||
|
||||
export function parseDmTechBlocks(rawText = '') {
|
||||
const text = String(rawText || '');
|
||||
if (!text.startsWith('<SHiNE:')) return defaultParsed(text);
|
||||
if (!hasTechPrefix(text)) return defaultParsed(text);
|
||||
|
||||
const blocks = [];
|
||||
let cursor = 0;
|
||||
let replyRef = null;
|
||||
let callSummary = null;
|
||||
|
||||
while (text.startsWith('<SHiNE:', cursor)) {
|
||||
while (hasTechPrefix(text, cursor)) {
|
||||
const end = text.indexOf('>', cursor);
|
||||
if (end < 0) {
|
||||
if (!blocks.length) return defaultParsed(text);
|
||||
break;
|
||||
}
|
||||
|
||||
const inner = text.slice(cursor + 7, end);
|
||||
const prefixLength = getTechPrefixLength(text, cursor);
|
||||
if (!prefixLength) break;
|
||||
const inner = text.slice(cursor + prefixLength, end);
|
||||
const segments = inner.split(';').map((part) => String(part || '').trim()).filter(Boolean);
|
||||
if (!segments.length) {
|
||||
if (!blocks.length) return defaultParsed(text);
|
||||
|
||||
Reference in New Issue
Block a user