Сократить служебные теги SHiNE и сохранить обратную совместимость

This commit is contained in:
AidarKC
2026-08-12 13:14:23 +04:00
parent 4d80ab639e
commit 8f32e82d14
15 changed files with 144 additions and 86 deletions
+19 -12
View File
@@ -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,
}));
+2 -2
View File
@@ -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');
+21 -7
View File
@@ -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);