From 4956ba73525c003cfcec3ed85f100e380b065cf570b3f69db1d49ceedd902268 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 8 May 2026 19:06:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B6=D0=B5=D1=81=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20=D0=B8=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20legacy=20USER=5FPARAM=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 4 +- shine-UI/js/pages/add-channel-view.js | 12 +- shine-UI/js/pages/channel-view.js | 178 +----------------- shine-UI/js/services/auth-service.js | 110 ++--------- shine-UI/js/services/channel-name-rules.js | 27 +-- .../shine/db/channels/ChannelNameRules.java | 47 +---- 6 files changed, 34 insertions(+), 344 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index a33be7a..54add94 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.42 -server.version=1.2.36 +client.version=1.2.43 +server.version=1.2.37 diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js index 3cd4e7b..475f40a 100644 --- a/shine-UI/js/pages/add-channel-view.js +++ b/shine-UI/js/pages/add-channel-view.js @@ -44,11 +44,11 @@ export function render({ navigate }) { form.className = 'card stack'; form.innerHTML = ` Создание канала -

Можно использовать кириллицу, латиницу, цифры, пробел, _ и -.

+

Можно использовать только латиницу, цифры, _ и -.

Длина названия: от 3 до 32 символов. Название уникально во всей системе.

- +
@@ -124,18 +124,14 @@ export function render({ navigate }) { errorEl.textContent = ''; try { - const created = await authService.addBlockCreateChannel({ + await authService.addBlockCreateChannel({ login, storagePwd, channelName: normalizeChannelDisplayName(check.name), channelDescription: normalizeChannelDescription(check.description), }); - const baseMessage = `Канал "${normalizeChannelDisplayName(check.name)}" создан.`; - const successMessage = created?.usedLegacyDescriptionFallback && created?.savedDescriptionViaUserParam - ? `${baseMessage} Описание сохранено через блок параметра.` - : baseMessage; - persistCreateSuccessFlash(successMessage); + persistCreateSuccessFlash(`Канал "${normalizeChannelDisplayName(check.name)}" создан.`); navigate('channels-list'); } catch (error) { errorEl.textContent = toUserMessage(error, 'Не удалось создать канал.'); diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index df91883..c2cc1ee 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -123,41 +123,6 @@ function buildAbsoluteRouteUrl(routePath = '') { return url.toString(); } -function channelDescriptionParamKey(selector) { - const owner = String(selector?.ownerBlockchainName || '').trim(); - const rootNo = Number(selector?.channelRootBlockNumber); - const rootHash = normalizeRouteHash(selector?.channelRootBlockHash); - if (!owner || !Number.isFinite(rootNo)) return ''; - return `channel_desc:${owner}:${rootNo}:${rootHash}`; -} - -function parseDescriptionOverride(payload) { - if (!payload || typeof payload !== 'object') { - return { hasOverride: false, description: '' }; - } - - const rawValue = String(payload?.value ?? payload?.param_value ?? '').trim(); - if (!rawValue && !Number(payload?.time_ms || payload?.timeMs || 0)) { - return { hasOverride: false, description: '' }; - } - - if (!rawValue) { - return { hasOverride: true, description: '' }; - } - - try { - const parsed = JSON.parse(rawValue); - if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { - const value = typeof parsed.v === 'string' ? parsed.v : ''; - return { hasOverride: true, description: value.trim() }; - } - } catch { - // legacy raw string value - } - - return { hasOverride: true, description: rawValue }; -} - function buildSelectorFromRoute(route, channelId) { const params = route?.params || {}; @@ -410,88 +375,6 @@ function openAddMessageModal({ channelName, onSubmit }) { if (textEl) textEl.focus(); } -function openEditDescriptionModal({ initialValue = '', onSubmit }) { - const root = document.getElementById('modal-root'); - root.innerHTML = ` - - `; - - const textEl = root.querySelector('#channel-description-text'); - const counterEl = root.querySelector('#channel-description-counter'); - const errorEl = root.querySelector('#channel-description-error'); - const submitEl = root.querySelector('#channel-description-submit'); - const cancelEl = root.querySelector('#channel-description-cancel'); - - let inFlight = false; - - const compute = () => { - const value = String(textEl?.value || '').replace(/\s+/g, ' ').trim(); - const bytes = new TextEncoder().encode(value).length; - const ok = bytes <= 200; - return { - value, - bytes, - ok, - error: ok ? '' : 'Описание слишком длинное: максимум 200 байт UTF-8.', - }; - }; - - const setBusy = (busy) => { - inFlight = !!busy; - submitEl.disabled = inFlight; - cancelEl.disabled = inFlight; - if (textEl) textEl.disabled = inFlight; - submitEl.textContent = inFlight ? 'Сохраняем...' : 'Сохранить'; - }; - - const close = () => { - root.innerHTML = ''; - }; - - const updateValidation = () => { - const check = compute(); - counterEl.textContent = `${check.bytes} / 200 байт`; - errorEl.textContent = check.error; - submitEl.disabled = inFlight || !check.ok; - return check; - }; - - cancelEl?.addEventListener('click', close); - textEl?.addEventListener('input', updateValidation); - submitEl?.addEventListener('click', async () => { - if (inFlight) return; - const check = updateValidation(); - if (!check.ok) return; - - setBusy(true); - errorEl.textContent = ''; - try { - await onSubmit(check.value); - close(); - } catch (error) { - setBusy(false); - errorEl.textContent = toUserMessage(error, 'Не удалось сохранить описание.'); - } - }); - - if (textEl) { - textEl.value = String(initialValue || ''); - textEl.focus(); - } - updateValidation(); -} - function mapApiMessageToPost(message, selector, localNumber) { const blockNumber = toSafeInt(message?.messageRef?.blockNumber); const blockHash = normalizeMessageHash(message?.messageRef?.blockHash); @@ -552,27 +435,11 @@ async function loadFromApi(route, channelId) { const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1)); const ownerLogin = String(payload.channel?.ownerLogin || '').trim(); - const readDescription = async () => { - const sourceDescription = String(payload.channel?.channelDescription || '').trim(); - const paramKey = channelDescriptionParamKey(selector); - if (!ownerLogin || !paramKey) return sourceDescription; - - try { - const paramPayload = await authService.getUserParam(ownerLogin, paramKey); - const override = parseDescriptionOverride(paramPayload); - return override.hasOverride ? override.description : sourceDescription; - } catch { - return sourceDescription; - } - }; - - const resolvedDescription = await readDescription(); - return { channel: { name: payload.channel?.channelName || 'неизвестный канал', displayName: `${ownerLogin || 'неизвестно'}/${payload.channel?.channelName || 'неизвестный канал'}`, - description: resolvedDescription, + description: String(payload.channel?.channelDescription || '').trim(), ownerName: ownerLogin || 'неизвестно', }, posts, @@ -818,22 +685,6 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) { }); headActions.append(aboutButton); - if (channelData.isOwnChannel) { - const editButton = document.createElement('button'); - editButton.type = 'button'; - editButton.className = 'secondary-btn small-btn'; - editButton.textContent = '✎'; - editButton.title = 'Редактировать описание'; - editButton.addEventListener('click', (event) => { - animatePress(event.currentTarget); - openEditDescriptionModal({ - initialValue: channelData.channel.description || '', - onSubmit: async (nextValue) => handlers.onEditDescription(nextValue), - }); - }); - headActions.append(editButton); - } - head.append(title); head.append(owner, headActions); @@ -1024,25 +875,6 @@ export function render({ navigate, route }) { rerender(); }; - const onEditDescription = async (descriptionText) => { - const { login, storagePwd } = requireSigningSession(); - const selector = routeSelector; - const param = channelDescriptionParamKey(selector); - if (!param) throw new Error('Идентификатор канала не готов для обновления описания.'); - - const value = JSON.stringify({ v: String(descriptionText || '').trim() }); - await authService.addBlockUserParam({ - login, - storagePwd, - param, - value, - }); - - softHaptic(10); - showToast('Описание канала сохранено'); - rerender(); - }; - screen.append( renderHeader({ title: '', @@ -1085,14 +917,6 @@ export function render({ navigate, route }) { } }, onShare: onShare, - onEditDescription: async (descriptionText) => { - try { - await onEditDescription(descriptionText); - showStatus(''); - } catch (error) { - throw new Error(toUserMessage(error, 'Не удалось сохранить описание.')); - } - }, onSubscribeChannel: async (event) => { animatePress(event?.currentTarget); try { diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index e789dec..ea54d9a 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -92,27 +92,6 @@ function opError(op, response) { return error; } -function isLegacyCreateChannelFormatError(error) { - const code = String(error?.code || '').trim().toUpperCase(); - const text = String(error?.message || '').toLowerCase(); - if (code === 'BAD_BLOCK_FORMAT') return true; - return ( - text.includes('unknown body type/version') || - text.includes('unknown tech body type/version/subtype') || - text.includes('bad_block_format') - ); -} - -function channelDescriptionParamKeyFromSelector(selector) { - const owner = String(selector?.ownerBlockchainName || '').trim(); - const rootNo = Number(selector?.channelRootBlockNumber); - const rootHash = String(selector?.channelRootBlockHash || '').trim().toLowerCase(); - if (!owner || !Number.isFinite(rootNo) || rootNo < 0 || !/^[0-9a-f]{64}$/.test(rootHash)) { - return ''; - } - return `channel_desc:${owner}:${rootNo}:${rootHash}`; -} - function makeClientInfo() { const ua = navigator.userAgent || 'unknown'; return ua.slice(0, 50); @@ -416,26 +395,6 @@ function makeConnectionBodyBytes({ ); } -function makeCreateChannelBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, channelName }) { - const check = validateChannelDisplayName(channelName); - if (!check.ok) throw new Error(channelNameErrorText(check.code)); - const cleanName = check.normalized; - - const nameBytes = utf8Bytes(cleanName); - if (nameBytes.length < 1 || nameBytes.length > 255) { - throw new Error('Channel name must be 1..255 bytes'); - } - - return concatBytes( - int32Bytes(lineCode), - int32Bytes(prevLineNumber), - hexToBytes(normalizeHex32(prevLineHashHex)), - int32Bytes(thisLineNumber), - int8Byte(nameBytes.length), - nameBytes - ); -} - function normalizeChannelDescription(value) { const text = String(value == null ? '' : value).trim().replace(/\s+/g, ' '); const bytes = utf8Bytes(text); @@ -1088,44 +1047,21 @@ export class AuthService { thisLineNumber = createdChannels.length + 1; } - const submitCreate = async (useV2) => { - const bodyBytes = useV2 - ? makeCreateChannelBodyV2Bytes({ - lineCode: 0, - prevLineNumber, - prevLineHashHex, - thisLineNumber, - channelName: cleanChannelName, - channelDescription: cleanChannelDescription, - }) - : makeCreateChannelBodyBytes({ - lineCode: 0, - prevLineNumber, - prevLineHashHex, - thisLineNumber, - channelName: cleanChannelName, - }); - - return this.addBlockSigned({ - login: cleanLogin, - storagePwd, - msgType: MSG_TYPE_TECH, - msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, - msgVersion: useV2 ? CREATE_CHANNEL_BODY_VERSION : 1, - bodyBytes, - }); - }; - - let payload; - let usedLegacyDescriptionFallback = false; - let savedDescriptionViaUserParam = false; - try { - payload = await submitCreate(true); - } catch (error) { - if (!isLegacyCreateChannelFormatError(error)) throw error; - payload = await submitCreate(false); - usedLegacyDescriptionFallback = true; - } + const payload = await this.addBlockSigned({ + login: cleanLogin, + storagePwd, + msgType: MSG_TYPE_TECH, + msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, + msgVersion: CREATE_CHANNEL_BODY_VERSION, + bodyBytes: makeCreateChannelBodyV2Bytes({ + lineCode: 0, + prevLineNumber, + prevLineHashHex, + thisLineNumber, + channelName: cleanChannelName, + channelDescription: cleanChannelDescription, + }), + }); const selector = { ownerBlockchainName: blockchainName, @@ -1133,24 +1069,8 @@ export class AuthService { channelRootBlockHash: normalizeHex32(payload?.serverLastGlobalHash, ZERO64), }; - if (usedLegacyDescriptionFallback && cleanChannelDescription) { - const param = channelDescriptionParamKeyFromSelector(selector); - if (!param) { - throw new Error('Не удалось сохранить описание канала: некорректный идентификатор канала.'); - } - await this.addBlockUserParam({ - login: cleanLogin, - storagePwd, - param, - value: JSON.stringify({ v: cleanChannelDescription }), - }); - savedDescriptionViaUserParam = true; - } - return { ...payload, - usedLegacyDescriptionFallback, - savedDescriptionViaUserParam, channel: { ...selector, }, diff --git a/shine-UI/js/services/channel-name-rules.js b/shine-UI/js/services/channel-name-rules.js index de45ea6..80ba197 100644 --- a/shine-UI/js/services/channel-name-rules.js +++ b/shine-UI/js/services/channel-name-rules.js @@ -1,10 +1,10 @@ const MIN_LEN = 3; const MAX_LEN = 32; -const ALLOWED_CHARS_RE = /^[\p{Script=Latin}\p{Script=Cyrillic}0-9 _-]+$/u; +const ALLOWED_CHARS_RE = /^[A-Za-z0-9_-]+$/; export function normalizeChannelDisplayName(value) { if (value == null) return ''; - return String(value).trim().replace(/\s+/g, ' '); + return String(value).trim(); } export function normalizeChannelDescription(value) { @@ -16,24 +16,9 @@ export function toCanonicalChannelSlug(value) { const normalized = normalizeChannelDisplayName(value); if (!normalized) return ''; - const lowered = normalized.toLowerCase().replace(/\u0451/g, '\u0435'); - let out = ''; - let pendingSeparator = false; - - for (const ch of lowered) { - if (ch === ' ' || ch === '_' || ch === '-') { - pendingSeparator = out.length > 0; - continue; - } - if (!/[\p{Script=Latin}\p{Script=Cyrillic}0-9]/u.test(ch)) { - return ''; - } - if (pendingSeparator && out.length > 0) out += '-'; - out += ch; - pendingSeparator = false; - } - - return out.replace(/-+$/g, ''); + const lowered = normalized.toLowerCase(); + if (!ALLOWED_CHARS_RE.test(lowered)) return ''; + return lowered; } export function validateChannelDisplayName(value) { @@ -73,7 +58,7 @@ export function channelNameErrorText(code) { case 'too_long': return 'Название слишком длинное: максимум 32 символа.'; case 'bad_chars': - return 'Разрешены кириллица, латиница, цифры, пробел, _ и -.'; + return 'Разрешены только латиница, цифры, _ и -.'; case 'reserved': return 'Название "0" зарезервировано.'; default: diff --git a/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java b/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java index 139f8d2..ee5aa2e 100644 --- a/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java +++ b/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java @@ -7,13 +7,13 @@ public final class ChannelNameRules { private static final int MIN_DISPLAY_NAME_LENGTH = 3; private static final int MAX_DISPLAY_NAME_LENGTH = 32; private static final Pattern DISPLAY_ALLOWED_PATTERN = - Pattern.compile("^[\\p{IsLatin}\\p{IsCyrillic}0-9 _-]+$"); + Pattern.compile("^[A-Za-z0-9_-]+$"); private ChannelNameRules() {} public static String normalizeDisplayName(String value) { if (value == null) return ""; - return value.trim().replaceAll("\\s+", " "); + return value.trim(); } public static String requireValidDisplayNameForCreate(String rawName) { @@ -40,45 +40,10 @@ public final class ChannelNameRules { throw new IllegalArgumentException("channelName is blank"); } - String lowered = normalized.toLowerCase(Locale.ROOT).replace('\u0451', '\u0435'); - StringBuilder slug = new StringBuilder(lowered.length()); - boolean pendingSeparator = false; - - for (int i = 0; i < lowered.length(); ) { - int cp = lowered.codePointAt(i); - i += Character.charCount(cp); - - if (cp == ' ' || cp == '_' || cp == '-') { - pendingSeparator = slug.length() > 0; - continue; - } - - if (!isLatinOrCyrillicOrDigit(cp)) { - throw new IllegalArgumentException("channelName contains unsupported characters"); - } - - if (pendingSeparator && slug.length() > 0) { - slug.append('-'); - } - pendingSeparator = false; - slug.appendCodePoint(cp); + String lowered = normalized.toLowerCase(Locale.ROOT); + if (!DISPLAY_ALLOWED_PATTERN.matcher(lowered).matches()) { + throw new IllegalArgumentException("channelName contains unsupported characters"); } - - int len = slug.length(); - if (len > 0 && slug.charAt(len - 1) == '-') { - slug.deleteCharAt(len - 1); - } - - if (slug.length() == 0) { - throw new IllegalArgumentException("channelName canonical slug is empty"); - } - - return slug.toString(); - } - - private static boolean isLatinOrCyrillicOrDigit(int cp) { - if (Character.isDigit(cp)) return true; - Character.UnicodeScript script = Character.UnicodeScript.of(cp); - return script == Character.UnicodeScript.LATIN || script == Character.UnicodeScript.CYRILLIC; + return lowered; } }