chore: remove gateway REST API and UI (#9989)

Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
Abhijay Jain
2026-06-25 06:14:18 +05:30
committed by GitHub
parent eea698945c
commit 416aad8136
14 changed files with 2 additions and 1235 deletions
+2 -7
View File
@@ -37,8 +37,8 @@ async fn shutdown_signal() {
}
pub async fn run() -> Result<()> {
// Install the rustls crypto provider early, before any spawned tasks (tunnel,
// gateways, etc.) try to open TLS connections. Both `ring` and `aws-lc-rs`
// Install the rustls crypto provider early, before any spawned tasks (tunnel, etc.)
// try to open TLS connections. Both `ring` and `aws-lc-rs`
// features are enabled on rustls (via different transitive deps), so rustls
// cannot auto-detect a provider — we must pick one explicitly.
#[cfg(feature = "rustls-tls")]
@@ -86,11 +86,6 @@ pub async fn run() -> Result<()> {
let addr = settings.socket_addr();
let gateway_manager = app_state.gateway_manager.clone();
tokio::spawn(async move {
gateway_manager.check_auto_start().await;
});
if settings.tls {
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
{
-225
View File
@@ -1,225 +0,0 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
};
use goose::gateway::manager::GatewayStatus;
use goose::gateway::GatewayConfig;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use utoipa::ToSchema;
#[derive(Deserialize, ToSchema)]
pub struct StartGatewayRequest {
pub gateway_type: String,
pub platform_config: serde_json::Value,
#[serde(default)]
pub max_sessions: usize,
}
#[derive(Deserialize, ToSchema)]
pub struct StopGatewayRequest {
pub gateway_type: String,
}
#[derive(Deserialize, ToSchema)]
pub struct RestartGatewayRequest {
pub gateway_type: String,
}
#[derive(Deserialize, ToSchema)]
pub struct RemoveGatewayRequest {
pub gateway_type: String,
}
#[derive(Deserialize, ToSchema)]
pub struct CreatePairingRequest {
pub gateway_type: String,
}
#[derive(Serialize, ToSchema)]
pub struct PairingCodeResponse {
pub code: String,
pub expires_at: i64,
}
#[utoipa::path(
post,
path = "/gateway/start",
request_body = StartGatewayRequest,
responses(
(status = 200, description = "Gateway started"),
(status = 400, description = "Bad request", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn start_gateway(
State(state): State<Arc<AppState>>,
Json(request): Json<StartGatewayRequest>,
) -> Response {
let mut config = GatewayConfig {
gateway_type: request.gateway_type,
platform_config: request.platform_config,
max_sessions: request.max_sessions,
};
let gw = match goose::gateway::create_gateway(&mut config) {
Ok(gw) => gw,
Err(e) => return ErrorResponse::bad_request(e.to_string()).into_response(),
};
match state.gateway_manager.start_gateway(config, gw).await {
Ok(()) => StatusCode::OK.into_response(),
Err(e) => ErrorResponse::bad_request(e.to_string()).into_response(),
}
}
#[utoipa::path(
post,
path = "/gateway/stop",
request_body = StopGatewayRequest,
responses(
(status = 200, description = "Gateway stopped"),
(status = 404, description = "Gateway not found", body = ErrorResponse)
)
)]
pub async fn stop_gateway(
State(state): State<Arc<AppState>>,
Json(request): Json<StopGatewayRequest>,
) -> Response {
match state
.gateway_manager
.stop_gateway(&request.gateway_type)
.await
{
Ok(()) => StatusCode::OK.into_response(),
Err(e) => ErrorResponse::not_found(e.to_string()).into_response(),
}
}
#[utoipa::path(
post,
path = "/gateway/restart",
request_body = RestartGatewayRequest,
responses(
(status = 200, description = "Gateway restarted"),
(status = 400, description = "Bad request", body = ErrorResponse),
(status = 404, description = "No saved config", body = ErrorResponse)
)
)]
pub async fn restart_gateway(
State(state): State<Arc<AppState>>,
Json(request): Json<RestartGatewayRequest>,
) -> Response {
match state
.gateway_manager
.restart_gateway(&request.gateway_type)
.await
{
Ok(()) => StatusCode::OK.into_response(),
Err(e) => ErrorResponse::bad_request(e.to_string()).into_response(),
}
}
#[utoipa::path(
post,
path = "/gateway/remove",
request_body = RemoveGatewayRequest,
responses(
(status = 200, description = "Gateway removed"),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn remove_gateway(
State(state): State<Arc<AppState>>,
Json(request): Json<RemoveGatewayRequest>,
) -> Response {
match state
.gateway_manager
.remove_gateway(&request.gateway_type)
.await
{
Ok(()) => StatusCode::OK.into_response(),
Err(e) => ErrorResponse::internal(e.to_string()).into_response(),
}
}
#[utoipa::path(
get,
path = "/gateway/status",
responses(
(status = 200, description = "Gateway statuses", body = Vec<GatewayStatus>)
)
)]
pub async fn gateway_status(State(state): State<Arc<AppState>>) -> Json<Vec<GatewayStatus>> {
Json(state.gateway_manager.status().await)
}
#[utoipa::path(
post,
path = "/gateway/pair",
request_body = CreatePairingRequest,
responses(
(status = 200, description = "Pairing code generated", body = PairingCodeResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn create_pairing_code(
State(state): State<Arc<AppState>>,
Json(request): Json<CreatePairingRequest>,
) -> Response {
match state
.gateway_manager
.generate_pairing_code(&request.gateway_type)
.await
{
Ok((code, expires_at)) => (
StatusCode::OK,
Json(PairingCodeResponse { code, expires_at }),
)
.into_response(),
Err(e) => ErrorResponse::internal(e.to_string()).into_response(),
}
}
#[utoipa::path(
delete,
path = "/gateway/pair/{platform}/{user_id}",
params(
("platform" = String, Path, description = "Platform name"),
("user_id" = String, Path, description = "Platform user ID")
),
responses(
(status = 200, description = "User unpaired"),
(status = 404, description = "Pairing not found", body = ErrorResponse)
)
)]
pub async fn unpair_user(
State(state): State<Arc<AppState>>,
Path((platform, user_id)): Path<(String, String)>,
) -> Response {
match state.gateway_manager.unpair_user(&platform, &user_id).await {
Ok(true) => StatusCode::OK.into_response(),
Ok(false) => {
ErrorResponse::not_found(format!("No pairing found for {}/{}", platform, user_id))
.into_response()
}
Err(e) => ErrorResponse::internal(e.to_string()).into_response(),
}
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/gateway/start", post(start_gateway))
.route("/gateway/stop", post(stop_gateway))
.route("/gateway/restart", post(restart_gateway))
.route("/gateway/remove", post(remove_gateway))
.route("/gateway/status", get(gateway_status))
.route("/gateway/pair", post(create_pairing_code))
.route("/gateway/pair/{platform}/{user_id}", delete(unpair_user))
.with_state(state)
}
-2
View File
@@ -4,7 +4,6 @@ pub mod config_management;
pub mod dictation;
pub mod errors;
pub mod features;
pub mod gateway;
#[cfg(feature = "local-inference")]
pub mod local_inference;
pub mod mcp_app_proxy;
@@ -42,7 +41,6 @@ pub fn configure(state: Arc<crate::state::AppState>, secret_key: String) -> Rout
.merge(setup::routes(state.clone()))
.merge(telemetry::routes(state.clone()))
.merge(tunnel::routes(state.clone()))
.merge(gateway::routes(state.clone()))
.merge(mcp_ui_proxy::routes(secret_key.clone()))
.merge(mcp_app_proxy::routes(secret_key))
.merge(session_events::routes(state.clone()))
-4
View File
@@ -14,7 +14,6 @@ use tokio::task::JoinHandle;
use crate::session_event_bus::SessionEventBus;
use crate::tunnel::TunnelManager;
use goose::agents::ExtensionLoadResult;
use goose::gateway::manager::GatewayManager;
#[cfg(feature = "local-inference")]
use goose::providers::local_inference::InferenceRuntime;
@@ -27,7 +26,6 @@ pub struct AppState {
pub recipe_file_hash_map: Arc<Mutex<HashMap<String, PathBuf>>>,
recipe_session_tracker: Arc<Mutex<HashSet<String>>>,
pub tunnel_manager: Arc<TunnelManager>,
pub gateway_manager: Arc<GatewayManager>,
pub extension_loading_tasks: ExtensionLoadingTasks,
#[cfg(feature = "local-inference")]
inference_runtime: Arc<OnceLock<Arc<InferenceRuntime>>>,
@@ -40,14 +38,12 @@ impl AppState {
let agent_manager = AgentManager::instance().await?;
let tunnel_manager = Arc::new(TunnelManager::new(tls));
let gateway_manager = Arc::new(GatewayManager::new(agent_manager.clone())?);
Ok(Arc::new(Self {
agent_manager,
recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())),
recipe_session_tracker: Arc::new(Mutex::new(HashSet::new())),
tunnel_manager,
gateway_manager,
extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())),
#[cfg(feature = "local-inference")]
inference_runtime: Arc::new(OnceLock::new()),
@@ -20,8 +20,6 @@ import {
KeyRound,
} from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import GatewaySettingsSection from './gateways/GatewaySettingsSection';
import { getTunnelStatus } from '../../api/sdk.gen';
import ChatSettingsSection from './chat/ChatSettingsSection';
import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection';
import AuthSettingsSection from './auth/AuthSettingsSection';
@@ -86,7 +84,6 @@ export default function SettingsView({
viewOptions: SettingsViewOptions;
}) {
const [activeTab, setActiveTab] = useState('models');
const [tunnelDisabled, setTunnelDisabled] = useState(false);
const hasTrackedInitialTab = useRef(false);
const { localInference } = useFeatures();
const intl = useIntl();
@@ -112,7 +109,6 @@ export default function SettingsView({
prompts: 'prompts',
keyboard: 'keyboard',
auth: 'auth',
gateway: 'sharing',
'local-inference': 'local-inference',
};
@@ -137,16 +133,6 @@ export default function SettingsView({
}
}, [activeTab]);
useEffect(() => {
getTunnelStatus()
.then(({ data }) => {
setTunnelDisabled(data?.state === 'disabled');
})
.catch(() => {
setTunnelDisabled(false);
});
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !event.defaultPrevented) {
@@ -269,7 +255,6 @@ export default function SettingsView({
<div className="space-y-8 pb-8">
<SessionSharingSection />
<ExternalBackendSection />
{!tunnelDisabled && <GatewaySettingsSection />}
</div>
</TabsContent>
@@ -1,502 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { Button } from '../../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Input } from '../../ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
import { Loader2, Copy, Check, Square, Trash2, User } from 'lucide-react';
import { getApiUrl } from '../../../config';
import { defineMessages, useIntl } from '../../../i18n';
const i18n = defineMessages({
loading: {
id: 'gatewaySettings.loading',
defaultMessage: 'Loading...',
},
pairedUsers: {
id: 'gatewaySettings.pairedUsers',
defaultMessage: 'Paired Users',
},
telegram: {
id: 'gatewaySettings.telegram',
defaultMessage: 'Telegram',
},
running: {
id: 'gatewaySettings.running',
defaultMessage: 'Running',
},
stopped: {
id: 'gatewaySettings.stopped',
defaultMessage: 'Stopped',
},
pairDevice: {
id: 'gatewaySettings.pairDevice',
defaultMessage: 'Pair Device',
},
stop: {
id: 'gatewaySettings.stop',
defaultMessage: 'Stop',
},
start: {
id: 'gatewaySettings.start',
defaultMessage: 'Start',
},
remove: {
id: 'gatewaySettings.remove',
defaultMessage: 'Remove',
},
pasteBotToken: {
id: 'gatewaySettings.pasteBotToken',
defaultMessage: 'Paste bot token here',
},
botFatherInstructions: {
id: 'gatewaySettings.botFatherInstructions',
defaultMessage:
'Open @BotFather on your phone, send /newbot, and follow the prompts to name your bot. BotFather will reply with an API token — paste it below.',
},
pairingCode: {
id: 'gatewaySettings.pairingCode',
defaultMessage: 'Pairing Code',
},
sendCodeToPair: {
id: 'gatewaySettings.sendCodeToPair',
defaultMessage: 'Send this code to your {gatewayType} bot to pair.',
},
expiresIn: {
id: 'gatewaySettings.expiresIn',
defaultMessage: 'Expires in {time}',
},
close: {
id: 'gatewaySettings.close',
defaultMessage: 'Close',
},
failedToStart: {
id: 'gatewaySettings.failedToStart',
defaultMessage: 'Failed to start',
},
failedToStop: {
id: 'gatewaySettings.failedToStop',
defaultMessage: 'Failed to stop',
},
failedToRemove: {
id: 'gatewaySettings.failedToRemove',
defaultMessage: 'Failed to remove',
},
failedToUnpairUser: {
id: 'gatewaySettings.failedToUnpairUser',
defaultMessage: 'Failed to unpair user',
},
failedToGeneratePairingCode: {
id: 'gatewaySettings.failedToGeneratePairingCode',
defaultMessage: 'Failed to generate pairing code',
},
});
interface PairedUserInfo {
platform: string;
user_id: string;
display_name: string | null;
session_id: string;
paired_at: number;
}
interface GatewayStatus {
gateway_type: string;
running: boolean;
configured: boolean;
paired_users: PairedUserInfo[];
info?: Record<string, string>;
}
interface PairingCodeResponse {
code: string;
expires_at: number;
}
async function gatewayFetch(endpoint: string, options: globalThis.RequestInit = {}) {
const secretKey = await window.electron.getSecretKey();
const url = getApiUrl(endpoint);
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': secretKey,
...options.headers,
},
});
}
export default function GatewaySettingsSection() {
const intl = useIntl();
const [gateways, setGateways] = useState<GatewayStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pairingCode, setPairingCode] = useState<PairingCodeResponse | null>(null);
const [pairingGatewayType, setPairingGatewayType] = useState<string | null>(null);
const [copiedCode, setCopiedCode] = useState(false);
const fetchStatus = useCallback(async () => {
try {
const response = await gatewayFetch('/gateway/status');
if (response.ok) {
setGateways(await response.json());
}
} catch (err) {
console.error('Failed to fetch gateway status:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 5000);
return () => clearInterval(interval);
}, [fetchStatus]);
const doPost = async (endpoint: string, body: object, errorMsg: string) => {
setError(null);
try {
const response = await gatewayFetch(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || errorMsg);
}
await fetchStatus();
} catch (err) {
setError(err instanceof Error ? err.message : errorMsg);
}
};
const handleUnpairUser = async (platform: string, userId: string) => {
setError(null);
try {
const response = await gatewayFetch(`/gateway/pair/${platform}/${userId}`, {
method: 'DELETE',
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || intl.formatMessage(i18n.failedToUnpairUser));
}
await fetchStatus();
} catch (err) {
setError(err instanceof Error ? err.message : intl.formatMessage(i18n.failedToUnpairUser));
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedCode(true);
setTimeout(() => setCopiedCode(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
if (loading) {
return (
<div className="flex items-center gap-2 text-sm text-text-muted py-4">
<Loader2 className="h-4 w-4 animate-spin" />
{intl.formatMessage(i18n.loading)}
</div>
);
}
const telegram = gateways.find((g) => g.gateway_type === 'telegram');
return (
<>
{error && (
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded text-sm text-red-800 dark:text-red-200 mb-4">
{error}
</div>
)}
<TelegramGatewayCard
status={telegram}
onStart={(config) =>
doPost(
'/gateway/start',
{ gateway_type: 'telegram', platform_config: config, max_sessions: 0 },
intl.formatMessage(i18n.failedToStart)
)
}
onRestart={() =>
doPost('/gateway/restart', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToStart))
}
onStop={() => doPost('/gateway/stop', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToStop))}
onRemove={() => doPost('/gateway/remove', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToRemove))}
onGenerateCode={async () => {
setError(null);
try {
const response = await gatewayFetch('/gateway/pair', {
method: 'POST',
body: JSON.stringify({ gateway_type: 'telegram' }),
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || intl.formatMessage(i18n.failedToGeneratePairingCode));
}
const data: PairingCodeResponse = await response.json();
setPairingCode(data);
setPairingGatewayType('telegram');
} catch (err) {
setError(err instanceof Error ? err.message : intl.formatMessage(i18n.failedToGeneratePairingCode));
}
}}
onUnpairUser={handleUnpairUser}
/>
<PairingCodeModal
open={pairingCode !== null}
onClose={() => {
setPairingCode(null);
setPairingGatewayType(null);
}}
code={pairingCode}
gatewayType={pairingGatewayType}
onCopy={copyToClipboard}
copied={copiedCode}
/>
</>
);
}
function PairedUsersList({
users,
onUnpairUser,
}: {
users: PairedUserInfo[];
onUnpairUser: (platform: string, userId: string) => void;
}) {
const intl = useIntl();
if (users.length === 0) return null;
return (
<div className="space-y-1 mt-2">
<h4 className="text-xs text-text-muted font-medium">{intl.formatMessage(i18n.pairedUsers)}</h4>
{users.map((user) => (
<div
key={`${user.platform}-${user.user_id}`}
className="flex items-center justify-between py-1.5 px-2 bg-background-muted rounded text-sm"
>
<div className="flex items-center gap-2 min-w-0">
<User className="h-3 w-3 text-text-muted flex-shrink-0" />
<span className="truncate">{user.display_name || user.user_id}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onUnpairUser(user.platform, user.user_id)}
className="h-6 w-6 p-0 text-text-muted hover:text-red-600 flex-shrink-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
);
}
function TelegramGatewayCard({
status,
onStart,
onRestart,
onStop,
onRemove,
onGenerateCode,
onUnpairUser,
}: {
status: GatewayStatus | undefined;
onStart: (config: Record<string, unknown>) => Promise<void>;
onRestart: () => Promise<void>;
onStop: () => Promise<void>;
onRemove: () => Promise<void>;
onGenerateCode: () => void;
onUnpairUser: (platform: string, userId: string) => void;
}) {
const intl = useIntl();
const [botToken, setBotToken] = useState('');
const [busy, setBusy] = useState(false);
const running = status?.running ?? false;
const configured = status?.configured ?? false;
const wrap = (fn: () => Promise<void>) => async () => {
setBusy(true);
try {
await fn();
} finally {
setBusy(false);
}
};
const handleFirstStart = wrap(async () => {
if (!botToken.trim()) return;
await onStart({ bot_token: botToken.trim() });
setBotToken('');
});
return (
<Card className="rounded-lg">
<CardHeader className="pb-0">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
{intl.formatMessage(i18n.telegram)}
{running && (
<span className="inline-flex items-center text-xs text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded-full">
{intl.formatMessage(i18n.running)}
</span>
)}
{!running && configured && (
<span className="inline-flex items-center text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/30 px-2 py-0.5 rounded-full">
{intl.formatMessage(i18n.stopped)}
</span>
)}
</CardTitle>
<div className="flex items-center gap-2">
{running && (
<>
<Button variant="outline" size="sm" onClick={onGenerateCode}>
{intl.formatMessage(i18n.pairDevice)}
</Button>
<Button variant="destructive" size="sm" disabled={busy} onClick={wrap(onStop)}>
<Square className="h-3 w-3 mr-1" />
{intl.formatMessage(i18n.stop)}
</Button>
</>
)}
{!running && configured && (
<>
<Button size="sm" disabled={busy} onClick={wrap(onRestart)}>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : intl.formatMessage(i18n.start)}
</Button>
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={wrap(onRemove)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900/20"
>
<Trash2 className="h-3 w-3 mr-1" />
{intl.formatMessage(i18n.remove)}
</Button>
</>
)}
</div>
</div>
</CardHeader>
<CardContent className="pt-3 space-y-2">
{!running && !configured && (
<>
<div className="text-xs text-text-muted space-y-1.5 mb-2">
<p>
{intl.formatMessage(i18n.botFatherInstructions)}
</p>
</div>
<div className="flex items-center gap-2">
<Input
type="password"
placeholder={intl.formatMessage(i18n.pasteBotToken)}
value={botToken}
onChange={(e) => setBotToken(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleFirstStart()}
className="text-sm"
/>
<Button size="sm" onClick={handleFirstStart} disabled={busy || !botToken.trim()}>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : intl.formatMessage(i18n.start)}
</Button>
</div>
</>
)}
{status && <PairedUsersList users={status.paired_users} onUnpairUser={onUnpairUser} />}
</CardContent>
</Card>
);
}
function PairingCodeModal({
open,
onClose,
code,
gatewayType,
onCopy,
copied,
}: {
open: boolean;
onClose: () => void;
code: PairingCodeResponse | null;
gatewayType: string | null;
onCopy: (text: string) => void;
copied: boolean;
}) {
const intl = useIntl();
const [timeRemaining, setTimeRemaining] = useState(0);
useEffect(() => {
if (!code) return;
const updateTimer = () => {
const remaining = Math.max(0, code.expires_at - Math.floor(Date.now() / 1000));
setTimeRemaining(remaining);
if (remaining === 0) {
onClose();
}
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [code, onClose]);
if (!code) return null;
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{intl.formatMessage(i18n.pairingCode)}</DialogTitle>
</DialogHeader>
<div className="py-6 space-y-4">
<div className="flex justify-center">
<div className="flex items-center gap-2">
<code className="text-4xl font-mono font-bold tracking-[0.3em] select-all">
{code.code}
</code>
<Button
variant="ghost"
size="sm"
onClick={() => onCopy(code.code)}
className="flex-shrink-0"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<p className="text-center text-sm text-text-muted">
{intl.formatMessage(i18n.sendCodeToPair, { gatewayType })}
</p>
<div className="text-center text-xs text-text-muted">
{intl.formatMessage(i18n.expiresIn, {
time: `${minutes}:${seconds.toString().padStart(2, '0')}`,
})}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
{intl.formatMessage(i18n.close)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "An unexpected error occurred during setup."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "Open @BotFather on your phone, send /newbot, and follow the prompts to name your bot. BotFather will reply with an API token — paste it below."
},
"gatewaySettings.close": {
"defaultMessage": "Close"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "Expires in {time}"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "Failed to generate pairing code"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "Failed to remove"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "Failed to start"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "Failed to stop"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "Failed to unpair user"
},
"gatewaySettings.loading": {
"defaultMessage": "Loading..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "Pair Device"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "Paired Users"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "Pairing Code"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "Paste bot token here"
},
"gatewaySettings.remove": {
"defaultMessage": "Remove"
},
"gatewaySettings.running": {
"defaultMessage": "Running"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "Send this code to your {gatewayType} bot to pair."
},
"gatewaySettings.start": {
"defaultMessage": "Start"
},
"gatewaySettings.stop": {
"defaultMessage": "Stop"
},
"gatewaySettings.stopped": {
"defaultMessage": "Stopped"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "Close"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "Ocurrió un error inesperado durante la configuración."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "Abre @BotFather en tu teléfono, envía /newbot y sigue las indicaciones para nombrar tu bot. BotFather te responderá con un token de API — pégalo abajo."
},
"gatewaySettings.close": {
"defaultMessage": "Cerrar"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "Expira en {time}"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "No se pudo generar el código de emparejamiento"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "No se pudo quitar"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "No se pudo iniciar"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "No se pudo detener"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "No se pudo desemparejar al usuario"
},
"gatewaySettings.loading": {
"defaultMessage": "Cargando..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "Emparejar dispositivo"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "Usuarios emparejados"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "Código de emparejamiento"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "Pega aquí el token del bot"
},
"gatewaySettings.remove": {
"defaultMessage": "Quitar"
},
"gatewaySettings.running": {
"defaultMessage": "En ejecución"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "Envía este código a tu bot de {gatewayType} para emparejar."
},
"gatewaySettings.start": {
"defaultMessage": "Iniciar"
},
"gatewaySettings.stop": {
"defaultMessage": "Detener"
},
"gatewaySettings.stopped": {
"defaultMessage": "Detenido"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "Cerrar"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "सेटअप के दौरान एक अप्रत्याशित त्रुटि उत्पन्न हुई."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "अपने फ़ोन पर @BotFather खोलें, /newbot भेजें, और अपने बॉट को नाम देने के लिए संकेतों का पालन करें। BotFather एक API टोकन के साथ उत्तर देगा - इसे नीचे चिपकाएँ।"
},
"gatewaySettings.close": {
"defaultMessage": "बंद करें"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "{time} में समाप्त होता है"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "युग्मन कोड जनरेट करने में विफल"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "हटाने में विफल"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "प्रारंभ करने में विफल"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "रोकने में विफल"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "उपयोगकर्ता को अयुग्मित करने में विफल"
},
"gatewaySettings.loading": {
"defaultMessage": "लोड हो रहा है..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "जोड़ी डिवाइस"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "युग्मित उपयोगकर्ता"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "युग्मन कोड"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "यहां बॉट टोकन चिपकाएं"
},
"gatewaySettings.remove": {
"defaultMessage": "हटाओ"
},
"gatewaySettings.running": {
"defaultMessage": "चल रहा है"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "जोड़ी बनाने के लिए इस कोड को अपने {gatewayType} बॉट पर भेजें।"
},
"gatewaySettings.start": {
"defaultMessage": "प्रारंभ करें"
},
"gatewaySettings.stop": {
"defaultMessage": "रुकें"
},
"gatewaySettings.stopped": {
"defaultMessage": "रुक गया"
},
"gatewaySettings.telegram": {
"defaultMessage": "टेलीग्राम"
},
"goosehintsModal.close": {
"defaultMessage": "बंद करें"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "セットアップ中に予期しないエラーが発生しました。"
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "スマートフォンで @BotFather を開き、/newbot を送信して、指示に従ってボットに名前を付けます。BotFather から API トークンが届くので、下に貼り付けてください。"
},
"gatewaySettings.close": {
"defaultMessage": "閉じる"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "{time} 後に期限切れ"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "ペアリングコードの生成に失敗しました"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "削除に失敗しました"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "開始に失敗しました"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "停止に失敗しました"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "ユーザーのペアリング解除に失敗しました"
},
"gatewaySettings.loading": {
"defaultMessage": "読み込み中..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "デバイスをペアリング"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "ペアリング済みユーザー"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "ペアリングコード"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "ボットトークンをここに貼り付け"
},
"gatewaySettings.remove": {
"defaultMessage": "削除"
},
"gatewaySettings.running": {
"defaultMessage": "実行中"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "ペアリングするには、このコードを {gatewayType} ボットに送信してください。"
},
"gatewaySettings.start": {
"defaultMessage": "開始"
},
"gatewaySettings.stop": {
"defaultMessage": "停止"
},
"gatewaySettings.stopped": {
"defaultMessage": "停止中"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "閉じる"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "설정 중에 예상치 못한 오류가 발생했습니다."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "휴대폰에서 @BotFather를 열고 /newbot을 보낸 뒤, 안내에 따라 봇 이름을 지정하세요. BotFather가 API 토큰을 보내면 아래에 붙여넣으세요."
},
"gatewaySettings.close": {
"defaultMessage": "닫기"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "{time} 후 만료"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "페어링 코드를 생성하지 못했습니다."
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "제거하지 못했습니다."
},
"gatewaySettings.failedToStart": {
"defaultMessage": "시작하지 못했습니다."
},
"gatewaySettings.failedToStop": {
"defaultMessage": "중지하지 못했습니다."
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "사용자 페어링을 해제하지 못했습니다."
},
"gatewaySettings.loading": {
"defaultMessage": "로드 중..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "기기 페어링"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "페어링된 사용자"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "페어링 코드"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "봇 토큰을 여기에 붙여넣으세요"
},
"gatewaySettings.remove": {
"defaultMessage": "제거"
},
"gatewaySettings.running": {
"defaultMessage": "실행 중"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "페어링하려면 이 코드를 {gatewayType} 봇에 보내세요."
},
"gatewaySettings.start": {
"defaultMessage": "시작"
},
"gatewaySettings.stop": {
"defaultMessage": "중지"
},
"gatewaySettings.stopped": {
"defaultMessage": "중지됨"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "닫기"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "Во время настройки произошла непредвиденная ошибка."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "Откройте @BotFather на телефоне, отправьте /newbot и следуйте подсказкам, чтобы назвать бота. BotFather ответит API-токеном — вставьте его ниже."
},
"gatewaySettings.close": {
"defaultMessage": "Закрыть"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "Истекает через {time}"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "Не удалось создать код подключения"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "Не удалось удалить"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "Не удалось запустить"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "Не удалось остановить"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "Не удалось отменить подключение пользователя"
},
"gatewaySettings.loading": {
"defaultMessage": "Загрузка..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "Подключить устройство"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "Подключенные пользователи"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "Код подключения"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "Вставьте токен бота здесь"
},
"gatewaySettings.remove": {
"defaultMessage": "Удалить"
},
"gatewaySettings.running": {
"defaultMessage": "Выполняется"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "Отправьте этот код вашему боту {gatewayType} для подключения."
},
"gatewaySettings.start": {
"defaultMessage": "Запустить"
},
"gatewaySettings.stop": {
"defaultMessage": "Остановить"
},
"gatewaySettings.stopped": {
"defaultMessage": "Остановлено"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "Закрыть"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "Kurulum sırasında beklenmeyen bir hata oluştu."
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "Telefonunuzda @BotFather'ı açın, /newbot gönderin ve botunuza isim vermek için talimatları izleyin. BotFather bir API jetonuyla yanıt verecektir; bunu aşağıya yapıştırın."
},
"gatewaySettings.close": {
"defaultMessage": "Kapat"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "Süresi {time}'de doluyor"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "Eşleştirme kodu oluşturulamadı"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "Kaldırılamadı"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "Başlatılamadı"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "Durdurulamadı"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "Kullanıcının eşlemesi kaldırılamadı"
},
"gatewaySettings.loading": {
"defaultMessage": "Yükleniyor..."
},
"gatewaySettings.pairDevice": {
"defaultMessage": "Cihazı Eşleştir"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "Eşlenen Kullanıcılar"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "Eşleştirme Kodu"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "Bot jetonunu buraya yapıştırın"
},
"gatewaySettings.remove": {
"defaultMessage": "Kaldır"
},
"gatewaySettings.running": {
"defaultMessage": "Koşu"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "Eşleştirmek için bu kodu {gatewayType} botunuza gönderin."
},
"gatewaySettings.start": {
"defaultMessage": "Başlat"
},
"gatewaySettings.stop": {
"defaultMessage": "Durdur"
},
"gatewaySettings.stopped": {
"defaultMessage": "Durduruldu"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telgraf"
},
"goosehintsModal.close": {
"defaultMessage": "Kapat"
},
-60
View File
@@ -1229,66 +1229,6 @@
"freeOptionCards.unexpectedError": {
"defaultMessage": "设置过程中发生意外错误。"
},
"gatewaySettings.botFatherInstructions": {
"defaultMessage": "在手机上打开 @BotFather,发送 /newbot,按提示为你的 bot 命名。BotFather 会回复一个 API token —— 把它粘贴到下方。"
},
"gatewaySettings.close": {
"defaultMessage": "关闭"
},
"gatewaySettings.expiresIn": {
"defaultMessage": "{time} 后过期"
},
"gatewaySettings.failedToGeneratePairingCode": {
"defaultMessage": "生成配对码失败"
},
"gatewaySettings.failedToRemove": {
"defaultMessage": "移除失败"
},
"gatewaySettings.failedToStart": {
"defaultMessage": "启动失败"
},
"gatewaySettings.failedToStop": {
"defaultMessage": "停止失败"
},
"gatewaySettings.failedToUnpairUser": {
"defaultMessage": "解除用户配对失败"
},
"gatewaySettings.loading": {
"defaultMessage": "加载中…"
},
"gatewaySettings.pairDevice": {
"defaultMessage": "配对设备"
},
"gatewaySettings.pairedUsers": {
"defaultMessage": "已配对用户"
},
"gatewaySettings.pairingCode": {
"defaultMessage": "配对码"
},
"gatewaySettings.pasteBotToken": {
"defaultMessage": "在此粘贴 bot token"
},
"gatewaySettings.remove": {
"defaultMessage": "移除"
},
"gatewaySettings.running": {
"defaultMessage": "运行中"
},
"gatewaySettings.sendCodeToPair": {
"defaultMessage": "把此码发送给你的 {gatewayType} bot 以配对。"
},
"gatewaySettings.start": {
"defaultMessage": "启动"
},
"gatewaySettings.stop": {
"defaultMessage": "停止"
},
"gatewaySettings.stopped": {
"defaultMessage": "已停止"
},
"gatewaySettings.telegram": {
"defaultMessage": "Telegram"
},
"goosehintsModal.close": {
"defaultMessage": "关闭"
},