mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 13:10:44 +08:00
feat: add unified IM access policies
This commit is contained in:
parent
075d2713c1
commit
840e5aa857
89 changed files with 5573 additions and 1676 deletions
|
|
@ -1,24 +1,9 @@
|
|||
import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
|
||||
|
||||
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
||||
export const TELEGRAM_ENDPOINTS = Object.freeze({
|
||||
...TOKEN_BOT_ENDPOINTS,
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
||||
|
||||
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询', {
|
||||
normalizeBotExtension: (value) => {
|
||||
const source = value?.accessPolicy;
|
||||
const accessMode = source?.accessMode === 'private-allowlist'
|
||||
? 'private-allowlist' : 'compatible';
|
||||
const allowedUsers = Array.isArray(source?.allowedUsers)
|
||||
? [...new Set(source.allowedUsers.filter((entry) => (
|
||||
typeof entry === 'string' && /^[1-9]\d{0,15}$/.test(entry)
|
||||
)))]
|
||||
: [];
|
||||
return { accessPolicy: { accessMode, allowedUsers } };
|
||||
},
|
||||
});
|
||||
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询');
|
||||
|
||||
export const unwrapRpcResult = api.unwrapRpcResult;
|
||||
export const normalizeSnapshot = api.normalizeSnapshot;
|
||||
|
|
|
|||
|
|
@ -1,121 +1,11 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { TelegramLogoGlyph } from '../../channel-logos.js';
|
||||
import { createTokenChannelSettings } from '../shared/token-channel.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import {
|
||||
TELEGRAM_ENDPOINTS,
|
||||
telegramClientApi,
|
||||
} from './api.js';
|
||||
import { installTelegramStyles } from './styles.js';
|
||||
|
||||
function policyFor(account) {
|
||||
return {
|
||||
accessMode: account?.accessPolicy?.accessMode === 'private-allowlist'
|
||||
? 'private-allowlist' : 'compatible',
|
||||
allowedUsers: Array.isArray(account?.accessPolicy?.allowedUsers)
|
||||
? account.accessPolicy.allowedUsers : [],
|
||||
};
|
||||
}
|
||||
|
||||
function allowedUsersFromText(value) {
|
||||
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
||||
if (entries.some((entry) => !/^[1-9]\d{0,15}$/.test(entry))) {
|
||||
throw new TypeError('User ID 必须是 1–16 位正整数,每行一个。');
|
||||
}
|
||||
return [...new Set(entries)];
|
||||
}
|
||||
|
||||
export function TelegramAccessSettings({ account, busy = false, onSave }) {
|
||||
const policy = policyFor(account);
|
||||
const sourceUsers = policy.allowedUsers.join('\n');
|
||||
const accessHelpId = React.useId();
|
||||
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
|
||||
const [allowedUsers, setAllowedUsers] = React.useState(sourceUsers);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAccessMode(policy.accessMode);
|
||||
setAllowedUsers(sourceUsers);
|
||||
setError(null);
|
||||
}, [policy.accessMode, sourceUsers]);
|
||||
|
||||
const save = async (event) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const normalized = allowedUsersFromText(allowedUsers);
|
||||
if (typeof onSave !== 'function') throw new Error('Telegram 访问设置暂不可用。');
|
||||
await onSave({ accessMode, allowedUsers: normalized });
|
||||
} catch (caught) {
|
||||
setError(caught?.message ?? 'Telegram 访问设置保存失败。');
|
||||
}
|
||||
};
|
||||
|
||||
const privateAllowlist = accessMode === 'private-allowlist';
|
||||
const savedPrivateAllowlist = policy.accessMode === 'private-allowlist';
|
||||
const emptyAllowlist = privateAllowlist && allowedUsers.trim() === '';
|
||||
return h('form', { className: 'dtg-access', onSubmit: save },
|
||||
h('div', { className: 'dtg-accessHeading' },
|
||||
h('strong', null, '访问设置'),
|
||||
h('span', { className: 'dtg-accessStatus' },
|
||||
h('span', { className: 'dtg-accessBadge', 'data-mode': policy.accessMode },
|
||||
savedPrivateAllowlist ? '已生效:安全模式' : '已生效:兼容模式'),
|
||||
h('span', { className: 'dtg-accessHelp' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dtg-accessHelpButton',
|
||||
'aria-label': '查看 Telegram 访问模式说明',
|
||||
'aria-describedby': accessHelpId,
|
||||
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
||||
h('span', {
|
||||
id: accessHelpId,
|
||||
className: 'dtg-accessTooltip',
|
||||
role: 'tooltip',
|
||||
},
|
||||
h('span', { className: 'dtg-accessTooltipItem' },
|
||||
h('strong', null, '兼容模式'),
|
||||
h('span', null, '保持原有行为:私聊直接响应,群聊在被提及或回复时响应。')),
|
||||
h('span', { className: 'dtg-accessTooltipItem' },
|
||||
h('strong', null, '安全模式'),
|
||||
h('span', null, '群聊全部忽略,私聊仅允许白名单用户。')))))),
|
||||
h('label', { className: 'dtg-accessField' },
|
||||
h('span', null, '模式'),
|
||||
h('select', {
|
||||
value: accessMode,
|
||||
disabled: busy,
|
||||
'aria-label': 'Telegram 访问模式',
|
||||
onChange: (event) => { setAccessMode(event.target.value); setError(null); },
|
||||
},
|
||||
h('option', { value: 'compatible' }, '兼容模式(默认)'),
|
||||
h('option', { value: 'private-allowlist' }, '安全模式(私聊白名单)'))),
|
||||
h('label', { className: 'dtg-accessField' },
|
||||
h('span', null, '允许私聊的 Telegram User ID'),
|
||||
h('textarea', {
|
||||
value: allowedUsers,
|
||||
disabled: busy || !privateAllowlist,
|
||||
rows: 3,
|
||||
placeholder: '每行一个数字 User ID',
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
onChange: (event) => { setAllowedUsers(event.target.value); setError(null); },
|
||||
}),
|
||||
h('small', null, privateAllowlist
|
||||
? '白名单仅属于当前机器人。'
|
||||
: '兼容模式下暂不使用白名单,切换模式时会保留。')),
|
||||
emptyAllowlist
|
||||
? h('p', { className: 'dtg-accessWarning', role: 'status' },
|
||||
'白名单为空;保存后该机器人会拒绝所有入站消息。')
|
||||
: null,
|
||||
error ? h('p', { className: 'dtg-accessError', role: 'alert' }, error) : null,
|
||||
h('div', { className: 'dtg-accessActions' },
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
className: 'ddt-button',
|
||||
'data-kind': 'secondary',
|
||||
disabled: busy,
|
||||
}, busy ? '正在保存…' : '保存访问设置')));
|
||||
}
|
||||
|
||||
const channel = createTokenChannelSettings({
|
||||
channel: 'Telegram',
|
||||
endpoints: TELEGRAM_ENDPOINTS,
|
||||
|
|
@ -129,8 +19,6 @@ const channel = createTokenChannelSettings({
|
|||
emptyTitle: '接入 Telegram 机器人',
|
||||
emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
|
||||
platformLabel: 'Telegram',
|
||||
AccountSettings: TelegramAccessSettings,
|
||||
accountSettingsEndpoint: TELEGRAM_ENDPOINTS.setAccessPolicy,
|
||||
});
|
||||
|
||||
export const TelegramSettingsTab = channel.SettingsTab;
|
||||
|
|
|
|||
|
|
@ -4,34 +4,6 @@ const CSS = String.raw`
|
|||
.dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
|
||||
.dtg-avatar { color: #fff; background: #229ed9; }
|
||||
.dtg-avatar svg { display: block; }
|
||||
.dtg-access { min-width: 0; width: 100%; max-width: 100%; display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
|
||||
.dtg-accessHeading { position: relative; min-width: 0; max-width: 100%; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 12px; }
|
||||
.dtg-accessHeading > strong { min-width: 0; font-size: 13px; overflow-wrap: anywhere; }
|
||||
.dtg-accessStatus { min-width: 0; max-width: 100%; flex: 0 1 auto; display: inline-flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 6px; }
|
||||
.dtg-accessBadge { min-width: 0; max-width: 100%; flex: 0 1 auto; padding: 3px 8px; border-radius: 999px; color: #1687bd; background: #eaf7fd; font-size: 11px; font-weight: 700; overflow-wrap: anywhere; text-align: center; }
|
||||
.dtg-accessBadge[data-mode="private-allowlist"] { color: #a15c00; background: #fff3d6; }
|
||||
.dtg-accessHelp { position: static; display: inline-flex; flex: none; }
|
||||
.dtg-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #229ed9 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1687bd; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
|
||||
.dtg-accessHelpButton:hover { border-color: #229ed9; color: #1178a8; background: #eaf7fd; }
|
||||
.dtg-accessHelpButton:focus-visible { outline: none; border-color: #229ed9; box-shadow: 0 0 0 3px color-mix(in srgb, #229ed9 18%, transparent); }
|
||||
.dtg-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: min(300px, 100%); max-width: 100%; display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
||||
.dtg-accessTooltipItem { display: grid; gap: 2px; }
|
||||
.dtg-accessTooltipItem + .dtg-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
|
||||
.dtg-accessTooltipItem strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 17px; font-weight: 700; }
|
||||
.dtg-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; font-weight: 400; }
|
||||
.dtg-accessHelp:hover .dtg-accessTooltip, .dtg-accessHelp:focus-within .dtg-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
||||
.dtg-accessField { min-width: 0; max-width: 100%; display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
||||
.dtg-accessField select, .dtg-accessField textarea { min-width: 0; width: 100%; max-width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
|
||||
.dtg-accessField select { height: 34px; padding: 0 9px; }
|
||||
.dtg-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.dtg-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
|
||||
.dtg-accessField > span, .dtg-accessField small { overflow-wrap: anywhere; }
|
||||
.dtg-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
|
||||
.dtg-accessWarning, .dtg-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
|
||||
.dtg-accessWarning { color: #a15c00; }
|
||||
.dtg-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
|
||||
.dtg-accessActions { min-width: 0; max-width: 100%; display: flex; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.dtg-accessActions .ddt-button { max-width: 100%; white-space: normal; }
|
||||
`;
|
||||
|
||||
export function installTelegramStyles() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue