mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Fix/telegram login (#4)
* Add UserProvider to App component and enhance user data fetching logic - Integrated UserProvider into the App component to manage user state. - Updated UserProvider to fetch current user data on token availability and handle token expiration by clearing the token on fetch failure. - Improved user data fetching logic for better error handling and user experience. * Enhance Telegram connection handling and error management - Added logic to handle Telegram account mismatch, providing user feedback and resetting connection state. - Improved user verification during authentication to ensure the logged-in account matches the Telegram account. - Refactored connection status checks and error handling for better user experience. - Introduced a method to clear session and disconnect when account mismatch occurs. - Updated polling mechanism for authentication status to ensure timely updates. This update improves the robustness of the Telegram connection process and enhances user feedback during authentication failures. * ran prettier * Refactor socket and auth state management for user-specific handling - Updated socket state management to support multiple users by introducing user-specific selectors and actions. - Refactored connection status handling in the socket service to dispatch user-specific status updates. - Enhanced onboarding state management to track completion per user, allowing for more granular control. - Introduced new selectors for socket and auth states to improve readability and maintainability. - Updated relevant components to utilize the new user-specific state management. This update improves the application's ability to handle multiple users and enhances the overall user experience during onboarding and socket connections. * Enhance MTProto client session management and introduce user-specific session handling - Updated the MTProtoService to manage sessions per user by storing session data under a user-specific key. - Modified the initialize method to accept a userId parameter, allowing for user-specific session initialization and management. - Improved session loading and saving logic to ensure proper handling of user sessions. - Added a utility function to generate session keys based on userId for better maintainability. This update enhances the application's ability to handle multiple user sessions effectively. * Refactor Telegram state management for user-specific handling - Introduced user-specific state management in the Telegram store, allowing for better handling of multiple users. - Updated selectors to retrieve state based on the current user, improving data encapsulation and reducing global state dependencies. - Enhanced reducers and thunks to ensure actions are dispatched with user context, maintaining user-specific data integrity. - Modified persistence configuration to store Telegram state scoped by user, ensuring isolated state management. This update significantly improves the application's ability to manage multiple user sessions effectively and enhances overall user experience. * Enhance MTProtoService connection handling with user-specific initialization - Updated the checkConnection method to accept an optional userId parameter, allowing for user-specific connection initialization. - Modified the connection logic to ensure proper initialization only occurs when a userId is provided, improving session management. - Refactored related thunks to use a consistent naming convention for userId parameters, enhancing code clarity. This update improves the handling of user sessions in the MTProtoService, aligning with recent enhancements in user-specific state management. * Refactor Telegram connection handling for user-specific management - Updated the TelegramConnectionModal to utilize user-specific identifiers for connection status and authentication processes, enhancing session management. - Refactored related components and selectors to ensure userId is consistently passed and utilized, improving clarity and maintainability. - Enhanced the ConnectionsPanel and ConnectStep to check for saved sessions based on userId, allowing for better handling of multiple user connections. This update significantly improves the application's ability to manage user-specific Telegram connections and enhances the overall user experience. * Refactor Telegram connection logic to simplify session management - Removed unnecessary userId checks and localStorage interactions from ConnectionsPanel and ConnectStep components, streamlining the connection status determination. - Updated MTProtoService to manage session data through Redux instead of localStorage, enhancing consistency and maintainability. - Improved session loading and saving logic to focus solely on Redux state, ensuring better integration with user-specific session handling. This update enhances the clarity and efficiency of the Telegram connection management process, aligning with recent improvements in user-specific state management. * Update CLAUDE.md to clarify localStorage usage and emphasize Redux for state management - Revised documentation to discourage the use of localStorage and sessionStorage for app state, advocating for Redux and Redux Persist instead. - Added guidelines for removing existing localStorage usage and migrating relevant data to Redux. - Updated service layer documentation to reflect changes in session management, specifying that session data is now stored in Redux rather than localStorage. - Enhanced clarity on deep link handling and the prevention of infinite reload loops. This update aligns with recent improvements in user-specific state management and reinforces best practices for state handling. * Enhance onboarding process with error handling and API integration - Updated the Onboarding component to include an API call for marking onboarding as complete, with error handling to provide user feedback. - Refactored the GetStartedStep component to support asynchronous completion handling, including loading state and error messages. - Introduced a new onboardingComplete method in the userApi service to facilitate the onboarding completion process. This update improves the user experience during onboarding by ensuring proper error management and feedback. * Enhance TelegramConnectionModal with socket connection management and improved initialization flow - Integrated socket connection handling in the TelegramConnectionModal to ensure the socket is connected when the modal opens, enhancing real-time features. - Refactored the initialization logic to streamline the connection process, including user verification and error handling. - Introduced a reference to track the QR code flow state, preventing multiple initiations and improving user experience during authentication. This update improves the reliability of the Telegram connection process and enhances user feedback during the onboarding experience. * Refactor Telegram connection checks for improved logic consistency - Updated the ConnectionsPanel and ConnectStep components to refine the logic for determining if a Telegram connection is established, ensuring that both session string and authentication status are required for a valid connection. - Simplified className definitions in the UI components for better readability and maintainability. This update enhances the clarity of connection status checks and improves the overall user experience during the onboarding process. * Refactor Telegram authentication flow and improve error handling - Simplified the TelegramLoginButton component by removing complex authentication logic and replacing it with a direct link to the Telegram bot for login. - Enhanced the Login page to consume a login token from the URL, providing better error handling and user feedback during the authentication process. - Introduced a new API service for consuming login tokens, ensuring secure retrieval of JWT tokens from the backend. This update streamlines the authentication experience and improves error management, enhancing overall user experience during login.
This commit is contained in:
@@ -79,11 +79,18 @@ State lives in `src/store/` using Redux Toolkit slices:
|
||||
- **socketSlice** — connection status, socket ID
|
||||
- **telegramSlice** — connection/auth status, chats, messages, threads (selectively persisted; loading/error states excluded)
|
||||
|
||||
Redux Persist stores auth and telegram state to localStorage. The telegram slice has a complex nested structure in `src/store/telegram/` with separate files for types, reducers, extraReducers, and thunks.
|
||||
Redux Persist stores auth and telegram state (storage backend is configurable; default uses localStorage). The telegram slice has a complex nested structure in `src/store/telegram/` with separate files for types, reducers, extraReducers, and thunks.
|
||||
|
||||
### LocalStorage
|
||||
|
||||
- **Do not use `localStorage` (or `sessionStorage`) for app state or feature logic.** Use Redux (and Redux Persist where needed) instead.
|
||||
- **Remove any existing `localStorage` usage** when touching related code. User-scoped data (auth, onboarding, Telegram session, socket state) lives in Redux, keyed by user id where applicable. Telegram session is in `telegram.byUser[userId].sessionString`, not localStorage.
|
||||
- **Exceptions**: Redux-persist may use a localStorage-backed storage adapter by default; that is the persistence layer, not app logic. Any other remaining usage (e.g. deep-link `deepLinkHandled` flag) should be migrated to Redux or similar when that code is modified.
|
||||
- **General rule**: Avoid adding new `localStorage` or `sessionStorage` usage; prefer Redux and remove existing usage when you work on affected areas.
|
||||
|
||||
### Service Layer (Singletons)
|
||||
|
||||
- **mtprotoService** (`src/services/mtprotoService.ts`) — Telegram MTProto client via `telegram` npm package. Session stored in localStorage as `telegram_session`. Auto-retries FLOOD_WAIT up to 60s.
|
||||
- **mtprotoService** (`src/services/mtprotoService.ts`) — Telegram MTProto client via `telegram` npm package. Session stored in Redux (`telegram.byUser[userId].sessionString`), not localStorage. Auto-retries FLOOD_WAIT up to 60s.
|
||||
- **socketService** (`src/services/socketService.ts`) — Socket.io client. Auth token passed in socket `auth` object (not query string). Transports: polling first, then WebSocket.
|
||||
- **apiClient** (`src/services/apiClient.ts`) — HTTP client for REST backend.
|
||||
|
||||
@@ -116,7 +123,7 @@ Web-to-desktop handoff using `outsourced://` URL scheme:
|
||||
4. Backend returns `sessionToken` + user object
|
||||
5. App stores session in Redux, navigates to onboarding/home
|
||||
|
||||
Key file: `src/utils/desktopDeepLinkListener.ts` (lazy-loaded in `main.tsx`). Uses `localStorage.deepLinkHandled` flag to prevent infinite reload loops. Deep links do NOT work in `tauri dev` on macOS — must use built `.app` bundle.
|
||||
Key file: `src/utils/desktopDeepLinkListener.ts` (lazy-loaded in `main.tsx`). Uses a `deepLinkHandled` flag to prevent infinite reload loops. Deep links do NOT work in `tauri dev` on macOS — must use built `.app` bundle.
|
||||
|
||||
### Rust Backend (`src-tauri/src/lib.rs`)
|
||||
|
||||
@@ -180,6 +187,7 @@ Key updates from recent commits:
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- **No localStorage**: Avoid `localStorage` and `sessionStorage`; use Redux (and persist) for app state. Remove any direct usage when working on affected code.
|
||||
- **Modal System**: Settings modal uses `createPortal` pattern with URL-based routing. Clean white design (not glass morphism) for system settings. Navigate with `/settings` and `/settings/connections` paths.
|
||||
- **Component Reuse**: Connection management reuses `connectOptions` array and components from onboarding flow. Maintains consistent UX patterns across features.
|
||||
- **Redux Integration**: Settings modal integrates with existing slices - auth for logout, user for profile display, telegram for connection status. No new state management needed.
|
||||
|
||||
+17
-14
@@ -1,22 +1,25 @@
|
||||
import { HashRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { store, persistor } from './store';
|
||||
import SocketProvider from './providers/SocketProvider';
|
||||
import TelegramProvider from './providers/TelegramProvider';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import { HashRouter as Router } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import { store, persistor } from "./store";
|
||||
import UserProvider from "./providers/UserProvider";
|
||||
import SocketProvider from "./providers/SocketProvider";
|
||||
import TelegramProvider from "./providers/TelegramProvider";
|
||||
import AppRoutes from "./AppRoutes";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SocketProvider>
|
||||
<TelegramProvider>
|
||||
<Router>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
</TelegramProvider>
|
||||
</SocketProvider>
|
||||
<UserProvider>
|
||||
<SocketProvider>
|
||||
<TelegramProvider>
|
||||
<Router>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
</TelegramProvider>
|
||||
</SocketProvider>
|
||||
</UserProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
+19
-11
@@ -1,12 +1,12 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import Welcome from './pages/Welcome';
|
||||
import Login from './pages/Login';
|
||||
import Onboarding from './pages/onboarding/Onboarding';
|
||||
import Home from './pages/Home';
|
||||
import PublicRoute from './components/PublicRoute';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import DefaultRedirect from './components/DefaultRedirect';
|
||||
import SettingsModal from './components/settings/SettingsModal';
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Welcome from "./pages/Welcome";
|
||||
import Login from "./pages/Login";
|
||||
import Onboarding from "./pages/onboarding/Onboarding";
|
||||
import Home from "./pages/Home";
|
||||
import PublicRoute from "./components/PublicRoute";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import DefaultRedirect from "./components/DefaultRedirect";
|
||||
import SettingsModal from "./components/settings/SettingsModal";
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
@@ -42,7 +42,11 @@ const AppRoutes = () => {
|
||||
<Route
|
||||
path="/home"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
|
||||
<ProtectedRoute
|
||||
requireAuth={true}
|
||||
requireOnboarded={true}
|
||||
redirectTo="/onboarding"
|
||||
>
|
||||
<Home />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
@@ -52,7 +56,11 @@ const AppRoutes = () => {
|
||||
<Route
|
||||
path="/settings/*"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true} requireOnboarded={true} redirectTo="/onboarding">
|
||||
<ProtectedRoute
|
||||
requireAuth={true}
|
||||
requireOnboarded={true}
|
||||
redirectTo="/onboarding"
|
||||
>
|
||||
<Home />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const GmailIcon = ({ className = "w-4 h-4" }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 5.5v13.05c0 .85-.73 1.59-1.59 1.59H1.59C.73 21.14 0 20.4 0 19.55V5.5L12 13.25 24 5.5zM24 4.5c0-.42-.2-.83-.53-1.09L12 11.25.53 3.41C.2 3.67 0 4.08 0 4.5v.75L12 13 24 5.25V4.5z"/>
|
||||
<path d="M5.5 4.5L12 9.75 18.5 4.5H5.5z" opacity="0.3"/>
|
||||
<path d="M24 5.5v13.05c0 .85-.73 1.59-1.59 1.59H1.59C.73 21.14 0 20.4 0 19.55V5.5L12 13.25 24 5.5zM24 4.5c0-.42-.2-.83-.53-1.09L12 11.25.53 3.41C.2 3.67 0 4.08 0 4.5v.75L12 13 24 5.25V4.5z" />
|
||||
<path d="M5.5 4.5L12 9.75 18.5 4.5H5.5z" opacity="0.3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
const GoogleIcon = ({ className = "w-5 h-5" }: { className?: string }) => {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
|
||||
interface ConnectionIndicatorProps {
|
||||
status?: 'connected' | 'disconnected' | 'connecting';
|
||||
status?: "connected" | "disconnected" | "connecting";
|
||||
description?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ConnectionIndicator = ({
|
||||
status: overrideStatus,
|
||||
description = 'Your browser is now connected to the AlphaHuman AI. Keep this tab open to keep the connection alive. You can message your assistant with the button below.',
|
||||
className = '',
|
||||
description = "Your browser is now connected to the AlphaHuman AI. Keep this tab open to keep the connection alive. You can message your assistant with the button below.",
|
||||
className = "",
|
||||
}: ConnectionIndicatorProps) => {
|
||||
// Use socket store status, but allow override via props
|
||||
const storeStatus = useAppSelector((state) => state.socket.status);
|
||||
const storeStatus = useAppSelector(selectSocketStatus);
|
||||
const status = overrideStatus || storeStatus;
|
||||
const statusConfig = {
|
||||
connected: {
|
||||
color: 'bg-sage-500',
|
||||
textColor: 'text-sage-500',
|
||||
text: 'Connected to AlphaHuman AI 🚀',
|
||||
color: "bg-sage-500",
|
||||
textColor: "text-sage-500",
|
||||
text: "Connected to AlphaHuman AI 🚀",
|
||||
},
|
||||
disconnected: {
|
||||
color: 'bg-coral-500',
|
||||
textColor: 'text-coral-500',
|
||||
text: 'Disconnected',
|
||||
color: "bg-coral-500",
|
||||
textColor: "text-coral-500",
|
||||
text: "Disconnected",
|
||||
},
|
||||
connecting: {
|
||||
color: 'bg-amber-500',
|
||||
textColor: 'text-amber-500',
|
||||
text: 'Connecting',
|
||||
color: "bg-amber-500",
|
||||
textColor: "text-amber-500",
|
||||
text: "Connecting",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,7 +38,9 @@ const ConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${config.color} rounded-full ${status === "connected" ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<span className={`text-sm ${config.textColor}`}>{config.text}</span>
|
||||
</div>
|
||||
{description && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
/**
|
||||
* Default redirect component that routes users based on their auth and onboarding status
|
||||
@@ -9,7 +10,7 @@ import { useAppSelector } from '../store/hooks';
|
||||
*/
|
||||
const DefaultRedirect = () => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
if (token && isOnboarded) {
|
||||
return <Navigate to="/home" replace />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Design System Showcase Component
|
||||
@@ -46,17 +46,23 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-12 flex flex-wrap gap-4">
|
||||
<button className="btn-premium">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Start Trading
|
||||
</button>
|
||||
<button className="btn-glass">
|
||||
View Portfolio
|
||||
</button>
|
||||
<button className="btn-outline">
|
||||
Market Analysis
|
||||
</button>
|
||||
<button className="btn-glass">View Portfolio</button>
|
||||
<button className="btn-outline">Market Analysis</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -70,19 +76,25 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Primary Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Primary Ocean</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Primary Ocean
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-500 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 500</p>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 500
|
||||
</p>
|
||||
<p className="text-xs text-stone-500">#5B9BF3</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-primary-600 rounded-xl shadow-soft" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-900">Primary 600</p>
|
||||
<p className="text-sm font-medium text-stone-900">
|
||||
Primary 600
|
||||
</p>
|
||||
<p className="text-xs text-stone-500">#4A83DD</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +103,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Success Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Sage Success</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Sage Success
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-sage-500 rounded-xl shadow-soft" />
|
||||
@@ -112,7 +126,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
|
||||
{/* Accent Colors */}
|
||||
<div className="card-elevated p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">Accent Palette</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4">
|
||||
Accent Palette
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-accent-lavender rounded-xl shadow-soft" />
|
||||
@@ -164,14 +180,17 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Body · 1rem</p>
|
||||
<p className="text-base text-stone-700 leading-relaxed max-w-3xl">
|
||||
The typography system is designed to create clear visual hierarchy while maintaining
|
||||
excellent readability. Each text size has been carefully calibrated with appropriate
|
||||
line heights and letter spacing to ensure optimal legibility across all device sizes.
|
||||
The typography system is designed to create clear visual hierarchy
|
||||
while maintaining excellent readability. Each text size has been
|
||||
carefully calibrated with appropriate line heights and letter
|
||||
spacing to ensure optimal legibility across all device sizes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-stone-500 mb-1">Monospace · For prices and data</p>
|
||||
<p className="text-xs text-stone-500 mb-1">
|
||||
Monospace · For prices and data
|
||||
</p>
|
||||
<p className="text-crypto-price text-2xl text-stone-900">
|
||||
$48,392.50 <span className="text-market-bullish">+2.45%</span>
|
||||
</p>
|
||||
@@ -205,7 +224,9 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
<div className="space-y-2">
|
||||
<p className="price-ticker up">$48,392.50</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-market-bullish font-medium">+$1,185.20</span>
|
||||
<span className="text-sm text-market-bullish font-medium">
|
||||
+$1,185.20
|
||||
</span>
|
||||
<span className="text-sm text-market-bullish">(+2.45%)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,8 +243,18 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
</p>
|
||||
<div className="flex items-center text-primary-600 font-medium">
|
||||
<span>View Details</span>
|
||||
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<svg
|
||||
className="w-4 h-4 ml-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,25 +262,43 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Elevated Card */}
|
||||
<div className="card-elevated">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-stone-900">Quick Stats</h3>
|
||||
<h3 className="text-lg font-semibold text-stone-900">
|
||||
Quick Stats
|
||||
</h3>
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
<svg
|
||||
className="w-4 h-4 text-primary-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Total Value</span>
|
||||
<span className="font-mono font-semibold text-stone-900">$125,430</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
$125,430
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">24h Change</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">+3.2%</span>
|
||||
<span className="font-mono font-semibold text-market-bullish">
|
||||
+3.2%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-stone-600">Holdings</span>
|
||||
<span className="font-mono font-semibold text-stone-900">12</span>
|
||||
<span className="font-mono font-semibold text-stone-900">
|
||||
12
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,32 +327,82 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
{/* Navigation Items */}
|
||||
<nav className="p-2">
|
||||
<a href="#" className="nav-item-premium active">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
/>
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
Profile Settings
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
Notifications
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
Security
|
||||
</a>
|
||||
<a href="#" className="nav-item-premium">
|
||||
<svg className="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="w-5 h-5 mr-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
Help & Support
|
||||
</a>
|
||||
@@ -367,9 +466,7 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button className="btn-premium w-full">
|
||||
Complete Transaction
|
||||
</button>
|
||||
<button className="btn-premium w-full">Complete Transaction</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -377,4 +474,4 @@ const DesignSystemShowcase: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignSystemShowcase;
|
||||
export default DesignSystemShowcase;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import GmailIcon from '../assets/icons/GmailIcon';
|
||||
import GmailIcon from "../assets/icons/GmailIcon";
|
||||
|
||||
interface GmailConnectionIndicatorProps {
|
||||
description?: string;
|
||||
@@ -7,7 +7,7 @@ interface GmailConnectionIndicatorProps {
|
||||
|
||||
const GmailConnectionIndicator = ({
|
||||
description,
|
||||
className = '',
|
||||
className = "",
|
||||
}: GmailConnectionIndicatorProps) => {
|
||||
// Gmail is always offline for now (placeholder)
|
||||
const gmailIsOnline = false;
|
||||
@@ -15,11 +15,17 @@ const GmailConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${gmailIsOnline ? 'bg-red-500' : 'bg-gray-500'} rounded-full ${gmailIsOnline ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${gmailIsOnline ? "bg-red-500" : "bg-gray-500"} rounded-full ${gmailIsOnline ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<GmailIcon className={`w-4 h-4 ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`} />
|
||||
<span className={`text-sm ${gmailIsOnline ? 'text-red-500' : 'text-gray-500'}`}>
|
||||
{gmailIsOnline ? 'Connected to Gmail' : 'Gmail is Offline'}
|
||||
<GmailIcon
|
||||
className={`w-4 h-4 ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${gmailIsOnline ? "text-red-500" : "text-gray-500"}`}
|
||||
>
|
||||
{gmailIsOnline ? "Connected to Gmail" : "Gmail is Offline"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLottie } from 'lottie-react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLottie } from "lottie-react";
|
||||
|
||||
interface LottieAnimationProps {
|
||||
src: string;
|
||||
@@ -8,14 +8,21 @@ interface LottieAnimationProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const LottieAnimation = ({ src, className = '', height = 200, width = 200 }: LottieAnimationProps) => {
|
||||
const LottieAnimation = ({
|
||||
src,
|
||||
className = "",
|
||||
height = 200,
|
||||
width = 200,
|
||||
}: LottieAnimationProps) => {
|
||||
const [animationData, setAnimationData] = useState<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(src)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setAnimationData(data))
|
||||
.catch((error) => console.error('Failed to load Lottie animation:', error));
|
||||
.catch((error) =>
|
||||
console.error("Failed to load Lottie animation:", error),
|
||||
);
|
||||
}, [src]);
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -3,13 +3,18 @@ interface PrivacyFeatureCardProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PrivacyFeatureCard = ({ title, description }: PrivacyFeatureCardProps) => {
|
||||
const PrivacyFeatureCard = ({
|
||||
title,
|
||||
description,
|
||||
}: PrivacyFeatureCardProps) => {
|
||||
return (
|
||||
<div className="bg-stone-800/50 rounded-xl p-3 border border-stone-700">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm mb-2 text-center">{title}</h3>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">{description}</p>
|
||||
<p className="opacity-70 text-xs leading-relaxed text-center">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,17 @@ interface ProgressIndicatorProps {
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ currentStep, totalSteps }: ProgressIndicatorProps) => {
|
||||
const ProgressIndicator = ({
|
||||
currentStep,
|
||||
totalSteps,
|
||||
}: ProgressIndicatorProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center space-x-1.5 mb-6">
|
||||
{Array.from({ length: totalSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-8 h-0.5 rounded-full ${
|
||||
index < currentStep ? 'bg-primary-500' : 'bg-stone-700'
|
||||
index < currentStep ? "bg-primary-500" : "bg-stone-700"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,16 +19,16 @@ const ProtectedRoute = ({
|
||||
redirectTo,
|
||||
}: ProtectedRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If auth is required but user is not logged in
|
||||
if (requireAuth && !token) {
|
||||
return <Navigate to={redirectTo || '/'} replace />;
|
||||
return <Navigate to={redirectTo || "/"} replace />;
|
||||
}
|
||||
|
||||
// If onboarding is required but user is not onboarded
|
||||
if (requireOnboarded && !isOnboarded) {
|
||||
return <Navigate to={redirectTo || '/onboarding'} replace />;
|
||||
return <Navigate to={redirectTo || "/onboarding"} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectIsOnboarded } from "../store/authSelectors";
|
||||
|
||||
interface PublicRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -13,11 +14,11 @@ interface PublicRouteProps {
|
||||
*/
|
||||
const PublicRoute = ({ children, redirectTo }: PublicRouteProps) => {
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
const isOnboarded = useAppSelector((state) => state.auth.isOnboarded);
|
||||
const isOnboarded = useAppSelector(selectIsOnboarded);
|
||||
|
||||
// If user is logged in and onboarded, redirect to home
|
||||
if (token && isOnboarded) {
|
||||
return <Navigate to={redirectTo || '/home'} replace />;
|
||||
return <Navigate to={redirectTo || "/home"} replace />;
|
||||
}
|
||||
|
||||
// If user is logged in but not onboarded, redirect to onboarding
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTelegram } from '../providers/TelegramProvider';
|
||||
import TelegramIcon from '../assets/icons/telegram.svg';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTelegram } from "../providers/TelegramProvider";
|
||||
import TelegramIcon from "../assets/icons/telegram.svg";
|
||||
|
||||
interface TelegramConnectionIndicatorProps {
|
||||
description?: string;
|
||||
@@ -9,7 +9,7 @@ interface TelegramConnectionIndicatorProps {
|
||||
|
||||
const TelegramConnectionIndicator = ({
|
||||
description,
|
||||
className = '',
|
||||
className = "",
|
||||
}: TelegramConnectionIndicatorProps) => {
|
||||
const { isAuthenticated, connectionStatus, checkConnection } = useTelegram();
|
||||
const [telegramIsOnline, setTelegramIsOnline] = useState(false);
|
||||
@@ -26,13 +26,13 @@ const TelegramConnectionIndicator = ({
|
||||
const isConnected = await checkConnection();
|
||||
setTelegramIsOnline(isConnected);
|
||||
} catch (error) {
|
||||
console.warn('Failed to check Telegram connection:', error);
|
||||
console.warn("Failed to check Telegram connection:", error);
|
||||
setTelegramIsOnline(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately if connected
|
||||
if (connectionStatus === 'connected') {
|
||||
if (connectionStatus === "connected") {
|
||||
checkTelegramConnection();
|
||||
} else {
|
||||
setTelegramIsOnline(false);
|
||||
@@ -52,11 +52,19 @@ const TelegramConnectionIndicator = ({
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<div className="flex items-center justify-center space-x-2 mb-3">
|
||||
<div className={`w-2 h-2 ${telegramIsOnline ? 'bg-blue-500' : 'bg-gray-500'} rounded-full ${telegramIsOnline ? 'animate-pulse' : ''}`}></div>
|
||||
<div
|
||||
className={`w-2 h-2 ${telegramIsOnline ? "bg-blue-500" : "bg-gray-500"} rounded-full ${telegramIsOnline ? "animate-pulse" : ""}`}
|
||||
></div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<img src={TelegramIcon} alt="Telegram" className={`w-4 h-4 ${telegramIsOnline ? 'opacity-100' : 'opacity-50'}`} />
|
||||
<span className={`text-sm ${telegramIsOnline ? 'text-blue-500' : 'text-gray-500'}`}>
|
||||
{telegramIsOnline ? 'Connected to Telegram' : 'Telegram is Offline'}
|
||||
<img
|
||||
src={TelegramIcon}
|
||||
alt="Telegram"
|
||||
className={`w-4 h-4 ${telegramIsOnline ? "opacity-100" : "opacity-50"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${telegramIsOnline ? "text-blue-500" : "text-gray-500"}`}
|
||||
>
|
||||
{telegramIsOnline ? "Connected to Telegram" : "Telegram is Offline"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { useAppDispatch, useAppSelector } from "../store/hooks";
|
||||
import {
|
||||
initializeTelegram,
|
||||
connectTelegram,
|
||||
@@ -9,9 +9,17 @@ import {
|
||||
setAuthStatus,
|
||||
setAuthError,
|
||||
setConnectionStatus,
|
||||
} from '../store/telegram';
|
||||
import { selectIsInitialized, selectConnectionStatus } from '../store/telegramSelectors';
|
||||
import { mtprotoService } from '../services/mtprotoService';
|
||||
resetTelegramForUser,
|
||||
} from "../store/telegram";
|
||||
import {
|
||||
selectIsInitialized,
|
||||
selectConnectionStatus,
|
||||
selectTelegramCurrentUserId,
|
||||
} from "../store/telegramSelectors";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
import { mtprotoService } from "../services/mtprotoService";
|
||||
import { socketService } from "../services/socketService";
|
||||
import type { User } from "../types/api";
|
||||
|
||||
interface TelegramConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -19,15 +27,30 @@ interface TelegramConnectionModalProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
type ConnectionStep = 'qr' | '2fa' | 'loading' | 'error';
|
||||
type ConnectionStep = "qr" | "2fa" | "loading" | "error";
|
||||
|
||||
const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnectionModalProps) => {
|
||||
const TELEGRAM_ACCOUNT_MISMATCH_ERROR =
|
||||
"This Telegram account doesn't match your logged-in account. Please use the Telegram account you signed up with.";
|
||||
|
||||
function telegramMeIdMatchesUser(me: { id: unknown }, appUser: User): boolean {
|
||||
return String(me.id) === String(appUser.telegramId);
|
||||
}
|
||||
|
||||
const TelegramConnectionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: TelegramConnectionModalProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const userId = useAppSelector(selectTelegramCurrentUserId);
|
||||
const isInitialized = useAppSelector(selectIsInitialized);
|
||||
const connectionStatus = useAppSelector(selectConnectionStatus);
|
||||
const socketStatus = useAppSelector(selectSocketStatus);
|
||||
const token = useAppSelector((state) => state.auth.token);
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ConnectionStep>('qr');
|
||||
const [password, setPassword] = useState('');
|
||||
const [currentStep, setCurrentStep] = useState<ConnectionStep>("qr");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordHint, setPasswordHint] = useState<string | undefined>();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [qrCodeExpires, setQrCodeExpires] = useState<number | null>(null);
|
||||
@@ -35,53 +58,40 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
|
||||
// Store password promise resolver
|
||||
const passwordResolverRef = useRef<((password: string) => void) | null>(null);
|
||||
const qrFlowStartedRef = useRef(false);
|
||||
|
||||
// Initialize and connect when modal opens
|
||||
// Ensure socket is connected when modal opens (e.g. for MCP / real-time features)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen) {
|
||||
qrFlowStartedRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (token && socketStatus !== "connected") {
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
setCurrentStep('loading');
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram()).unwrap();
|
||||
}
|
||||
if (connectionStatus !== 'connected') {
|
||||
await dispatch(connectTelegram()).unwrap();
|
||||
}
|
||||
// Check if already authenticated (but don't fail if not authenticated)
|
||||
try {
|
||||
const authCheck = await dispatch(checkAuthStatus()).unwrap();
|
||||
if (authCheck) {
|
||||
onComplete();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
// If checkAuthStatus fails, we're not authenticated - continue with QR flow
|
||||
console.log('Not authenticated, proceeding with QR code flow');
|
||||
}
|
||||
// Start QR code flow
|
||||
startQrCodeFlow();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Telegram';
|
||||
setError(errorMessage);
|
||||
setCurrentStep('error');
|
||||
dispatch(setConnectionStatus('error'));
|
||||
}
|
||||
};
|
||||
socketService.connect(token);
|
||||
}
|
||||
}, [isOpen, token, socketStatus]);
|
||||
|
||||
init();
|
||||
}, [isOpen, isInitialized, connectionStatus, dispatch, onComplete, onClose]);
|
||||
const handleTelegramAccountMismatch = useCallback(async () => {
|
||||
if (!userId) return;
|
||||
setError(TELEGRAM_ACCOUNT_MISMATCH_ERROR);
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
try {
|
||||
await mtprotoService.clearSessionAndDisconnect(userId);
|
||||
} catch (e) {
|
||||
console.warn("clearSessionAndDisconnect failed:", e);
|
||||
}
|
||||
dispatch(resetTelegramForUser({ userId }));
|
||||
}, [dispatch, userId]);
|
||||
|
||||
const startQrCodeFlow = useCallback(async () => {
|
||||
try {
|
||||
setIsAuthenticating(true);
|
||||
setError(null);
|
||||
setCurrentStep('qr');
|
||||
setPassword('');
|
||||
setCurrentStep("qr");
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
setQrCodeUrl(null);
|
||||
setQrCodeExpires(null);
|
||||
@@ -94,15 +104,17 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// Convert Uint8Array to base64url
|
||||
const binary = Array.from(qrCode.token)
|
||||
.map((byte) => String.fromCharCode(byte))
|
||||
.join('');
|
||||
.join("");
|
||||
tokenBase64 = btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
} else {
|
||||
// If it's a Buffer, use toString
|
||||
const buffer = qrCode.token as { toString: (encoding: string) => string };
|
||||
tokenBase64 = buffer.toString('base64url');
|
||||
const buffer = qrCode.token as {
|
||||
toString: (encoding: string) => string;
|
||||
};
|
||||
tokenBase64 = buffer.toString("base64url");
|
||||
}
|
||||
const url = `tg://login?token=${tokenBase64}`;
|
||||
setQrCodeUrl(url);
|
||||
@@ -110,17 +122,20 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
},
|
||||
async (hint) => {
|
||||
// 2FA password required
|
||||
console.log('Password callback invoked with hint:', hint);
|
||||
console.log("Password callback invoked with hint:", hint);
|
||||
setPasswordHint(hint);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
|
||||
// Wait for user to enter password
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
passwordResolverRef.current = (password: string) => {
|
||||
console.log('Password resolved in callback, sending to Telegram:', password ? '***' : 'empty');
|
||||
console.log(
|
||||
"Password resolved in callback, sending to Telegram:",
|
||||
password ? "***" : "empty",
|
||||
);
|
||||
if (!password || !password.trim()) {
|
||||
reject(new Error('Password is required'));
|
||||
reject(new Error("Password is required"));
|
||||
return;
|
||||
}
|
||||
resolve(password.trim());
|
||||
@@ -129,8 +144,10 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// Set a timeout to prevent hanging forever
|
||||
setTimeout(() => {
|
||||
if (passwordResolverRef.current) {
|
||||
console.warn('Password callback timeout - no password provided');
|
||||
reject(new Error('Password input timeout'));
|
||||
console.warn(
|
||||
"Password callback timeout - no password provided",
|
||||
);
|
||||
reject(new Error("Password input timeout"));
|
||||
passwordResolverRef.current = null;
|
||||
}
|
||||
}, 300000); // 5 minutes timeout
|
||||
@@ -138,64 +155,144 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
},
|
||||
async (err) => {
|
||||
// Handle errors
|
||||
const errorMessage = err.message || 'Authentication error';
|
||||
const errorMessage = err.message || "Authentication error";
|
||||
|
||||
// Check if it's a 2FA password needed error
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED') || errorMessage.includes('PASSWORD')) {
|
||||
if (
|
||||
errorMessage.includes("SESSION_PASSWORD_NEEDED") ||
|
||||
errorMessage.includes("PASSWORD")
|
||||
) {
|
||||
// This should trigger the password callback, but if it doesn't, handle it here
|
||||
setPasswordHint(undefined);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
// Don't stop authentication - let the password callback handle it
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
dispatch(setAuthError(errorMessage));
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
|
||||
// Check if it's a cancellation
|
||||
if (errorMessage.includes('AUTH_USER_CANCEL') || errorMessage.includes('cancel')) {
|
||||
setCurrentStep('qr');
|
||||
if (
|
||||
errorMessage.includes("AUTH_USER_CANCEL") ||
|
||||
errorMessage.includes("cancel")
|
||||
) {
|
||||
setCurrentStep("qr");
|
||||
setIsAuthenticating(false);
|
||||
return true; // Stop authentication
|
||||
}
|
||||
|
||||
return false; // Continue
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Authentication successful
|
||||
setIsAuthenticating(false);
|
||||
let me: { id: unknown } | null = null;
|
||||
try {
|
||||
await dispatch(checkAuthStatus()).unwrap();
|
||||
me = await dispatch(checkAuthStatus(userId)).unwrap();
|
||||
} catch (err) {
|
||||
// Even if checkAuthStatus fails, we know we just authenticated
|
||||
console.warn('checkAuthStatus failed after authentication, but continuing:', err);
|
||||
console.warn("checkAuthStatus failed after authentication:", err);
|
||||
}
|
||||
dispatch(setAuthStatus('authenticated'));
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
if (!me || !telegramMeIdMatchesUser(me, user)) {
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
dispatch(setAuthStatus({ userId, status: "authenticated" }));
|
||||
onComplete();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setIsAuthenticating(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Authentication failed";
|
||||
|
||||
// If it's a password needed error, we should already be on the 2FA step
|
||||
// Don't show it as an error, just log it
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED')) {
|
||||
console.log('Password required - user should be on 2FA step');
|
||||
// If we're not on 2FA step, switch to it
|
||||
if (currentStep !== '2fa') {
|
||||
setCurrentStep('2fa');
|
||||
if (errorMessage.includes("SESSION_PASSWORD_NEEDED")) {
|
||||
console.log("Password required - user should be on 2FA step");
|
||||
if (currentStep !== "2fa") {
|
||||
setCurrentStep("2fa");
|
||||
setPasswordHint(undefined);
|
||||
}
|
||||
return; // Don't show error, password callback should handle it
|
||||
return;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
setCurrentStep('error');
|
||||
dispatch(setAuthError(errorMessage));
|
||||
setCurrentStep("error");
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
}
|
||||
}, [dispatch, onComplete, onClose]);
|
||||
}, [
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !userId) return;
|
||||
if (qrFlowStartedRef.current) return;
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
setCurrentStep("loading");
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram(userId)).unwrap();
|
||||
}
|
||||
if (connectionStatus !== "connected") {
|
||||
await dispatch(connectTelegram(userId)).unwrap();
|
||||
}
|
||||
try {
|
||||
const authCheck = await dispatch(checkAuthStatus(userId)).unwrap();
|
||||
if (authCheck) {
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
if (!telegramMeIdMatchesUser(authCheck, user)) {
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
onComplete();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Not authenticated, proceeding with QR code flow");
|
||||
}
|
||||
if (qrFlowStartedRef.current) return;
|
||||
qrFlowStartedRef.current = true;
|
||||
await startQrCodeFlow();
|
||||
} catch (err) {
|
||||
qrFlowStartedRef.current = false;
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to initialize Telegram";
|
||||
setError(errorMessage);
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
}
|
||||
};
|
||||
|
||||
void init();
|
||||
}, [
|
||||
isOpen,
|
||||
isInitialized,
|
||||
connectionStatus,
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
startQrCodeFlow,
|
||||
]);
|
||||
|
||||
// Update countdown timer every second and reload QR code on timeout
|
||||
useEffect(() => {
|
||||
@@ -205,11 +302,14 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
}
|
||||
|
||||
const updateTimer = () => {
|
||||
const remaining = Math.max(0, Math.floor((qrCodeExpires * 1000 - Date.now()) / 1000));
|
||||
const remaining = Math.max(
|
||||
0,
|
||||
Math.floor((qrCodeExpires * 1000 - Date.now()) / 1000),
|
||||
);
|
||||
setTimeRemaining(remaining);
|
||||
|
||||
// If timer reaches 0 and we're on QR step, reload the QR code
|
||||
if (remaining === 0 && currentStep === 'qr' && !isAuthenticating) {
|
||||
if (remaining === 0 && currentStep === "qr" && !isAuthenticating) {
|
||||
startQrCodeFlow();
|
||||
}
|
||||
};
|
||||
@@ -225,68 +325,92 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
|
||||
// Poll authentication status every 5 seconds when QR code is displayed
|
||||
useEffect(() => {
|
||||
if (currentStep !== 'qr' || !qrCodeUrl || isAuthenticating) {
|
||||
if (currentStep !== "qr" || !qrCodeUrl || isAuthenticating) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
const pollAuthStatus = async () => {
|
||||
try {
|
||||
if (!mtprotoService.isReady()) return;
|
||||
const client = mtprotoService.getClient();
|
||||
if (!client) return;
|
||||
|
||||
const isAuthorized = await client.checkAuthorization();
|
||||
|
||||
if (isAuthorized) {
|
||||
// User is authenticated - check if we need password
|
||||
try {
|
||||
// Try to get user info - if this fails with SESSION_PASSWORD_NEEDED, we need password
|
||||
await client.getMe();
|
||||
// If getMe succeeds, authentication is complete
|
||||
console.log('QR code scanned and authenticated successfully');
|
||||
// The signInWithQrCode promise should resolve, but if it doesn't, trigger completion
|
||||
const me = await client.getMe();
|
||||
if (!user) {
|
||||
setError("Could not verify account. Please try again.");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
setIsAuthenticating(false);
|
||||
return;
|
||||
}
|
||||
if (!telegramMeIdMatchesUser(me, user)) {
|
||||
setIsAuthenticating(false);
|
||||
await handleTelegramAccountMismatch();
|
||||
return;
|
||||
}
|
||||
console.log("QR code scanned and authenticated successfully");
|
||||
setIsAuthenticating(false);
|
||||
dispatch(setAuthStatus('authenticated'));
|
||||
dispatch(setAuthStatus({ userId, status: "authenticated" }));
|
||||
onComplete();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
if (errorMessage.includes('SESSION_PASSWORD_NEEDED') || errorMessage.includes('PASSWORD')) {
|
||||
// Need password - switch to 2FA step
|
||||
console.log('Password required after QR scan - switching to 2FA step');
|
||||
if (currentStep === 'qr' && !passwordResolverRef.current) {
|
||||
// Trigger password callback manually if it wasn't called
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
if (
|
||||
errorMessage.includes("SESSION_PASSWORD_NEEDED") ||
|
||||
errorMessage.includes("PASSWORD")
|
||||
) {
|
||||
console.log(
|
||||
"Password required after QR scan - switching to 2FA step",
|
||||
);
|
||||
if (currentStep === "qr" && !passwordResolverRef.current) {
|
||||
setPasswordHint(undefined);
|
||||
setCurrentStep('2fa');
|
||||
setCurrentStep("2fa");
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore errors during polling - they're expected if not authenticated yet
|
||||
console.debug('Auth status check:', err instanceof Error ? err.message : String(err));
|
||||
console.debug(
|
||||
"Auth status check:",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately, then every 5 seconds
|
||||
checkAuthStatus();
|
||||
const interval = setInterval(checkAuthStatus, 5000);
|
||||
pollAuthStatus();
|
||||
const interval = setInterval(pollAuthStatus, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentStep, qrCodeUrl, isAuthenticating, dispatch, onComplete, onClose]);
|
||||
}, [
|
||||
currentStep,
|
||||
qrCodeUrl,
|
||||
isAuthenticating,
|
||||
dispatch,
|
||||
onComplete,
|
||||
onClose,
|
||||
user,
|
||||
userId,
|
||||
handleTelegramAccountMismatch,
|
||||
]);
|
||||
|
||||
const handle2FASubmit = async () => {
|
||||
const passwordValue = password.trim();
|
||||
|
||||
if (!passwordValue) {
|
||||
setError('Please enter your password');
|
||||
setError("Please enter your password");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordResolverRef.current) {
|
||||
console.error('Password resolver is null - password callback may not be active');
|
||||
setError('Authentication session expired. Please try again.');
|
||||
setCurrentStep('qr');
|
||||
console.error(
|
||||
"Password resolver is null - password callback may not be active",
|
||||
);
|
||||
setError("Authentication session expired. Please try again.");
|
||||
setCurrentStep("qr");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,7 +423,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
const resolver = passwordResolverRef.current;
|
||||
passwordResolverRef.current = null; // Clear before resolving to prevent double calls
|
||||
|
||||
console.log('Resolving password promise - sending password to Telegram');
|
||||
console.log("Resolving password promise - sending password to Telegram");
|
||||
resolver(passwordValue);
|
||||
|
||||
// The authentication will continue in the background via signInWithQrCode
|
||||
@@ -307,20 +431,21 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
// We keep isAuthenticating true until the promise resolves or rejects
|
||||
} catch (err) {
|
||||
setIsAuthenticating(false);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Password verification failed';
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Password verification failed";
|
||||
setError(errorMessage);
|
||||
dispatch(setAuthError(errorMessage));
|
||||
console.error('Error in handle2FASubmit:', err);
|
||||
dispatch(setAuthError({ userId, error: errorMessage }));
|
||||
console.error("Error in handle2FASubmit:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep === '2fa') {
|
||||
setCurrentStep('qr');
|
||||
setPassword('');
|
||||
if (currentStep === "2fa") {
|
||||
setCurrentStep("qr");
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
if (passwordResolverRef.current) {
|
||||
passwordResolverRef.current('');
|
||||
passwordResolverRef.current("");
|
||||
passwordResolverRef.current = null;
|
||||
}
|
||||
} else {
|
||||
@@ -328,12 +453,27 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
const handleRetry = async () => {
|
||||
if (!userId) return;
|
||||
setError(null);
|
||||
setPassword('');
|
||||
setPassword("");
|
||||
setPasswordHint(undefined);
|
||||
setQrCodeUrl(null);
|
||||
setQrCodeExpires(null);
|
||||
setCurrentStep("loading");
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await dispatch(initializeTelegram(userId)).unwrap();
|
||||
}
|
||||
if (connectionStatus !== "connected") {
|
||||
await dispatch(connectTelegram(userId)).unwrap();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to initialize");
|
||||
setCurrentStep("error");
|
||||
dispatch(setConnectionStatus({ userId, status: "error" }));
|
||||
return;
|
||||
}
|
||||
startQrCodeFlow();
|
||||
};
|
||||
|
||||
@@ -343,22 +483,22 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<div
|
||||
className="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm flex items-center justify-center"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: 9999
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-black/90 shadow-large animate-fade-up max-w-4xl max-h-[90vh] overflow-y-auto flex flex-col items-center justify-center rounded-3xl focus:outline-none"
|
||||
style={{
|
||||
maxWidth: '56rem',
|
||||
maxHeight: '90vh',
|
||||
padding: 0
|
||||
maxWidth: "56rem",
|
||||
maxHeight: "90vh",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{/* Close button */}
|
||||
@@ -366,27 +506,49 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 z-10 w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{currentStep === 'loading' ? (
|
||||
{currentStep === "loading" ? (
|
||||
<div className="text-center py-8 flex flex-col items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mb-4"></div>
|
||||
<p className="opacity-70">Initializing Telegram connection...</p>
|
||||
</div>
|
||||
) : currentStep === 'error' ? (
|
||||
) : currentStep === "error" ? (
|
||||
<>
|
||||
{/* Error Screen */}
|
||||
<div className="text-center flex flex-col items-center justify-center">
|
||||
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-8 h-8 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Connection Error</h2>
|
||||
<p className="opacity-70 text-sm mb-6">{error || 'An error occurred'}</p>
|
||||
<p className="opacity-70 text-sm mb-6">
|
||||
{error || "An error occurred"}
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
@@ -403,7 +565,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : currentStep === 'qr' ? (
|
||||
) : currentStep === "qr" ? (
|
||||
<>
|
||||
{/* QR Code Screen */}
|
||||
<div className="text-center flex flex-col items-center justify-center w-full">
|
||||
@@ -428,10 +590,14 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
{isAuthenticating ? (
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600 text-sm">Generating QR code...</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Generating QR code...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-600 text-sm">Loading QR code...</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Loading QR code...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -456,21 +622,27 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">1</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Open Telegram on your phone</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Open Telegram on your phone
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 text-left">
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">2</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Go to Settings > Devices > Link Desktop Device</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Go to Settings > Devices > Link Desktop Device
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3 text-left">
|
||||
<div className="w-6 h-6 bg-purple-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-white font-bold text-xs">3</span>
|
||||
</div>
|
||||
<p className="opacity-90 text-sm">Point your phone at this screen to confirm login</p>
|
||||
<p className="opacity-90 text-sm">
|
||||
Point your phone at this screen to confirm login
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -483,7 +655,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
<p className="opacity-70 text-sm mb-6">
|
||||
{passwordHint
|
||||
? `Your account is protected with two-step verification. Hint: ${passwordHint}`
|
||||
: 'Your account is protected with two-step verification. Please enter your password to continue.'}
|
||||
: "Your account is protected with two-step verification. Please enter your password to continue."}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
@@ -499,7 +671,11 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && password.trim() && !isAuthenticating) {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
password.trim() &&
|
||||
!isAuthenticating
|
||||
) {
|
||||
handle2FASubmit();
|
||||
}
|
||||
}}
|
||||
@@ -531,7 +707,7 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
|
||||
disabled={!password.trim() || isAuthenticating}
|
||||
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
{isAuthenticating ? 'Verifying...' : 'Continue'}
|
||||
{isAuthenticating ? "Verifying..." : "Continue"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,300 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BACKEND_URL, TELEGRAM_BOT_ID } from '../utils/config';
|
||||
import { useAppDispatch } from '../store/hooks';
|
||||
import { setToken } from '../store/authSlice';
|
||||
import { TELEGRAM_BOT_USERNAME } from "../utils/config";
|
||||
import { openUrl } from "../utils/openUrl";
|
||||
|
||||
interface TelegramAuthData {
|
||||
id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
interface TelegramLoginButtonProps {
|
||||
onSuccess?: () => void;
|
||||
className?: string;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const TelegramLoginButton = ({
|
||||
onSuccess,
|
||||
className = '',
|
||||
text = 'Yes, Login with Telegram',
|
||||
className = "",
|
||||
text = "Yes, Login with Telegram",
|
||||
disabled: externalDisabled = false,
|
||||
}: TelegramLoginButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
|
||||
const handleTelegramLogin = async () => {
|
||||
if (isAuthenticating || externalDisabled) return;
|
||||
|
||||
setIsAuthenticating(true);
|
||||
|
||||
try {
|
||||
const origin = window.location.origin;
|
||||
const botId = TELEGRAM_BOT_ID;
|
||||
// Pass origin as query param so backend can use it for postMessage
|
||||
const callbackUrl = `${BACKEND_URL}/auth/telegram/callback?frontend_origin=${encodeURIComponent(origin)}`;
|
||||
const oauthUrl = `https://oauth.telegram.org/auth?bot_id=${botId}&origin=${encodeURIComponent(origin)}&request_access=write&embed=0&return_to=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
// Open popup window
|
||||
const width = 550;
|
||||
const height = 470;
|
||||
const availLeft = 'availLeft' in screen ? (screen as { availLeft: number }).availLeft : 0;
|
||||
const availTop = 'availTop' in screen ? (screen as { availTop: number }).availTop : 0;
|
||||
const left = Math.max(0, (screen.width - width) / 2) + availLeft;
|
||||
const top = Math.max(0, (screen.height - height) / 2) + availTop;
|
||||
|
||||
const popup = window.open(
|
||||
oauthUrl,
|
||||
'telegram_oauth',
|
||||
`width=${width},height=${height},left=${left},top=${top},status=0,location=0,menubar=0,toolbar=0`
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
throw new Error('Failed to open popup window. Please allow popups for this site.');
|
||||
}
|
||||
|
||||
let authCompleted = false;
|
||||
|
||||
// Listen for postMessage from Telegram OAuth
|
||||
const messageHandler = async (event: MessageEvent) => {
|
||||
// 1) Only accept messages from Telegram OAuth
|
||||
if (event.origin !== 'https://oauth.telegram.org') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Parse the payload (Telegram sends a JSON string)
|
||||
let data: any;
|
||||
try {
|
||||
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Handle successful auth_result from Telegram
|
||||
if (data.event === 'auth_result' && data.result && !authCompleted) {
|
||||
authCompleted = true;
|
||||
window.removeEventListener('message', messageHandler);
|
||||
popup.close();
|
||||
|
||||
const telegramData: TelegramAuthData = {
|
||||
id: data.result.id,
|
||||
first_name: data.result.first_name,
|
||||
last_name: data.result.last_name,
|
||||
username: data.result.username,
|
||||
photo_url: data.result.photo_url,
|
||||
auth_date: data.result.auth_date,
|
||||
hash: data.result.hash,
|
||||
};
|
||||
|
||||
try {
|
||||
// Send Telegram auth data to backend to verify and exchange for JWT
|
||||
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'telegram',
|
||||
telegramUser: telegramData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!webCompleteResponse.ok) {
|
||||
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to complete authentication');
|
||||
}
|
||||
|
||||
const { data: completeData } = await webCompleteResponse.json();
|
||||
const { loginToken } = completeData || {};
|
||||
|
||||
if (!loginToken) {
|
||||
throw new Error('No login token received from server');
|
||||
}
|
||||
|
||||
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: loginToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!exchangeResponse.ok) {
|
||||
const errorData = await exchangeResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to exchange token');
|
||||
}
|
||||
|
||||
const exchangeData = await exchangeResponse.json();
|
||||
const { sessionToken } = exchangeData.data || {};
|
||||
|
||||
if (!sessionToken) {
|
||||
throw new Error('No JWT token received from server');
|
||||
}
|
||||
|
||||
// Store JWT token in store (this is the JWT from the backend)
|
||||
dispatch(setToken(sessionToken));
|
||||
|
||||
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
navigate('/onboarding/step1');
|
||||
}
|
||||
setIsAuthenticating(false);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to complete Telegram authentication:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Fallback: Try to extract auth data from popup URL if postMessage doesn't work
|
||||
// This will only work if the popup redirects to our origin (unlikely but possible)
|
||||
const checkPopupUrl = setInterval(async () => {
|
||||
if (authCompleted || popup.closed) {
|
||||
clearInterval(checkPopupUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to read popup location (will throw if cross-origin)
|
||||
const popupUrl = popup.location.href;
|
||||
|
||||
// If popup is on our callback URL, extract data and get JWT from backend
|
||||
if (popupUrl.startsWith(callbackUrl.split('?')[0])) {
|
||||
const url = new URL(popupUrl);
|
||||
const telegramData: TelegramAuthData = {
|
||||
id: parseInt(url.searchParams.get('id') || '0', 10),
|
||||
first_name: url.searchParams.get('first_name') || undefined,
|
||||
last_name: url.searchParams.get('last_name') || undefined,
|
||||
username: url.searchParams.get('username') || undefined,
|
||||
photo_url: url.searchParams.get('photo_url') || undefined,
|
||||
auth_date: parseInt(url.searchParams.get('auth_date') || '0', 10),
|
||||
hash: url.searchParams.get('hash') || '',
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (telegramData.id && telegramData.auth_date && telegramData.hash) {
|
||||
authCompleted = true;
|
||||
clearInterval(checkPopupUrl);
|
||||
popup.close();
|
||||
window.removeEventListener('message', messageHandler);
|
||||
|
||||
try {
|
||||
// Send Telegram auth data to backend to get JWT token
|
||||
// Use web-complete to verify and then exchange for JWT
|
||||
const webCompleteResponse = await fetch(`${BACKEND_URL}/auth/web-complete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
method: 'telegram',
|
||||
telegramUser: telegramData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!webCompleteResponse.ok) {
|
||||
const errorData = await webCompleteResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to complete authentication');
|
||||
}
|
||||
|
||||
const { data } = await webCompleteResponse.json();
|
||||
const { loginToken } = data;
|
||||
|
||||
if (!loginToken) {
|
||||
throw new Error('No login token received from server');
|
||||
}
|
||||
|
||||
// Exchange handoff token for JWT session token
|
||||
const exchangeResponse = await fetch(`${BACKEND_URL}/auth/desktop-exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: loginToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!exchangeResponse.ok) {
|
||||
const errorData = await exchangeResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to exchange token');
|
||||
}
|
||||
|
||||
const exchangeData = await exchangeResponse.json();
|
||||
const { sessionToken } = exchangeData.data;
|
||||
|
||||
if (!sessionToken) {
|
||||
throw new Error('No JWT token received from server');
|
||||
}
|
||||
|
||||
// Store JWT token in store (this is the JWT from the backend)
|
||||
dispatch(setToken(sessionToken));
|
||||
|
||||
// Call onSuccess callback if provided, otherwise navigate to onboarding
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
navigate('/onboarding/step1');
|
||||
}
|
||||
setIsAuthenticating(false);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to complete authentication:', error);
|
||||
// alert(error instanceof Error ? error.message : 'Authentication failed. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin or other error - this is expected, continue polling
|
||||
// The postMessage handler will catch the auth completion
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Monitor popup closure
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed && !authCompleted) {
|
||||
clearInterval(checkClosed);
|
||||
clearInterval(checkPopupUrl);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Timeout after 5 minutes
|
||||
setTimeout(() => {
|
||||
if (!authCompleted) {
|
||||
if (!popup.closed) {
|
||||
popup.close();
|
||||
}
|
||||
clearInterval(checkClosed);
|
||||
clearInterval(checkPopupUrl);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
setIsAuthenticating(false);
|
||||
// alert('Authentication timed out. Please try again.');
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
} catch (error) {
|
||||
setIsAuthenticating(false);
|
||||
console.error('Failed to start Telegram authentication:', error);
|
||||
// alert(error instanceof Error ? error.message : 'Failed to start authentication. Please try again.');
|
||||
}
|
||||
const handleTelegramLogin = () => {
|
||||
if (externalDisabled) return;
|
||||
openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}?start=login`);
|
||||
};
|
||||
|
||||
const isDisabled = isAuthenticating || externalDisabled;
|
||||
const isDisabled = externalDisabled;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -305,7 +29,7 @@ const TelegramLoginButton = ({
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
<span>{isAuthenticating ? 'Authenticating...' : text}</span>
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface TypewriterGreetingProps {
|
||||
greetings: string[];
|
||||
@@ -13,10 +13,10 @@ const TypewriterGreeting = ({
|
||||
typingSpeed = 100,
|
||||
deletingSpeed = 50,
|
||||
pauseTime = 2000,
|
||||
className = '',
|
||||
className = "",
|
||||
}: TypewriterGreetingProps) => {
|
||||
const [currentGreetingIndex, setCurrentGreetingIndex] = useState(0);
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
@@ -56,7 +56,16 @@ const TypewriterGreeting = ({
|
||||
}, speed);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [displayedText, isDeleting, isPaused, currentGreetingIndex, greetings, typingSpeed, deletingSpeed, pauseTime]);
|
||||
}, [
|
||||
displayedText,
|
||||
isDeleting,
|
||||
isPaused,
|
||||
currentGreetingIndex,
|
||||
greetings,
|
||||
typingSpeed,
|
||||
deletingSpeed,
|
||||
pauseTime,
|
||||
]);
|
||||
|
||||
return (
|
||||
<h1 className={`text-2xl font-bold mb-4 ${className}`}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAppDispatch } from '../../store/hooks';
|
||||
import { clearToken } from '../../store/authSlice';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
import SettingsHeader from './components/SettingsHeader';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useAppDispatch } from "../../store/hooks";
|
||||
import { clearToken } from "../../store/authSlice";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "./components/SettingsHeader";
|
||||
import SettingsMenuItem from "./components/SettingsMenuItem";
|
||||
|
||||
const SettingsHome = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -15,129 +15,224 @@ const SettingsHome = () => {
|
||||
|
||||
const handleViewEncryptionKey = () => {
|
||||
// TODO: Show encryption key in a secure modal
|
||||
console.log('View encryption key');
|
||||
console.log("View encryption key");
|
||||
};
|
||||
|
||||
const handleDeleteAllData = () => {
|
||||
// TODO: Show confirmation dialog and delete all data
|
||||
console.log('Delete all data');
|
||||
console.log("Delete all data");
|
||||
};
|
||||
|
||||
// Main settings menu items
|
||||
const mainMenuItems = [
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Connections',
|
||||
description: 'Manage your connected accounts and services',
|
||||
id: "connections",
|
||||
title: "Connections",
|
||||
description: "Manage your connected accounts and services",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('connections'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("connections"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'messaging',
|
||||
title: 'Messaging',
|
||||
description: 'Configure messaging preferences and templates',
|
||||
id: "messaging",
|
||||
title: "Messaging",
|
||||
description: "Configure messaging preferences and templates",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('messaging'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("messaging"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'privacy',
|
||||
title: 'Privacy & Security',
|
||||
description: 'Control your privacy and security settings',
|
||||
id: "privacy",
|
||||
title: "Privacy & Security",
|
||||
description: "Control your privacy and security settings",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('privacy'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("privacy"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Profile',
|
||||
description: 'Update your profile information and preferences',
|
||||
id: "profile",
|
||||
title: "Profile",
|
||||
description: "Update your profile information and preferences",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('profile'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("profile"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Advanced',
|
||||
description: 'Advanced configuration and developer options',
|
||||
id: "advanced",
|
||||
title: "Advanced",
|
||||
description: "Advanced configuration and developer options",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('advanced'),
|
||||
dangerous: false
|
||||
onClick: () => navigateToSettings("advanced"),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'encryption',
|
||||
title: 'View Encryption Key',
|
||||
description: 'Access your encryption key for backup purposes',
|
||||
id: "encryption",
|
||||
title: "View Encryption Key",
|
||||
description: "Access your encryption key for backup purposes",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1721 9z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1721 9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleViewEncryptionKey,
|
||||
dangerous: false
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
title: 'Billing',
|
||||
description: 'Manage your subscription and payment methods',
|
||||
id: "billing",
|
||||
title: "Billing",
|
||||
description: "Manage your subscription and payment methods",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('billing'),
|
||||
dangerous: false
|
||||
}
|
||||
onClick: () => navigateToSettings("billing"),
|
||||
dangerous: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Destructive actions menu items
|
||||
const destructiveMenuItems = [
|
||||
{
|
||||
id: 'delete',
|
||||
title: 'Delete All Data',
|
||||
description: 'Permanently delete all your data and reset your account',
|
||||
id: "delete",
|
||||
title: "Delete All Data",
|
||||
description: "Permanently delete all your data and reset your account",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleDeleteAllData,
|
||||
dangerous: true
|
||||
dangerous: true,
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
title: 'Log out',
|
||||
description: 'Sign out of your account',
|
||||
id: "logout",
|
||||
title: "Log out",
|
||||
description: "Sign out of your account",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleLogout,
|
||||
dangerous: true
|
||||
}
|
||||
dangerous: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -12,13 +12,13 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
// Handle escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [onClose]);
|
||||
|
||||
// Handle backdrop click
|
||||
@@ -57,9 +57,9 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
ref={modalRef}
|
||||
className="glass rounded-3xl shadow-large w-full max-w-[520px] h-[800px] overflow-hidden animate-slide-right focus:outline-none focus:ring-0"
|
||||
style={{
|
||||
animationDuration: '300ms',
|
||||
animationTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
animationFillMode: 'both'
|
||||
animationDuration: "300ms",
|
||||
animationTimingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -72,4 +72,4 @@ const SettingsLayout = ({ children, onClose }: SettingsLayoutProps) => {
|
||||
return createPortal(modalContent, document.body);
|
||||
};
|
||||
|
||||
export default SettingsLayout;
|
||||
export default SettingsLayout;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import SettingsLayout from './SettingsLayout';
|
||||
import SettingsHome from './SettingsHome';
|
||||
import ConnectionsPanel from './panels/ConnectionsPanel';
|
||||
import MessagingPanel from './panels/MessagingPanel';
|
||||
import PrivacyPanel from './panels/PrivacyPanel';
|
||||
import ProfilePanel from './panels/ProfilePanel';
|
||||
import AdvancedPanel from './panels/AdvancedPanel';
|
||||
import BillingPanel from './panels/BillingPanel';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsLayout from "./SettingsLayout";
|
||||
import SettingsHome from "./SettingsHome";
|
||||
import ConnectionsPanel from "./panels/ConnectionsPanel";
|
||||
import MessagingPanel from "./panels/MessagingPanel";
|
||||
import PrivacyPanel from "./panels/PrivacyPanel";
|
||||
import ProfilePanel from "./panels/ProfilePanel";
|
||||
import AdvancedPanel from "./panels/AdvancedPanel";
|
||||
import BillingPanel from "./panels/BillingPanel";
|
||||
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";
|
||||
|
||||
const SettingsModal = () => {
|
||||
const location = useLocation();
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
// Only render modal when on settings routes
|
||||
const isSettingsRoute = location.pathname.startsWith('/settings');
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
|
||||
if (!isSettingsRoute) {
|
||||
return null;
|
||||
@@ -36,4 +36,4 @@ const SettingsModal = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
export default SettingsModal;
|
||||
|
||||
@@ -6,8 +6,8 @@ interface SettingsBackButtonProps {
|
||||
|
||||
const SettingsBackButton = ({
|
||||
onClick,
|
||||
title = 'Settings',
|
||||
className = ''
|
||||
title = "Settings",
|
||||
className = "",
|
||||
}: SettingsBackButtonProps) => {
|
||||
return (
|
||||
<div className={`bg-black/30 border-b border-stone-700 p-6 ${className}`}>
|
||||
@@ -35,4 +35,4 @@ const SettingsBackButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsBackButton;
|
||||
export default SettingsBackButton;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
|
||||
interface SettingsHeaderProps {
|
||||
className?: string;
|
||||
@@ -8,15 +8,17 @@ interface SettingsHeaderProps {
|
||||
}
|
||||
|
||||
const SettingsHeader = ({
|
||||
className = '',
|
||||
title = 'Settings',
|
||||
className = "",
|
||||
title = "Settings",
|
||||
showBackButton = false,
|
||||
onBack
|
||||
onBack,
|
||||
}: SettingsHeaderProps) => {
|
||||
const { closeSettings } = useSettingsNavigation();
|
||||
|
||||
return (
|
||||
<div className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}>
|
||||
<div
|
||||
className={`bg-black/30 border-b border-stone-700 p-6 relative ${className}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{/* Back button */}
|
||||
@@ -26,14 +28,27 @@ const SettingsHeader = ({
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors mr-3"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-lg font-semibold text-white" id="settings-modal-title">
|
||||
<h2
|
||||
className="text-lg font-semibold text-white"
|
||||
id="settings-modal-title"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -44,8 +59,18 @@ const SettingsHeader = ({
|
||||
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-stone-800/50 transition-colors"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<svg className="w-5 h-5 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<svg
|
||||
className="w-5 h-5 opacity-70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,4 +78,4 @@ const SettingsHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsHeader;
|
||||
export default SettingsHeader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface SettingsMenuItemProps {
|
||||
icon: ReactNode;
|
||||
@@ -17,16 +17,20 @@ const SettingsMenuItem = ({
|
||||
onClick,
|
||||
dangerous = false,
|
||||
isFirst = false,
|
||||
isLast = false
|
||||
isLast = false,
|
||||
}: SettingsMenuItemProps) => {
|
||||
// Color variations for dangerous items (like logout/delete)
|
||||
const titleColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const iconColor = dangerous ? 'text-amber-400' : 'text-white';
|
||||
const borderColor = 'border-stone-700'; // Use consistent border color for all items
|
||||
const titleColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const iconColor = dangerous ? "text-amber-400" : "text-white";
|
||||
const borderColor = "border-stone-700"; // Use consistent border color for all items
|
||||
|
||||
// Border classes for first/last items
|
||||
const borderClasses = isLast ? '' : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst ? 'first:rounded-t-3xl' : (isLast ? 'last:rounded-b-3xl' : '');
|
||||
const borderClasses = isLast ? "" : `border-b ${borderColor}`;
|
||||
const roundedClasses = isFirst
|
||||
? "first:rounded-t-3xl"
|
||||
: isLast
|
||||
? "last:rounded-b-3xl"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -37,16 +41,9 @@ const SettingsMenuItem = ({
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>
|
||||
{title}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="opacity-70 text-xs">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className={`font-medium text-sm mb-1 ${titleColor}`}>{title}</div>
|
||||
{description && <p className="opacity-70 text-xs">{description}</p>}
|
||||
</div>
|
||||
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import SettingsBackButton from './SettingsBackButton';
|
||||
import { ReactNode } from "react";
|
||||
import SettingsBackButton from "./SettingsBackButton";
|
||||
|
||||
interface SettingsPanelLayoutProps {
|
||||
title: string;
|
||||
@@ -12,16 +12,16 @@ const SettingsPanelLayout = ({
|
||||
title,
|
||||
onBack,
|
||||
children,
|
||||
className = ''
|
||||
className = "",
|
||||
}: SettingsPanelLayoutProps) => {
|
||||
return (
|
||||
<div className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}>
|
||||
<div
|
||||
className={`glass rounded-3xl overflow-hidden h-[600px] flex flex-col ${className}`}
|
||||
>
|
||||
<SettingsBackButton onClick={onBack} title={title} />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPanelLayout;
|
||||
export default SettingsPanelLayout;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export type AnimationState = 'entering' | 'entered' | 'exiting' | 'exited';
|
||||
export type AnimationState = "entering" | "entered" | "exiting" | "exited";
|
||||
|
||||
interface SettingsAnimationHook {
|
||||
isVisible: boolean;
|
||||
@@ -9,24 +9,24 @@ interface SettingsAnimationHook {
|
||||
startExit: () => void;
|
||||
}
|
||||
|
||||
export const useSettingsAnimation = (
|
||||
duration = 300
|
||||
): SettingsAnimationHook => {
|
||||
const [animationState, setAnimationState] = useState<AnimationState>('exited');
|
||||
export const useSettingsAnimation = (duration = 300): SettingsAnimationHook => {
|
||||
const [animationState, setAnimationState] =
|
||||
useState<AnimationState>("exited");
|
||||
|
||||
const isVisible = animationState === 'entering' || animationState === 'entered';
|
||||
const isVisible =
|
||||
animationState === "entering" || animationState === "entered";
|
||||
|
||||
const startEntry = () => {
|
||||
setAnimationState('entering');
|
||||
setAnimationState("entering");
|
||||
setTimeout(() => {
|
||||
setAnimationState('entered');
|
||||
setAnimationState("entered");
|
||||
}, duration);
|
||||
};
|
||||
|
||||
const startExit = () => {
|
||||
setAnimationState('exiting');
|
||||
setAnimationState("exiting");
|
||||
setTimeout(() => {
|
||||
setAnimationState('exited');
|
||||
setAnimationState("exited");
|
||||
}, duration);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useSettingsAnimation = (
|
||||
isVisible,
|
||||
animationState,
|
||||
startEntry,
|
||||
startExit
|
||||
startExit,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,7 +52,8 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
}, [isActive, duration]);
|
||||
|
||||
const getPanelClasses = () => {
|
||||
const baseClasses = 'transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]';
|
||||
const baseClasses =
|
||||
"transition-all duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]";
|
||||
if (!mounted) return `${baseClasses} opacity-0`;
|
||||
|
||||
return isActive
|
||||
@@ -62,6 +63,6 @@ export const usePanelAnimation = (isActive: boolean, duration = 300) => {
|
||||
|
||||
return {
|
||||
mounted,
|
||||
panelClasses: getPanelClasses()
|
||||
panelClasses: getPanelClasses(),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export type SettingsRoute = 'home' | 'connections' | 'messaging' | 'privacy' | 'profile' | 'advanced' | 'billing';
|
||||
export type SettingsRoute =
|
||||
| "home"
|
||||
| "connections"
|
||||
| "messaging"
|
||||
| "privacy"
|
||||
| "profile"
|
||||
| "advanced"
|
||||
| "billing";
|
||||
|
||||
interface SettingsNavigationHook {
|
||||
currentRoute: SettingsRoute;
|
||||
@@ -17,41 +24,44 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
// Determine current settings route from URL
|
||||
const getCurrentRoute = (): SettingsRoute => {
|
||||
const path = location.pathname;
|
||||
if (path.includes('/settings/connections')) return 'connections';
|
||||
if (path.includes('/settings/messaging')) return 'messaging';
|
||||
if (path.includes('/settings/privacy')) return 'privacy';
|
||||
if (path.includes('/settings/profile')) return 'profile';
|
||||
if (path.includes('/settings/advanced')) return 'advanced';
|
||||
if (path.includes('/settings/billing')) return 'billing';
|
||||
return 'home';
|
||||
if (path.includes("/settings/connections")) return "connections";
|
||||
if (path.includes("/settings/messaging")) return "messaging";
|
||||
if (path.includes("/settings/privacy")) return "privacy";
|
||||
if (path.includes("/settings/profile")) return "profile";
|
||||
if (path.includes("/settings/advanced")) return "advanced";
|
||||
if (path.includes("/settings/billing")) return "billing";
|
||||
return "home";
|
||||
};
|
||||
|
||||
const currentRoute = getCurrentRoute();
|
||||
|
||||
const navigateToSettings = useCallback((route: SettingsRoute = 'home') => {
|
||||
if (route === 'home') {
|
||||
navigate('/settings');
|
||||
} else {
|
||||
navigate(`/settings/${route}`);
|
||||
}
|
||||
}, [navigate]);
|
||||
const navigateToSettings = useCallback(
|
||||
(route: SettingsRoute = "home") => {
|
||||
if (route === "home") {
|
||||
navigate("/settings");
|
||||
} else {
|
||||
navigate(`/settings/${route}`);
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
if (currentRoute === 'home') {
|
||||
navigate('/home');
|
||||
if (currentRoute === "home") {
|
||||
navigate("/home");
|
||||
} else {
|
||||
navigate('/settings');
|
||||
navigate("/settings");
|
||||
}
|
||||
}, [navigate, currentRoute]);
|
||||
|
||||
const closeSettings = useCallback(() => {
|
||||
navigate('/home');
|
||||
navigate("/home");
|
||||
}, [navigate]);
|
||||
|
||||
return {
|
||||
currentRoute,
|
||||
navigateToSettings,
|
||||
navigateBack,
|
||||
closeSettings
|
||||
closeSettings,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const AdvancedPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,14 +16,32 @@ const AdvancedPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Advanced Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Advanced Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Configure advanced features, developer options, and system-level settings.
|
||||
Configure advanced features, developer options, and system-level
|
||||
settings.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -37,4 +55,4 @@ const AdvancedPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedPanel;
|
||||
export default AdvancedPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const BillingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,11 +16,23 @@ const BillingPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Billing & Subscription</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Billing & Subscription
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Manage your subscription, payment methods, and billing history.
|
||||
</p>
|
||||
@@ -36,4 +48,4 @@ const BillingPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BillingPanel;
|
||||
export default BillingPanel;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { selectIsAuthenticated } from '../../../store/telegramSelectors';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import TelegramConnectionModal from '../../TelegramConnectionModal';
|
||||
import { useState } from "react";
|
||||
import { useAppSelector } from "../../../store/hooks";
|
||||
import {
|
||||
selectIsAuthenticated,
|
||||
selectSessionString,
|
||||
} from "../../../store/telegramSelectors";
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
import TelegramConnectionModal from "../../TelegramConnectionModal";
|
||||
|
||||
import BinanceIcon from '../../../assets/icons/binance.svg';
|
||||
import NotionIcon from '../../../assets/icons/notion.svg';
|
||||
import TelegramIcon from '../../../assets/icons/telegram.svg';
|
||||
import MetamaskIcon from '../../../assets/icons/metamask.svg';
|
||||
import GoogleIcon from '../../../assets/icons/GoogleIcon';
|
||||
import BinanceIcon from "../../../assets/icons/binance.svg";
|
||||
import NotionIcon from "../../../assets/icons/notion.svg";
|
||||
import TelegramIcon from "../../../assets/icons/telegram.svg";
|
||||
import MetamaskIcon from "../../../assets/icons/metamask.svg";
|
||||
import GoogleIcon from "../../../assets/icons/GoogleIcon";
|
||||
|
||||
interface ConnectOption {
|
||||
id: string;
|
||||
@@ -19,61 +22,48 @@ interface ConnectOption {
|
||||
comingSoon?: boolean;
|
||||
}
|
||||
|
||||
// Reused from ConnectStep.tsx - helper to check saved session
|
||||
const hasSavedSession = (): boolean => {
|
||||
try {
|
||||
return !!localStorage.getItem('telegram_session');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ConnectionsPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const [isTelegramModalOpen, setIsTelegramModalOpen] = useState(false);
|
||||
|
||||
// Redux state
|
||||
const isTelegramAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||
const sessionString = useAppSelector((state) => state.telegram.sessionString);
|
||||
const sessionString = useAppSelector(selectSessionString);
|
||||
|
||||
// Check if Telegram account is connected (authenticated or has saved session)
|
||||
const isTelegramConnected = useMemo(() => {
|
||||
return isTelegramAuthenticated || !!sessionString || hasSavedSession();
|
||||
}, [isTelegramAuthenticated, sessionString]);
|
||||
const isTelegramConnected = !!sessionString && isTelegramAuthenticated;
|
||||
|
||||
// Connection options - reused from ConnectStep.tsx
|
||||
const connectOptions: ConnectOption[] = [
|
||||
{
|
||||
id: 'telegram',
|
||||
name: 'Telegram',
|
||||
description: 'Organize chats, automate messages and get insights.',
|
||||
id: "telegram",
|
||||
name: "Telegram",
|
||||
description: "Organize chats, automate messages and get insights.",
|
||||
icon: <img src={TelegramIcon} alt="Telegram" className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
description: 'Manage emails, contacts and calendar events',
|
||||
id: "google",
|
||||
name: "Google",
|
||||
description: "Manage emails, contacts and calendar events",
|
||||
icon: <GoogleIcon />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'notion',
|
||||
name: 'Notion',
|
||||
description: 'Manage tasks, documents and everything else in your Notion',
|
||||
id: "notion",
|
||||
name: "Notion",
|
||||
description: "Manage tasks, documents and everything else in your Notion",
|
||||
icon: <img src={NotionIcon} alt="Notion" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
name: 'Web3 Wallet',
|
||||
description: 'Trade the trenches in a safe and secure way.',
|
||||
id: "wallet",
|
||||
name: "Web3 Wallet",
|
||||
description: "Trade the trenches in a safe and secure way.",
|
||||
icon: <img src={MetamaskIcon} alt="Metamask" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
id: 'exchange',
|
||||
name: 'Crypto Trading Exchanges',
|
||||
description: 'Connect and make trades with deep insights.',
|
||||
id: "exchange",
|
||||
name: "Crypto Trading Exchanges",
|
||||
description: "Connect and make trades with deep insights.",
|
||||
icon: <img src={BinanceIcon} alt="Binance" className="w-5 h-5" />,
|
||||
comingSoon: true,
|
||||
},
|
||||
@@ -81,7 +71,7 @@ const ConnectionsPanel = () => {
|
||||
|
||||
// Check if an account is connected
|
||||
const isAccountConnected = (accountId: string): boolean => {
|
||||
if (accountId === 'telegram') {
|
||||
if (accountId === "telegram") {
|
||||
return isTelegramConnected;
|
||||
}
|
||||
// Add other account checks here when implemented
|
||||
@@ -89,17 +79,17 @@ const ConnectionsPanel = () => {
|
||||
};
|
||||
|
||||
const handleConnect = (provider: string) => {
|
||||
if (provider === 'telegram') {
|
||||
if (provider === "telegram") {
|
||||
if (isTelegramConnected) {
|
||||
// TODO: Show disconnect confirmation
|
||||
console.log('Disconnect Telegram');
|
||||
console.log("Disconnect Telegram");
|
||||
} else {
|
||||
setIsTelegramModalOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectOptions.find(opt => opt.id === provider)?.comingSoon) {
|
||||
if (connectOptions.find((opt) => opt.id === provider)?.comingSoon) {
|
||||
console.log(`${provider} coming soon`);
|
||||
return;
|
||||
}
|
||||
@@ -131,15 +121,15 @@ const ConnectionsPanel = () => {
|
||||
key={option.id}
|
||||
onClick={() => handleConnect(option.id)}
|
||||
disabled={option.comingSoon}
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${
|
||||
index === connectOptions.length - 1 ? '' : 'border-b border-stone-700'
|
||||
} hover:bg-stone-800/30 transition-all duration-200 text-left ${
|
||||
index === 0 ? 'first:rounded-t-3xl' : ''
|
||||
} ${
|
||||
index === connectOptions.length - 1 ? 'last:rounded-b-3xl' : ''
|
||||
} focus:outline-none focus:ring-0 focus:border-inherit relative ${
|
||||
option.comingSoon ? 'opacity-60 cursor-not-allowed' : ''
|
||||
}`}
|
||||
className={`w-full flex items-center justify-between p-3 bg-black/50 ${index === connectOptions.length - 1
|
||||
? ""
|
||||
: "border-b border-stone-700"
|
||||
} hover:bg-stone-800/30 transition-all duration-200 text-left ${index === 0 ? "first:rounded-t-3xl" : ""
|
||||
} ${index === connectOptions.length - 1
|
||||
? "last:rounded-b-3xl"
|
||||
: ""
|
||||
} focus:outline-none focus:ring-0 focus:border-inherit relative ${option.comingSoon ? "opacity-60 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Connection status dot - top right corner */}
|
||||
{isConnected && !option.comingSoon && (
|
||||
@@ -153,9 +143,7 @@ const ConnectionsPanel = () => {
|
||||
<div className="font-medium text-sm mb-1 text-white">
|
||||
{option.name}
|
||||
</div>
|
||||
<p className="opacity-70 text-xs">
|
||||
{option.description}
|
||||
</p>
|
||||
<p className="opacity-70 text-xs">{option.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -188,14 +176,27 @@ const ConnectionsPanel = () => {
|
||||
{/* Security notice */}
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-xl">
|
||||
<div className="flex items-start space-x-2">
|
||||
<svg className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-blue-300 text-sm">🔒 Privacy & Security</p>
|
||||
<p className="font-medium text-blue-300 text-sm">
|
||||
🔒 Privacy & Security
|
||||
</p>
|
||||
<p className="text-blue-200 text-xs mt-1">
|
||||
All data and credentials are stored locally with zero-data retention policy.
|
||||
Your information is encrypted and never shared with third parties.
|
||||
All data and credentials are stored locally with zero-data
|
||||
retention policy. Your information is encrypted and never
|
||||
shared with third parties.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const MessagingPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,13 +16,26 @@ const MessagingPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Messaging Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Messaging Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Configure your messaging preferences, notifications, and communication settings.
|
||||
Configure your messaging preferences, notifications, and
|
||||
communication settings.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -36,4 +49,4 @@ const MessagingPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MessagingPanel;
|
||||
export default MessagingPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const PrivacyPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,13 +16,26 @@ const PrivacyPanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Privacy & Security</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Privacy & Security
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Manage your privacy settings, data retention policies, and security preferences.
|
||||
Manage your privacy settings, data retention policies, and
|
||||
security preferences.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<span className="px-4 py-2 text-sm font-medium rounded-full border bg-stone-700/30 text-stone-300 border-stone-600/50">
|
||||
@@ -36,4 +49,4 @@ const PrivacyPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPanel;
|
||||
export default PrivacyPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from "../hooks/useSettingsNavigation";
|
||||
import SettingsHeader from "../components/SettingsHeader";
|
||||
|
||||
const ProfilePanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
@@ -16,11 +16,23 @@ const ProfilePanel = () => {
|
||||
<div className="p-4 h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-stone-700/50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-stone-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<svg
|
||||
className="w-8 h-8 text-stone-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-white mb-2">Profile Settings</h3>
|
||||
<h3 className="text-lg font-medium text-white mb-2">
|
||||
Profile Settings
|
||||
</h3>
|
||||
<p className="text-stone-400 text-sm max-w-sm mx-auto">
|
||||
Update your profile information, avatar, and personal preferences.
|
||||
</p>
|
||||
@@ -36,4 +48,4 @@ const ProfilePanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePanel;
|
||||
export default ProfilePanel;
|
||||
|
||||
+22
-22
@@ -1,24 +1,24 @@
|
||||
import { Country } from '../types/onboarding';
|
||||
import { Country } from "../types/onboarding";
|
||||
|
||||
export const countries: Country[] = [
|
||||
{ code: 'US', name: 'United States', flag: '🇺🇸', dialCode: '+1' },
|
||||
{ code: 'CA', name: 'Canada', flag: '🇨🇦', dialCode: '+1' },
|
||||
{ code: 'GB', name: 'United Kingdom', flag: '🇬🇧', dialCode: '+44' },
|
||||
{ code: 'DE', name: 'Germany', flag: '🇩🇪', dialCode: '+49' },
|
||||
{ code: 'FR', name: 'France', flag: '🇫🇷', dialCode: '+33' },
|
||||
{ code: 'JP', name: 'Japan', flag: '🇯🇵', dialCode: '+81' },
|
||||
{ code: 'KR', name: 'South Korea', flag: '🇰🇷', dialCode: '+82' },
|
||||
{ code: 'CN', name: 'China', flag: '🇨🇳', dialCode: '+86' },
|
||||
{ code: 'IN', name: 'India', flag: '🇮🇳', dialCode: '+91' },
|
||||
{ code: 'AU', name: 'Australia', flag: '🇦🇺', dialCode: '+61' },
|
||||
{ code: 'BR', name: 'Brazil', flag: '🇧🇷', dialCode: '+55' },
|
||||
{ code: 'MX', name: 'Mexico', flag: '🇲🇽', dialCode: '+52' },
|
||||
{ code: 'AR', name: 'Argentina', flag: '🇦🇷', dialCode: '+54' },
|
||||
{ code: 'CL', name: 'Chile', flag: '🇨🇱', dialCode: '+56' },
|
||||
{ code: 'SG', name: 'Singapore', flag: '🇸🇬', dialCode: '+65' },
|
||||
{ code: 'HK', name: 'Hong Kong', flag: '🇭🇰', dialCode: '+852' },
|
||||
{ code: 'CH', name: 'Switzerland', flag: '🇨🇭', dialCode: '+41' },
|
||||
{ code: 'NL', name: 'Netherlands', flag: '🇳🇱', dialCode: '+31' },
|
||||
{ code: 'SE', name: 'Sweden', flag: '🇸🇪', dialCode: '+46' },
|
||||
{ code: 'NO', name: 'Norway', flag: '🇳🇴', dialCode: '+47' },
|
||||
];
|
||||
{ code: "US", name: "United States", flag: "🇺🇸", dialCode: "+1" },
|
||||
{ code: "CA", name: "Canada", flag: "🇨🇦", dialCode: "+1" },
|
||||
{ code: "GB", name: "United Kingdom", flag: "🇬🇧", dialCode: "+44" },
|
||||
{ code: "DE", name: "Germany", flag: "🇩🇪", dialCode: "+49" },
|
||||
{ code: "FR", name: "France", flag: "🇫🇷", dialCode: "+33" },
|
||||
{ code: "JP", name: "Japan", flag: "🇯🇵", dialCode: "+81" },
|
||||
{ code: "KR", name: "South Korea", flag: "🇰🇷", dialCode: "+82" },
|
||||
{ code: "CN", name: "China", flag: "🇨🇳", dialCode: "+86" },
|
||||
{ code: "IN", name: "India", flag: "🇮🇳", dialCode: "+91" },
|
||||
{ code: "AU", name: "Australia", flag: "🇦🇺", dialCode: "+61" },
|
||||
{ code: "BR", name: "Brazil", flag: "🇧🇷", dialCode: "+55" },
|
||||
{ code: "MX", name: "Mexico", flag: "🇲🇽", dialCode: "+52" },
|
||||
{ code: "AR", name: "Argentina", flag: "🇦🇷", dialCode: "+54" },
|
||||
{ code: "CL", name: "Chile", flag: "🇨🇱", dialCode: "+56" },
|
||||
{ code: "SG", name: "Singapore", flag: "🇸🇬", dialCode: "+65" },
|
||||
{ code: "HK", name: "Hong Kong", flag: "🇭🇰", dialCode: "+852" },
|
||||
{ code: "CH", name: "Switzerland", flag: "🇨🇭", dialCode: "+41" },
|
||||
{ code: "NL", name: "Netherlands", flag: "🇳🇱", dialCode: "+31" },
|
||||
{ code: "SE", name: "Sweden", flag: "🇸🇪", dialCode: "+46" },
|
||||
{ code: "NO", name: "Norway", flag: "🇳🇴", dialCode: "+47" },
|
||||
];
|
||||
|
||||
+18
-12
@@ -1,22 +1,23 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { socketService } from '../services/socketService';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { useEffect, useRef } from "react";
|
||||
import { socketService } from "../services/socketService";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
import { selectSocketStatus } from "../store/socketSelectors";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
/**
|
||||
* React hook for using the Socket.IO connection
|
||||
* Note: The socket connection is managed by SocketProvider based on JWT token.
|
||||
* This hook provides access to the socket instance and methods.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { socket, isConnected, emit, on, off } = useSocket();
|
||||
*
|
||||
*
|
||||
* useEffect(() => {
|
||||
* on('ready', () => {
|
||||
* console.log('Socket ready!');
|
||||
* });
|
||||
*
|
||||
*
|
||||
* return () => {
|
||||
* off('ready');
|
||||
* };
|
||||
@@ -24,8 +25,10 @@ import type { Socket } from 'socket.io-client';
|
||||
* ```
|
||||
*/
|
||||
export const useSocket = () => {
|
||||
const listenersRef = useRef<Array<{ event: string; callback: (...args: unknown[]) => void }>>([]);
|
||||
const socketStatus = useAppSelector((state) => state.socket.status);
|
||||
const listenersRef = useRef<
|
||||
Array<{ event: string; callback: (...args: unknown[]) => void }>
|
||||
>([]);
|
||||
const socketStatus = useAppSelector(selectSocketStatus);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -50,10 +53,13 @@ export const useSocket = () => {
|
||||
socketService.off(event, callback);
|
||||
if (callback) {
|
||||
listenersRef.current = listenersRef.current.filter(
|
||||
(listener) => listener.event !== event || listener.callback !== callback
|
||||
(listener) =>
|
||||
listener.event !== event || listener.callback !== callback,
|
||||
);
|
||||
} else {
|
||||
listenersRef.current = listenersRef.current.filter((listener) => listener.event !== event);
|
||||
listenersRef.current = listenersRef.current.filter(
|
||||
(listener) => listener.event !== event,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +69,7 @@ export const useSocket = () => {
|
||||
|
||||
return {
|
||||
socket: socketService.getSocket() as Socket | null,
|
||||
isConnected: socketStatus === 'connected',
|
||||
isConnected: socketStatus === "connected",
|
||||
status: socketStatus,
|
||||
emit,
|
||||
on,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppSelector, useAppDispatch } from '../store/hooks';
|
||||
import { fetchCurrentUser } from '../store/userSlice';
|
||||
import { useEffect } from "react";
|
||||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import { fetchCurrentUser } from "../store/userSlice";
|
||||
|
||||
/**
|
||||
* Hook to access user data and automatically fetch it when token is available
|
||||
|
||||
+29
-29
@@ -2,42 +2,43 @@
|
||||
* Error handling utilities for MCP server
|
||||
*/
|
||||
|
||||
import type { MCPToolResult } from './types';
|
||||
import { ValidationError } from './validation';
|
||||
import type { MCPToolResult } from "./types";
|
||||
import { ValidationError } from "./validation";
|
||||
|
||||
export enum ErrorCategory {
|
||||
CHAT = 'CHAT',
|
||||
MSG = 'MSG',
|
||||
CONTACT = 'CONTACT',
|
||||
GROUP = 'GROUP',
|
||||
MEDIA = 'MEDIA',
|
||||
PROFILE = 'PROFILE',
|
||||
AUTH = 'AUTH',
|
||||
ADMIN = 'ADMIN',
|
||||
VALIDATION = 'VALIDATION',
|
||||
SEARCH = 'SEARCH',
|
||||
DRAFT = 'DRAFT',
|
||||
CHAT = "CHAT",
|
||||
MSG = "MSG",
|
||||
CONTACT = "CONTACT",
|
||||
GROUP = "GROUP",
|
||||
MEDIA = "MEDIA",
|
||||
PROFILE = "PROFILE",
|
||||
AUTH = "AUTH",
|
||||
ADMIN = "ADMIN",
|
||||
VALIDATION = "VALIDATION",
|
||||
SEARCH = "SEARCH",
|
||||
DRAFT = "DRAFT",
|
||||
}
|
||||
|
||||
function generateErrorCode(
|
||||
functionName: string,
|
||||
category?: ErrorCategory | string,
|
||||
): string {
|
||||
if (category === 'VALIDATION-001' || category === ErrorCategory.VALIDATION) {
|
||||
return 'VALIDATION-001';
|
||||
if (category === "VALIDATION-001" || category === ErrorCategory.VALIDATION) {
|
||||
return "VALIDATION-001";
|
||||
}
|
||||
|
||||
const prefix = category
|
||||
? typeof category === 'string' && category.startsWith('VALIDATION')
|
||||
? typeof category === "string" && category.startsWith("VALIDATION")
|
||||
? category
|
||||
: category
|
||||
: 'GEN';
|
||||
: "GEN";
|
||||
|
||||
const hash = Math.abs(
|
||||
functionName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0),
|
||||
) % 1000;
|
||||
const hash =
|
||||
Math.abs(
|
||||
functionName.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0),
|
||||
) % 1000;
|
||||
|
||||
return `${prefix}-ERR-${hash.toString().padStart(3, '0')}`;
|
||||
return `${prefix}-ERR-${hash.toString().padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
export function logAndFormatError(
|
||||
@@ -50,8 +51,8 @@ export function logAndFormatError(
|
||||
const contextStr = context
|
||||
? Object.entries(context)
|
||||
.map(([k, v]) => `${k}=${String(v)}`)
|
||||
.join(', ')
|
||||
: '';
|
||||
.join(", ")
|
||||
: "";
|
||||
|
||||
console.error(
|
||||
`[MCP] Error in ${functionName} (${contextStr}) - Code: ${errorCode}`,
|
||||
@@ -64,20 +65,19 @@ export function logAndFormatError(
|
||||
: `An error occurred (code: ${errorCode}). Check logs for details.`;
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: userMessage }],
|
||||
content: [{ type: "text", text: userMessage }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function withErrorHandling<T extends (...args: unknown[]) => Promise<MCPToolResult>>(
|
||||
fn: T,
|
||||
category?: ErrorCategory,
|
||||
): T {
|
||||
export function withErrorHandling<
|
||||
T extends (...args: unknown[]) => Promise<MCPToolResult>,
|
||||
>(fn: T, category?: ErrorCategory): T {
|
||||
return (async (...args: Parameters<T>): Promise<MCPToolResult> => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
const functionName = fn.name || 'unknown';
|
||||
const functionName = fn.name || "unknown";
|
||||
return logAndFormatError(
|
||||
functionName,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Used by MCP servers (e.g. telegram, gmail, etc.)
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './validation';
|
||||
export * from './errorHandler';
|
||||
export * from './logger';
|
||||
export { SocketIOMCPTransportImpl } from './transport';
|
||||
export * from "./types";
|
||||
export * from "./validation";
|
||||
export * from "./errorHandler";
|
||||
export * from "./logger";
|
||||
export { SocketIOMCPTransportImpl } from "./transport";
|
||||
|
||||
+11
-6
@@ -2,23 +2,28 @@
|
||||
* MCP logger - simple console logger with [MCP] prefix
|
||||
*/
|
||||
|
||||
type LogLevel = 'log' | 'warn' | 'error';
|
||||
type LogLevel = "log" | "warn" | "error";
|
||||
|
||||
const PREFIX = '[MCP]';
|
||||
const PREFIX = "[MCP]";
|
||||
|
||||
function log(level: LogLevel, message: string, ...data: unknown[]): void {
|
||||
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
|
||||
const fn =
|
||||
level === "error"
|
||||
? console.error
|
||||
: level === "warn"
|
||||
? console.warn
|
||||
: console.log;
|
||||
fn(PREFIX, message, ...data);
|
||||
}
|
||||
|
||||
export function mcpLog(message: string, ...data: unknown[]): void {
|
||||
log('log', message, ...data);
|
||||
log("log", message, ...data);
|
||||
}
|
||||
|
||||
export function mcpWarn(message: string, ...data: unknown[]): void {
|
||||
log('warn', message, ...data);
|
||||
log("warn", message, ...data);
|
||||
}
|
||||
|
||||
export function mcpError(message: string, ...data: unknown[]): void {
|
||||
log('error', message, ...data);
|
||||
log("error", message, ...data);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
* Main entry point for Telegram MCP integration
|
||||
*/
|
||||
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { TelegramMCPServer } from './server';
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { TelegramMCPServer } from "./server";
|
||||
|
||||
let telegramMCPInstance: TelegramMCPServer | undefined;
|
||||
|
||||
export function initTelegramMCPServer(socket: Socket | null | undefined): TelegramMCPServer {
|
||||
export function initTelegramMCPServer(
|
||||
socket: Socket | null | undefined,
|
||||
): TelegramMCPServer {
|
||||
telegramMCPInstance = new TelegramMCPServer(socket);
|
||||
console.log('[MCP] Telegram MCP server initialized');
|
||||
console.log("[MCP] Telegram MCP server initialized");
|
||||
return telegramMCPInstance;
|
||||
}
|
||||
|
||||
@@ -18,19 +20,21 @@ export function getTelegramMCPServer(): TelegramMCPServer | undefined {
|
||||
return telegramMCPInstance;
|
||||
}
|
||||
|
||||
export function updateTelegramMCPServerSocket(socket: Socket | null | undefined): void {
|
||||
export function updateTelegramMCPServerSocket(
|
||||
socket: Socket | null | undefined,
|
||||
): void {
|
||||
if (telegramMCPInstance) {
|
||||
telegramMCPInstance.updateSocket(socket);
|
||||
console.log('[MCP] Telegram MCP server socket updated');
|
||||
console.log("[MCP] Telegram MCP server socket updated");
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupTelegramMCPServer(): void {
|
||||
if (telegramMCPInstance) {
|
||||
telegramMCPInstance = undefined;
|
||||
console.log('[MCP] Telegram MCP server cleaned up');
|
||||
console.log("[MCP] Telegram MCP server cleaned up");
|
||||
}
|
||||
}
|
||||
|
||||
export { toHumanReadableAction } from './toolActionParser';
|
||||
export type { TelegramMCPServer } from './server';
|
||||
export { toHumanReadableAction } from "./toolActionParser";
|
||||
export type { TelegramMCPServer } from "./server";
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "../types";
|
||||
|
||||
import { store } from "../../../store";
|
||||
import { selectTelegramUserState } from "../../../store/telegramSelectors";
|
||||
import { ErrorCategory, logAndFormatError } from "../errorHandler";
|
||||
import { ValidationError } from "../validation";
|
||||
import { SocketIOMCPTransportImpl } from "../transport";
|
||||
@@ -104,7 +105,8 @@ export class TelegramMCPServer {
|
||||
};
|
||||
}
|
||||
|
||||
const telegramState: TelegramState = store.getState().telegram;
|
||||
const telegramState: TelegramState =
|
||||
selectTelegramUserState(store.getState());
|
||||
|
||||
try {
|
||||
return await toolHandler(args, {
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
* Uses mtprotoService + Redux telegram state (alphahuman)
|
||||
*/
|
||||
|
||||
import { store } from '../../../store';
|
||||
import { mtprotoService } from '../../../services/mtprotoService';
|
||||
import { store } from "../../../store";
|
||||
import { mtprotoService } from "../../../services/mtprotoService";
|
||||
import {
|
||||
selectOrderedChats,
|
||||
selectCurrentUser,
|
||||
} from '../../../store/telegramSelectors';
|
||||
import type { TelegramChat, TelegramUser, TelegramMessage } from '../../../store/telegram/types';
|
||||
selectTelegramUserState,
|
||||
} from "../../../store/telegramSelectors";
|
||||
import type {
|
||||
TelegramChat,
|
||||
TelegramUser,
|
||||
TelegramMessage,
|
||||
} from "../../../store/telegram/types";
|
||||
|
||||
function getTelegramState() {
|
||||
return selectTelegramUserState(store.getState());
|
||||
}
|
||||
|
||||
export interface FormattedEntity {
|
||||
id: string;
|
||||
@@ -28,10 +37,6 @@ export interface FormattedMessage {
|
||||
media_type?: string;
|
||||
}
|
||||
|
||||
function getTelegramState() {
|
||||
return store.getState().telegram;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chat by ID or username
|
||||
*/
|
||||
@@ -42,10 +47,15 @@ export function getChatById(chatId: string | number): TelegramChat | undefined {
|
||||
const chat = state.chats[idStr];
|
||||
if (chat) return chat;
|
||||
|
||||
if (typeof chatId === 'string' && (chatId.startsWith('@') || /^[a-zA-Z0-9_]+$/.test(chatId))) {
|
||||
const username = chatId.startsWith('@') ? chatId : `@${chatId}`;
|
||||
if (
|
||||
typeof chatId === "string" &&
|
||||
(chatId.startsWith("@") || /^[a-zA-Z0-9_]+$/.test(chatId))
|
||||
) {
|
||||
const username = chatId.startsWith("@") ? chatId : `@${chatId}`;
|
||||
return Object.values(state.chats).find(
|
||||
(c) => c.username && (c.username === username || c.username === username.slice(1)),
|
||||
(c) =>
|
||||
c.username &&
|
||||
(c.username === username || c.username === username.slice(1)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +105,7 @@ export async function sendMessage(
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
const entity = chat.username ? `@${chat.username.replace('@', '')}` : chat.id;
|
||||
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
|
||||
|
||||
if (replyToMessageId !== undefined) {
|
||||
const client = mtprotoService.getClient();
|
||||
@@ -129,8 +139,8 @@ export async function searchChats(query: string): Promise<TelegramChat[]> {
|
||||
const ordered = selectOrderedChats(state);
|
||||
const q = query.toLowerCase();
|
||||
return ordered.filter((c) => {
|
||||
const title = (c.title ?? '').toLowerCase();
|
||||
const un = (c.username ?? '').toLowerCase();
|
||||
const title = (c.title ?? "").toLowerCase();
|
||||
const un = (c.username ?? "").toLowerCase();
|
||||
return title.includes(q) || un.includes(q);
|
||||
});
|
||||
}
|
||||
@@ -146,23 +156,31 @@ export function getCurrentUser(): TelegramUser | undefined {
|
||||
/**
|
||||
* Format entity (chat or user) for display
|
||||
*/
|
||||
export function formatEntity(entity: TelegramChat | TelegramUser): FormattedEntity {
|
||||
if ('title' in entity) {
|
||||
export function formatEntity(
|
||||
entity: TelegramChat | TelegramUser,
|
||||
): FormattedEntity {
|
||||
if ("title" in entity) {
|
||||
const chat = entity as TelegramChat;
|
||||
const type = chat.type === 'channel' ? 'channel' : chat.type === 'supergroup' ? 'group' : chat.type;
|
||||
const type =
|
||||
chat.type === "channel"
|
||||
? "channel"
|
||||
: chat.type === "supergroup"
|
||||
? "group"
|
||||
: chat.type;
|
||||
return {
|
||||
id: chat.id,
|
||||
name: chat.title ?? 'Unknown',
|
||||
name: chat.title ?? "Unknown",
|
||||
type,
|
||||
username: chat.username,
|
||||
};
|
||||
}
|
||||
const user = entity as TelegramUser;
|
||||
const name = [user.firstName, user.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
const name =
|
||||
[user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
return {
|
||||
id: user.id,
|
||||
name,
|
||||
type: 'user',
|
||||
type: "user",
|
||||
username: user.username,
|
||||
phone: user.phoneNumber,
|
||||
};
|
||||
@@ -175,7 +193,7 @@ export function formatMessage(message: TelegramMessage): FormattedMessage {
|
||||
const result: FormattedMessage = {
|
||||
id: message.id,
|
||||
date: new Date(message.date * 1000).toISOString(),
|
||||
text: message.message ?? '',
|
||||
text: message.message ?? "",
|
||||
};
|
||||
if (message.fromId) result.from_id = message.fromId;
|
||||
if (message.media?.type) {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
type ToolArguments = Record<string, unknown>;
|
||||
|
||||
function formatId(id: string | number | undefined, prefix = ''): string {
|
||||
if (!id) return '';
|
||||
function formatId(id: string | number | undefined, prefix = ""): string {
|
||||
if (!id) return "";
|
||||
const str = String(id);
|
||||
return prefix ? `${prefix} ${str}` : str;
|
||||
}
|
||||
@@ -15,7 +15,10 @@ function truncateText(text: string, maxLength = 50): string {
|
||||
return `${text.substring(0, maxLength)}...`;
|
||||
}
|
||||
|
||||
export function toHumanReadableAction(toolName: string, args: ToolArguments): string {
|
||||
export function toHumanReadableAction(
|
||||
toolName: string,
|
||||
args: ToolArguments,
|
||||
): string {
|
||||
const parser = toolParsers[toolName];
|
||||
if (parser) return parser(args);
|
||||
return `Executing ${toolName} with provided parameters`;
|
||||
@@ -23,165 +26,165 @@ export function toHumanReadableAction(toolName: string, args: ToolArguments): st
|
||||
|
||||
const toolParsers: Record<string, (args: ToolArguments) => string> = {
|
||||
send_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const message = truncateText((args.message as string) || '', 100);
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const message = truncateText((args.message as string) || "", 100);
|
||||
return `Send message to ${chatId}: "${message}"`;
|
||||
},
|
||||
reply_to_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const text = truncateText((args.text as string) || '', 100);
|
||||
const text = truncateText((args.text as string) || "", 100);
|
||||
return `Reply to message ${messageId} in ${chatId}: "${text}"`;
|
||||
},
|
||||
edit_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const newText = truncateText((args.new_text as string) || '', 100);
|
||||
const newText = truncateText((args.new_text as string) || "", 100);
|
||||
return `Edit message ${messageId} in ${chatId} to: "${newText}"`;
|
||||
},
|
||||
delete_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Delete message ${messageId} from ${chatId}`;
|
||||
},
|
||||
forward_message: (args) => {
|
||||
const fromChat = formatId(args.from_chat_id as string | number, 'chat');
|
||||
const toChat = formatId(args.to_chat_id as string | number, 'chat');
|
||||
const fromChat = formatId(args.from_chat_id as string | number, "chat");
|
||||
const toChat = formatId(args.to_chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Forward message ${messageId} from ${fromChat} to ${toChat}`;
|
||||
},
|
||||
pin_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Pin message ${messageId} in ${chatId}`;
|
||||
},
|
||||
unpin_message: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Unpin message ${messageId} in ${chatId}`;
|
||||
},
|
||||
mark_as_read: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Mark messages as read in ${chatId}`;
|
||||
},
|
||||
send_reaction: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const reaction = (args.reaction as string) || '👍';
|
||||
const reaction = (args.reaction as string) || "👍";
|
||||
return `Add reaction ${reaction} to message ${messageId} in ${chatId}`;
|
||||
},
|
||||
remove_reaction: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const reaction = (args.reaction as string) || '';
|
||||
const reaction = (args.reaction as string) || "";
|
||||
return `Remove reaction ${reaction} from message ${messageId} in ${chatId}`;
|
||||
},
|
||||
save_draft: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const text = truncateText((args.text as string) || '', 50);
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const text = truncateText((args.text as string) || "", 50);
|
||||
return `Save draft in ${chatId}: "${text}"`;
|
||||
},
|
||||
clear_draft: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Clear draft in ${chatId}`;
|
||||
},
|
||||
list_chats: (args) => {
|
||||
const chatType = (args.chat_type as string) || 'all';
|
||||
const chatType = (args.chat_type as string) || "all";
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `List ${limit} ${chatType} chats`;
|
||||
},
|
||||
get_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get information for ${chatId}`;
|
||||
},
|
||||
create_group: (args) => {
|
||||
const title = (args.title as string) || 'Untitled Group';
|
||||
const title = (args.title as string) || "Untitled Group";
|
||||
const userCount = Array.isArray(args.user_ids) ? args.user_ids.length : 0;
|
||||
return `Create group "${title}" with ${userCount} member${userCount !== 1 ? 's' : ''}`;
|
||||
return `Create group "${title}" with ${userCount} member${userCount !== 1 ? "s" : ""}`;
|
||||
},
|
||||
create_channel: (args) => {
|
||||
const title = (args.title as string) || 'Untitled Channel';
|
||||
const title = (args.title as string) || "Untitled Channel";
|
||||
const description = args.description
|
||||
? `: ${truncateText((args.description as string) || '', 50)}`
|
||||
: '';
|
||||
? `: ${truncateText((args.description as string) || "", 50)}`
|
||||
: "";
|
||||
return `Create channel "${title}"${description}`;
|
||||
},
|
||||
edit_chat_title: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const newTitle = (args.new_title as string) || '';
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const newTitle = (args.new_title as string) || "";
|
||||
return `Change title of ${chatId} to "${newTitle}"`;
|
||||
},
|
||||
delete_chat_photo: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Remove photo from ${chatId}`;
|
||||
},
|
||||
edit_chat_photo: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Update photo for ${chatId}`;
|
||||
},
|
||||
leave_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Leave ${chatId}`;
|
||||
},
|
||||
mute_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const muteFor = args.mute_for ? ` for ${args.mute_for} seconds` : '';
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const muteFor = args.mute_for ? ` for ${args.mute_for} seconds` : "";
|
||||
return `Mute ${chatId}${muteFor}`;
|
||||
},
|
||||
unmute_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Unmute ${chatId}`;
|
||||
},
|
||||
archive_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Archive ${chatId}`;
|
||||
},
|
||||
unarchive_chat: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Unarchive ${chatId}`;
|
||||
},
|
||||
invite_to_group: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const userCount = Array.isArray(args.user_ids) ? args.user_ids.length : 0;
|
||||
return `Invite ${userCount} user${userCount !== 1 ? 's' : ''} to ${chatId}`;
|
||||
return `Invite ${userCount} user${userCount !== 1 ? "s" : ""} to ${chatId}`;
|
||||
},
|
||||
ban_user: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Ban ${userId} from ${chatId}`;
|
||||
},
|
||||
unban_user: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Unban ${userId} from ${chatId}`;
|
||||
},
|
||||
promote_admin: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Promote ${userId} to admin in ${chatId}`;
|
||||
},
|
||||
demote_admin: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Demote ${userId} from admin in ${chatId}`;
|
||||
},
|
||||
block_user: (args) => {
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Block ${userId}`;
|
||||
},
|
||||
unblock_user: (args) => {
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Unblock ${userId}`;
|
||||
},
|
||||
add_contact: (args) => {
|
||||
const firstName = (args.first_name as string) || '';
|
||||
const lastName = (args.last_name as string) || '';
|
||||
const phone = (args.phone_number as string) || '';
|
||||
const name = [firstName, lastName].filter(Boolean).join(' ') || phone;
|
||||
const firstName = (args.first_name as string) || "";
|
||||
const lastName = (args.last_name as string) || "";
|
||||
const phone = (args.phone_number as string) || "";
|
||||
const name = [firstName, lastName].filter(Boolean).join(" ") || phone;
|
||||
return `Add contact: ${name}`;
|
||||
},
|
||||
delete_contact: (args) => {
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Delete contact ${userId}`;
|
||||
},
|
||||
list_contacts: (args) => {
|
||||
@@ -189,144 +192,159 @@ const toolParsers: Record<string, (args: ToolArguments) => string> = {
|
||||
return `List ${limit} contacts`;
|
||||
},
|
||||
search_contacts: (args) => {
|
||||
const query = truncateText((args.query as string) || '', 30);
|
||||
const query = truncateText((args.query as string) || "", 30);
|
||||
return `Search contacts for "${query}"`;
|
||||
},
|
||||
search_messages: (args) => {
|
||||
const chatId = args.chat_id ? formatId(args.chat_id as string | number, 'chat') : 'all chats';
|
||||
const query = truncateText((args.query as string) || '', 30);
|
||||
const chatId = args.chat_id
|
||||
? formatId(args.chat_id as string | number, "chat")
|
||||
: "all chats";
|
||||
const query = truncateText((args.query as string) || "", 30);
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `Search for "${query}" in ${chatId} (limit: ${limit})`;
|
||||
},
|
||||
search_public_chats: (args) => {
|
||||
const query = truncateText((args.query as string) || '', 30);
|
||||
const query = truncateText((args.query as string) || "", 30);
|
||||
return `Search public chats for "${query}"`;
|
||||
},
|
||||
resolve_username: (args) => {
|
||||
const username = (args.username as string) || '';
|
||||
return `Resolve username @${username.replace('@', '')}`;
|
||||
const username = (args.username as string) || "";
|
||||
return `Resolve username @${username.replace("@", "")}`;
|
||||
},
|
||||
get_messages: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `Get ${limit} messages from ${chatId}`;
|
||||
},
|
||||
list_messages: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `List ${limit} messages from ${chatId}`;
|
||||
},
|
||||
get_history: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `Get message history from ${chatId} (${limit} messages)`;
|
||||
},
|
||||
get_pinned_messages: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get pinned messages from ${chatId}`;
|
||||
},
|
||||
get_message_context: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const limit = (args.limit as number) || 5;
|
||||
return `Get context around message ${messageId} in ${chatId} (±${limit} messages)`;
|
||||
},
|
||||
list_topics: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `List topics in ${chatId}`;
|
||||
},
|
||||
get_participants: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const limit = (args.limit as number) || 100;
|
||||
return `Get ${limit} participants from ${chatId}`;
|
||||
},
|
||||
get_admins: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get administrators of ${chatId}`;
|
||||
},
|
||||
get_banned_users: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get banned users from ${chatId}`;
|
||||
},
|
||||
get_blocked_users: () => 'Get list of blocked users',
|
||||
get_blocked_users: () => "Get list of blocked users",
|
||||
get_invite_link: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get invite link for ${chatId}`;
|
||||
},
|
||||
export_chat_invite: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Create invite link for ${chatId}`;
|
||||
},
|
||||
import_chat_invite: (args) => {
|
||||
const inviteHash = args.invite_hash ? truncateText((args.invite_hash as string) || '', 20) : '';
|
||||
const inviteHash = args.invite_hash
|
||||
? truncateText((args.invite_hash as string) || "", 20)
|
||||
: "";
|
||||
return `Join chat via invite link: ${inviteHash}`;
|
||||
},
|
||||
join_chat_by_link: (args) => {
|
||||
const inviteLink = args.invite_link ? truncateText((args.invite_link as string) || '', 30) : '';
|
||||
const inviteLink = args.invite_link
|
||||
? truncateText((args.invite_link as string) || "", 30)
|
||||
: "";
|
||||
return `Join chat via link: ${inviteLink}`;
|
||||
},
|
||||
subscribe_public_channel: (args) => {
|
||||
const username = (args.username as string) || '';
|
||||
return `Subscribe to public channel @${username.replace('@', '')}`;
|
||||
const username = (args.username as string) || "";
|
||||
return `Subscribe to public channel @${username.replace("@", "")}`;
|
||||
},
|
||||
update_profile: (args) => {
|
||||
const updates: string[] = [];
|
||||
if (args.first_name) updates.push(`first name: "${args.first_name as string}"`);
|
||||
if (args.last_name) updates.push(`last name: "${args.last_name as string}"`);
|
||||
if (args.bio) updates.push(`bio: "${truncateText((args.bio as string) || '', 30)}"`);
|
||||
return `Update profile (${updates.join(', ')})`;
|
||||
if (args.first_name)
|
||||
updates.push(`first name: "${args.first_name as string}"`);
|
||||
if (args.last_name)
|
||||
updates.push(`last name: "${args.last_name as string}"`);
|
||||
if (args.bio)
|
||||
updates.push(`bio: "${truncateText((args.bio as string) || "", 30)}"`);
|
||||
return `Update profile (${updates.join(", ")})`;
|
||||
},
|
||||
set_profile_photo: () => 'Set profile photo',
|
||||
delete_profile_photo: () => 'Delete profile photo',
|
||||
set_profile_photo: () => "Set profile photo",
|
||||
delete_profile_photo: () => "Delete profile photo",
|
||||
get_user_photos: (args) => {
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `Get ${limit} photos from ${userId}`;
|
||||
},
|
||||
get_user_status: (args) => {
|
||||
const userId = formatId(args.user_id as string | number, 'user');
|
||||
const userId = formatId(args.user_id as string | number, "user");
|
||||
return `Get status of ${userId}`;
|
||||
},
|
||||
get_me: () => 'Get current user information',
|
||||
get_me: () => "Get current user information",
|
||||
list_inline_buttons: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Get inline buttons from message ${messageId} in ${chatId}`;
|
||||
},
|
||||
press_inline_button: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
const buttonText = args.button_text ? ` "${truncateText((args.button_text as string) || '', 30)}"` : '';
|
||||
const buttonText = args.button_text
|
||||
? ` "${truncateText((args.button_text as string) || "", 30)}"`
|
||||
: "";
|
||||
return `Press inline button${buttonText} on message ${messageId} in ${chatId}`;
|
||||
},
|
||||
create_poll: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const question = truncateText((args.question as string) || '', 50);
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const question = truncateText((args.question as string) || "", 50);
|
||||
const optionCount = Array.isArray(args.options) ? args.options.length : 0;
|
||||
return `Create poll in ${chatId}: "${question}" with ${optionCount} options`;
|
||||
},
|
||||
get_bot_info: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
return `Get bot information from ${chatId}`;
|
||||
},
|
||||
set_bot_commands: (args) => {
|
||||
const chatId = args.chat_id ? formatId(args.chat_id as string | number, 'chat') : 'all chats';
|
||||
const commandCount = Array.isArray(args.commands) ? args.commands.length : 0;
|
||||
const chatId = args.chat_id
|
||||
? formatId(args.chat_id as string | number, "chat")
|
||||
: "all chats";
|
||||
const commandCount = Array.isArray(args.commands)
|
||||
? args.commands.length
|
||||
: 0;
|
||||
return `Set ${commandCount} bot commands for ${chatId}`;
|
||||
},
|
||||
get_privacy_settings: () => 'Get privacy settings',
|
||||
get_privacy_settings: () => "Get privacy settings",
|
||||
set_privacy_settings: (args) => {
|
||||
const setting = (args.setting as string) || 'unknown';
|
||||
const value = (args.value as string) || '';
|
||||
const setting = (args.setting as string) || "unknown";
|
||||
const value = (args.value as string) || "";
|
||||
return `Set privacy setting "${setting}" to ${value}`;
|
||||
},
|
||||
get_media_info: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const messageId = args.message_id;
|
||||
return `Get media information from message ${messageId} in ${chatId}`;
|
||||
},
|
||||
get_recent_actions: (args) => {
|
||||
const chatId = formatId(args.chat_id as string | number, 'chat');
|
||||
const chatId = formatId(args.chat_id as string | number, "chat");
|
||||
const limit = (args.limit as number) || 20;
|
||||
return `Get ${limit} recent admin actions from ${chatId}`;
|
||||
},
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import { optString } from '../args';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import { optString } from "../args";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'add_contact',
|
||||
description: 'Add a contact to your Telegram account',
|
||||
name: "add_contact",
|
||||
description: "Add a contact to your Telegram account",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
phone: { type: 'string', description: 'Phone number' },
|
||||
first_name: { type: 'string', description: 'First name' },
|
||||
last_name: { type: 'string', description: 'Last name' },
|
||||
phone: { type: "string", description: "Phone number" },
|
||||
first_name: { type: "string", description: "First name" },
|
||||
last_name: { type: "string", description: "Last name" },
|
||||
},
|
||||
required: ['phone', 'first_name'],
|
||||
required: ["phone", "first_name"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,12 +25,21 @@ export async function addContact(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const phone = typeof args.phone === 'string' ? args.phone : '';
|
||||
const firstName = typeof args.first_name === 'string' ? args.first_name : '';
|
||||
const lastName = optString(args, 'last_name') ?? '';
|
||||
const phone = typeof args.phone === "string" ? args.phone : "";
|
||||
const firstName =
|
||||
typeof args.first_name === "string" ? args.first_name : "";
|
||||
const lastName = optString(args, "last_name") ?? "";
|
||||
|
||||
if (!phone) return { content: [{ type: 'text', text: 'phone is required' }], isError: true };
|
||||
if (!firstName) return { content: [{ type: 'text', text: 'first_name is required' }], isError: true };
|
||||
if (!phone)
|
||||
return {
|
||||
content: [{ type: "text", text: "phone is required" }],
|
||||
isError: true,
|
||||
};
|
||||
if (!firstName)
|
||||
return {
|
||||
content: [{ type: "text", text: "first_name is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
@@ -49,10 +58,18 @@ export async function addContact(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Contact ' + firstName + ' ' + lastName + ' (' + phone + ') added.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Contact " + firstName + " " + lastName + " (" + phone + ") added.",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'add_contact',
|
||||
"add_contact",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { toInputPeer } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { toInputPeer } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'block_user',
|
||||
description: 'Block a user',
|
||||
name: "block_user",
|
||||
description: "Block a user",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID to block' },
|
||||
user_id: { type: "string", description: "User ID to block" },
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function blockUser(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -33,10 +33,12 @@ export async function blockUser(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'User ' + userId + ' blocked.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "User " + userId + " blocked." }],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'block_user',
|
||||
"block_user",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "clear_draft",
|
||||
@@ -23,10 +23,14 @@ export async function clearDraft(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -36,15 +40,19 @@ export async function clearDraft(
|
||||
await client.invoke(
|
||||
new Api.messages.SaveDraft({
|
||||
peer: inputPeer,
|
||||
message: '',
|
||||
message: "",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Draft cleared in chat ' + chatId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "Draft cleared in chat " + chatId + "." },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'clear_draft',
|
||||
"clear_draft",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.DRAFT,
|
||||
);
|
||||
|
||||
@@ -52,7 +52,8 @@ export async function createChannel(
|
||||
);
|
||||
});
|
||||
|
||||
const channelId = narrow<ResultWithChats>(result)?.chats?.[0]?.id ?? "unknown";
|
||||
const channelId =
|
||||
narrow<ResultWithChats>(result)?.chats?.[0]?.id ?? "unknown";
|
||||
const type = megagroup ? "Supergroup" : "Channel";
|
||||
return {
|
||||
content: [
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "create_poll",
|
||||
@@ -26,15 +26,27 @@ export async function createPoll(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const question = typeof args.question === 'string' ? args.question : '';
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const question = typeof args.question === "string" ? args.question : "";
|
||||
const options = Array.isArray(args.options) ? args.options.map(String) : [];
|
||||
|
||||
if (!question) return { content: [{ type: 'text', text: 'question is required' }], isError: true };
|
||||
if (options.length < 2) return { content: [{ type: 'text', text: 'At least 2 options are required' }], isError: true };
|
||||
if (!question)
|
||||
return {
|
||||
content: [{ type: "text", text: "question is required" }],
|
||||
isError: true,
|
||||
};
|
||||
if (options.length < 2)
|
||||
return {
|
||||
content: [{ type: "text", text: "At least 2 options are required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -47,25 +59,29 @@ export async function createPoll(
|
||||
media: new Api.InputMediaPoll({
|
||||
poll: new Api.Poll({
|
||||
id: bigInt(0),
|
||||
question: new Api.TextWithEntities({ text: question, entities: [] }),
|
||||
answers: options.map((opt, i) =>
|
||||
new Api.PollAnswer({
|
||||
text: new Api.TextWithEntities({ text: opt, entities: [] }),
|
||||
option: Buffer.from([i]),
|
||||
}),
|
||||
question: new Api.TextWithEntities({
|
||||
text: question,
|
||||
entities: [],
|
||||
}),
|
||||
answers: options.map(
|
||||
(opt, i) =>
|
||||
new Api.PollAnswer({
|
||||
text: new Api.TextWithEntities({ text: opt, entities: [] }),
|
||||
option: Buffer.from([i]),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
message: '',
|
||||
message: "",
|
||||
randomId: bigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Poll created: ' + question }] };
|
||||
return { content: [{ type: "text", text: "Poll created: " + question }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'create_poll',
|
||||
"create_poll",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { toInputUser } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { toInputUser } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'delete_contact',
|
||||
description: 'Delete a contact from your Telegram account',
|
||||
name: "delete_contact",
|
||||
description: "Delete a contact from your Telegram account",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID to remove from contacts' },
|
||||
user_id: {
|
||||
type: "string",
|
||||
description: "User ID to remove from contacts",
|
||||
},
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +26,7 @@ export async function deleteContact(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -35,10 +38,12 @@ export async function deleteContact(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Contact ' + userId + ' deleted.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "Contact " + userId + " deleted." }],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'delete_contact',
|
||||
"delete_contact",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'delete_message',
|
||||
description: 'Delete a message',
|
||||
name: "delete_message",
|
||||
description: "Delete a message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,21 +23,27 @@ export async function deleteMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -48,11 +54,13 @@ export async function deleteMessage(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} deleted successfully.` }],
|
||||
content: [
|
||||
{ type: "text", text: `Message ${messageId} deleted successfully.` },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'delete_message',
|
||||
"delete_message",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import type { ApiPhoto } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import type { ApiPhoto } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "delete_profile_photo",
|
||||
@@ -31,8 +31,15 @@ export async function deleteProfilePhoto(
|
||||
);
|
||||
});
|
||||
|
||||
if (!photos || !('photos' in photos) || !Array.isArray(photos.photos) || photos.photos.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No profile photo to delete.' }] };
|
||||
if (
|
||||
!photos ||
|
||||
!("photos" in photos) ||
|
||||
!Array.isArray(photos.photos) ||
|
||||
photos.photos.length === 0
|
||||
) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No profile photo to delete." }],
|
||||
};
|
||||
}
|
||||
|
||||
const photo = narrow<ApiPhoto>(photos.photos[0]);
|
||||
@@ -40,15 +47,21 @@ export async function deleteProfilePhoto(
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
await client.invoke(
|
||||
new Api.photos.DeletePhotos({
|
||||
id: [new Api.InputPhoto({ id: photo.id, accessHash: photo.accessHash, fileReference: photo.fileReference })],
|
||||
id: [
|
||||
new Api.InputPhoto({
|
||||
id: photo.id,
|
||||
accessHash: photo.accessHash,
|
||||
fileReference: photo.fileReference,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Profile photo deleted.' }] };
|
||||
return { content: [{ type: "text", text: "Profile photo deleted." }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'delete_profile_photo',
|
||||
"delete_profile_photo",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -8,9 +8,9 @@ export const tool: MCPTool = {
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
file_path: { type: 'string', description: 'Path to the photo file' },
|
||||
file_path: { type: "string", description: "Path to the photo file" },
|
||||
},
|
||||
required: ["chat_id", 'file_path'],
|
||||
required: ["chat_id", "file_path"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,7 +19,12 @@ export async function editChatPhoto(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'edit_chat_photo requires file upload which is not supported via MCP text interface. Use the Telegram client directly.' }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "edit_chat_photo requires file upload which is not supported via MCP text interface. Use the Telegram client directly.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'edit_message',
|
||||
description: 'Edit an existing message',
|
||||
name: "edit_message",
|
||||
description: "Edit an existing message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
new_text: { type: 'string', description: 'New message text' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
new_text: { type: "string", description: "New message text" },
|
||||
},
|
||||
required: ['chat_id', 'message_id', 'new_text'],
|
||||
required: ["chat_id", "message_id", "new_text"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,28 +24,34 @@ export async function editMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const newText = typeof args.new_text === 'string' ? args.new_text : '';
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const newText = typeof args.new_text === "string" ? args.new_text : "";
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (!newText) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'new_text is required' }],
|
||||
content: [{ type: "text", text: "new_text is required" }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -56,11 +62,13 @@ export async function editMessage(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} edited successfully.` }],
|
||||
content: [
|
||||
{ type: "text", text: `Message ${messageId} edited successfully.` },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'edit_message',
|
||||
"edit_message",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { optString } from '../args';
|
||||
import type { ChatInviteResult } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { optString } from "../args";
|
||||
import type { ChatInviteResult } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'export_chat_invite',
|
||||
description: 'Export a new chat invite link',
|
||||
name: "export_chat_invite",
|
||||
description: "Export a new chat invite link",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
title: { type: 'string', description: 'Link title' },
|
||||
expire_date: { type: 'number', description: 'Expiration timestamp' },
|
||||
usage_limit: { type: 'number', description: 'Max number of uses' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
title: { type: "string", description: "Link title" },
|
||||
expire_date: { type: "number", description: "Expiration timestamp" },
|
||||
usage_limit: { type: "number", description: "Max number of uses" },
|
||||
},
|
||||
required: ['chat_id'],
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,13 +29,19 @@ export async function exportChatInvite(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const title = optString(args, 'title');
|
||||
const expireDate = typeof args.expire_date === 'number' ? args.expire_date : undefined;
|
||||
const usageLimit = typeof args.usage_limit === 'number' ? args.usage_limit : undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const title = optString(args, "title");
|
||||
const expireDate =
|
||||
typeof args.expire_date === "number" ? args.expire_date : undefined;
|
||||
const usageLimit =
|
||||
typeof args.usage_limit === "number" ? args.usage_limit : undefined;
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -54,13 +60,18 @@ export async function exportChatInvite(
|
||||
|
||||
const link = narrow<ChatInviteResult>(result)?.link;
|
||||
if (!link) {
|
||||
return { content: [{ type: 'text', text: 'Could not create invite link.' }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "Could not create invite link." }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: `Invite link created: ${link}` }] };
|
||||
return {
|
||||
content: [{ type: "text", text: `Invite link created: ${link}` }],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'export_chat_invite',
|
||||
"export_chat_invite",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.GROUP,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'forward_message',
|
||||
description: 'Forward a message to another chat',
|
||||
name: "forward_message",
|
||||
description: "Forward a message to another chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
from_chat_id: { type: 'string', description: 'Source chat ID' },
|
||||
to_chat_id: { type: 'string', description: 'Target chat ID' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
from_chat_id: { type: "string", description: "Source chat ID" },
|
||||
to_chat_id: { type: "string", description: "Target chat ID" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
},
|
||||
required: ['from_chat_id', 'to_chat_id', 'message_id'],
|
||||
required: ["from_chat_id", "to_chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,27 +24,38 @@ export async function forwardMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const fromChatId = validateId(args.from_chat_id, 'from_chat_id');
|
||||
const toChatId = validateId(args.to_chat_id, 'to_chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const fromChatId = validateId(args.from_chat_id, "from_chat_id");
|
||||
const toChatId = validateId(args.to_chat_id, "to_chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const fromChat = getChatById(fromChatId);
|
||||
if (!fromChat) {
|
||||
return { content: [{ type: 'text', text: `Source chat not found: ${fromChatId}` }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Source chat not found: ${fromChatId}` },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const toChat = getChatById(toChatId);
|
||||
if (!toChat) {
|
||||
return { content: [{ type: 'text', text: `Target chat not found: ${toChatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Target chat not found: ${toChatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const fromEntity = fromChat.username ? fromChat.username : fromChat.id;
|
||||
@@ -59,11 +70,16 @@ export async function forwardMessage(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} forwarded from ${fromChatId} to ${toChatId}.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Message ${messageId} forwarded from ${fromChatId} to ${toChatId}.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'forward_message',
|
||||
"forward_message",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { optNumber } from '../args';
|
||||
import type { ApiUser } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { optNumber } from "../args";
|
||||
import type { ApiUser } from "../apiResultTypes";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_blocked_users',
|
||||
description: 'Get list of blocked users',
|
||||
name: "get_blocked_users",
|
||||
description: "Get list of blocked users",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: 'number', description: 'Max results', default: 50 },
|
||||
limit: { type: "number", description: "Max results", default: 50 },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -22,29 +22,33 @@ export async function getBlockedUsers(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const limit = optNumber(args, 'limit', 50);
|
||||
const limit = optNumber(args, "limit", 50);
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.invoke(
|
||||
new Api.contacts.GetBlocked({ offset: 0, limit }),
|
||||
);
|
||||
return client.invoke(new Api.contacts.GetBlocked({ offset: 0, limit }));
|
||||
});
|
||||
|
||||
if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No blocked users.' }] };
|
||||
if (
|
||||
!result ||
|
||||
!("users" in result) ||
|
||||
!Array.isArray(result.users) ||
|
||||
result.users.length === 0
|
||||
) {
|
||||
return { content: [{ type: "text", text: "No blocked users." }] };
|
||||
}
|
||||
|
||||
const lines = result.users.map((u: ApiUser) => {
|
||||
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
const username = u.username ? `@${u.username}` : '';
|
||||
const name =
|
||||
[u.firstName, u.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
const username = u.username ? `@${u.username}` : "";
|
||||
return `ID: ${u.id} | ${name} ${username}`.trim();
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_blocked_users',
|
||||
"get_blocked_users",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { FullUserResult } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { FullUserResult } from "../apiResultTypes";
|
||||
import { toInputUser, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -24,7 +24,7 @@ export async function getBotInfo(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const botId = validateId(args.chat_id, 'chat_id');
|
||||
const botId = validateId(args.chat_id, "chat_id");
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -38,30 +38,34 @@ export async function getBotInfo(
|
||||
const user = narrow<FullUserResult>(result)?.users?.[0];
|
||||
|
||||
if (!user) {
|
||||
return { content: [{ type: 'text', text: 'Bot not found: ' + botId }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "Bot not found: " + botId }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const name = [user.firstName, user.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
const name =
|
||||
[user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
const lines = [
|
||||
'Name: ' + name,
|
||||
'Username: @' + (user.username ?? 'N/A'),
|
||||
'ID: ' + user.id,
|
||||
'Bot: ' + (user.bot ? 'Yes' : 'No'),
|
||||
'About: ' + (fullUser?.about ?? 'N/A'),
|
||||
'Bot Info Description: ' + (fullUser?.botInfo?.description ?? 'N/A'),
|
||||
"Name: " + name,
|
||||
"Username: @" + (user.username ?? "N/A"),
|
||||
"ID: " + user.id,
|
||||
"Bot: " + (user.bot ? "Yes" : "No"),
|
||||
"About: " + (fullUser?.about ?? "N/A"),
|
||||
"Bot Info Description: " + (fullUser?.botInfo?.description ?? "N/A"),
|
||||
];
|
||||
|
||||
if (fullUser?.botInfo?.commands) {
|
||||
lines.push('Commands:');
|
||||
lines.push("Commands:");
|
||||
for (const cmd of fullUser.botInfo.commands) {
|
||||
lines.push(' /' + cmd.command + ' - ' + cmd.description);
|
||||
lines.push(" /" + cmd.command + " - " + cmd.description);
|
||||
}
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_bot_info',
|
||||
"get_bot_info",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
* Get Chat tool - Get detailed information about a specific chat
|
||||
*/
|
||||
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { formatEntity, getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { formatEntity, getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_chat',
|
||||
description: 'Get detailed information about a specific chat',
|
||||
name: "get_chat",
|
||||
description: "Get detailed information about a specific chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: {
|
||||
type: 'string',
|
||||
description: 'The ID or username of the chat',
|
||||
type: "string",
|
||||
description: "The ID or username of the chat",
|
||||
},
|
||||
},
|
||||
required: ['chat_id'],
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,12 +29,12 @@ export async function getChat(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const chat = getChatById(chatId);
|
||||
|
||||
if (!chat) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Chat not found: ${chatId}` }],
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -46,27 +46,27 @@ export async function getChat(
|
||||
result.push(`Title: ${entity.name}`);
|
||||
result.push(`Type: ${entity.type}`);
|
||||
if (entity.username) result.push(`Username: @${entity.username}`);
|
||||
if ('participantsCount' in chat && chat.participantsCount) {
|
||||
if ("participantsCount" in chat && chat.participantsCount) {
|
||||
result.push(`Participants: ${chat.participantsCount}`);
|
||||
}
|
||||
if ('unreadCount' in chat) {
|
||||
if ("unreadCount" in chat) {
|
||||
result.push(`Unread Messages: ${chat.unreadCount ?? 0}`);
|
||||
}
|
||||
|
||||
const lastMsg = chat.lastMessage;
|
||||
if (lastMsg) {
|
||||
const from = lastMsg.fromName ?? lastMsg.fromId ?? 'Unknown';
|
||||
const from = lastMsg.fromName ?? lastMsg.fromId ?? "Unknown";
|
||||
const date = new Date(lastMsg.date * 1000).toISOString();
|
||||
result.push(`Last Message: From ${from} at ${date}`);
|
||||
result.push(`Message: ${lastMsg.message || '[Media/No text]'}`);
|
||||
result.push(`Message: ${lastMsg.message || "[Media/No text]"}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: result.join('\n') }],
|
||||
content: [{ type: "text", text: result.join("\n") }],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_chat',
|
||||
"get_chat",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CHAT,
|
||||
);
|
||||
|
||||
@@ -2,21 +2,29 @@
|
||||
* Get Chats tool - Get a paginated list of chats
|
||||
*/
|
||||
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { optNumber } from '../args';
|
||||
import { formatEntity, getChats as getChatsApi } from '../telegramApi';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { optNumber } from "../args";
|
||||
import { formatEntity, getChats as getChatsApi } from "../telegramApi";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_chats',
|
||||
description: 'Get a paginated list of chats',
|
||||
name: "get_chats",
|
||||
description: "Get a paginated list of chats",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
page: { type: 'number', description: 'Page number (1-indexed)', default: 1 },
|
||||
page_size: { type: 'number', description: 'Number of chats per page', default: 20 },
|
||||
page: {
|
||||
type: "number",
|
||||
description: "Page number (1-indexed)",
|
||||
default: 1,
|
||||
},
|
||||
page_size: {
|
||||
type: "number",
|
||||
description: "Number of chats per page",
|
||||
default: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -26,15 +34,15 @@ export async function getChats(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const page = optNumber(args, 'page', 1);
|
||||
const pageSize = optNumber(args, 'page_size', 20);
|
||||
const page = optNumber(args, "page", 1);
|
||||
const pageSize = optNumber(args, "page_size", 20);
|
||||
const start = (page - 1) * pageSize;
|
||||
|
||||
const chats = await getChatsApi(pageSize + start);
|
||||
const paginatedChats = chats.slice(start, start + pageSize);
|
||||
|
||||
if (paginatedChats.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'Page out of range.' }] };
|
||||
return { content: [{ type: "text", text: "Page out of range." }] };
|
||||
}
|
||||
|
||||
const lines = paginatedChats.map((chat) => {
|
||||
@@ -42,10 +50,10 @@ export async function getChats(
|
||||
return `Chat ID: ${entity.id}, Title: ${entity.name}`;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_chats',
|
||||
"get_chats",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CHAT,
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { store } from '../../../../store';
|
||||
import { selectOrderedChats } from '../../../../store/telegramSelectors';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { store } from "../../../../store";
|
||||
import { selectOrderedChats } from "../../../../store/telegramSelectors";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_contact_chats',
|
||||
description: 'Get all chats that are direct messages with contacts',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
name: "get_contact_chats",
|
||||
description: "Get all chats that are direct messages with contacts",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
export async function getContactChats(
|
||||
@@ -17,21 +17,28 @@ export async function getContactChats(
|
||||
try {
|
||||
const state = store.getState();
|
||||
const chats = selectOrderedChats(state);
|
||||
const dmChats = chats.filter((c) => c.type === 'private');
|
||||
const dmChats = chats.filter((c) => c.type === "private");
|
||||
|
||||
if (dmChats.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No contact chats found.' }] };
|
||||
return { content: [{ type: "text", text: "No contact chats found." }] };
|
||||
}
|
||||
|
||||
const lines = dmChats.map((c) => {
|
||||
const username = c.username ? '@' + c.username : '';
|
||||
return ('ID: ' + c.id + ' | ' + (c.title ?? 'DM') + ' ' + username).trim();
|
||||
const username = c.username ? "@" + c.username : "";
|
||||
return (
|
||||
"ID: " +
|
||||
c.id +
|
||||
" | " +
|
||||
(c.title ?? "DM") +
|
||||
" " +
|
||||
username
|
||||
).trim();
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_contact_chats',
|
||||
"get_contact_chats",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import type { ContactIdEntry } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import type { ContactIdEntry } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
type ContactIdResult = number | ContactIdEntry;
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_contact_ids',
|
||||
description: 'Get IDs of all contacts',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
name: "get_contact_ids",
|
||||
description: "Get IDs of all contacts",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
export async function getContactIds(
|
||||
@@ -27,16 +27,20 @@ export async function getContactIds(
|
||||
});
|
||||
|
||||
if (!result || !Array.isArray(result) || result.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No contact IDs found.' }] };
|
||||
return { content: [{ type: "text", text: "No contact IDs found." }] };
|
||||
}
|
||||
|
||||
const ids = narrow<ContactIdResult[]>(result).map((c) =>
|
||||
String(typeof c === 'number' ? c : c.userId ?? c),
|
||||
String(typeof c === "number" ? c : (c.userId ?? c)),
|
||||
);
|
||||
return { content: [{ type: 'text', text: ids.length + ' contacts:\n' + ids.join('\n') }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: ids.length + " contacts:\n" + ids.join("\n") },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_contact_ids',
|
||||
"get_contact_ids",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { store } from '../../../../store';
|
||||
import { selectOrderedChats } from '../../../../store/telegramSelectors';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { store } from "../../../../store";
|
||||
import { selectOrderedChats } from "../../../../store/telegramSelectors";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_direct_chat_by_contact',
|
||||
description: 'Get direct message chat with a contact',
|
||||
name: "get_direct_chat_by_contact",
|
||||
description: "Get direct message chat with a contact",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID' },
|
||||
user_id: { type: "string", description: "User ID" },
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,27 +22,42 @@ export async function getDirectChatByContact(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const state = store.getState();
|
||||
const chats = selectOrderedChats(state);
|
||||
|
||||
const dmChat = chats.find(
|
||||
(c) => c.type === 'private' && String(c.id) === String(userId),
|
||||
(c) => c.type === "private" && String(c.id) === String(userId),
|
||||
);
|
||||
|
||||
if (!dmChat) {
|
||||
return { content: [{ type: 'text', text: 'No direct chat found with user ' + userId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No direct chat found with user " + userId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Chat ID: ' + dmChat.id + ' | Title: ' + (dmChat.title ?? 'DM') + ' | Username: ' + (dmChat.username ?? 'N/A'),
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Chat ID: " +
|
||||
dmChat.id +
|
||||
" | Title: " +
|
||||
(dmChat.title ?? "DM") +
|
||||
" | Username: " +
|
||||
(dmChat.username ?? "N/A"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_direct_chat_by_contact',
|
||||
"get_direct_chat_by_contact",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { UpdatesResult } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { UpdatesResult } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -25,25 +25,29 @@ export async function getDrafts(
|
||||
|
||||
const updates = narrow<UpdatesResult>(result);
|
||||
if (!updates || !updates.updates || updates.updates.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No drafts found.' }] };
|
||||
return { content: [{ type: "text", text: "No drafts found." }] };
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const update of updates.updates) {
|
||||
if (update.draft && update.draft.message) {
|
||||
const peerId = update.peer?.userId ?? update.peer?.chatId ?? update.peer?.channelId ?? '?';
|
||||
lines.push('Peer ' + peerId + ': ' + update.draft.message);
|
||||
const peerId =
|
||||
update.peer?.userId ??
|
||||
update.peer?.chatId ??
|
||||
update.peer?.channelId ??
|
||||
"?";
|
||||
lines.push("Peer " + peerId + ": " + update.draft.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No drafts found.' }] };
|
||||
return { content: [{ type: "text", text: "No drafts found." }] };
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_drafts',
|
||||
"get_drafts",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.DRAFT,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { optNumber } from '../args';
|
||||
import type { InlineBotResults } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { optNumber } from "../args";
|
||||
import type { InlineBotResults } from "../apiResultTypes";
|
||||
import { toInputUser, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -14,7 +14,7 @@ export const tool: MCPTool = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query" },
|
||||
limit: { type: 'number', description: 'Max results', default: 10 },
|
||||
limit: { type: "number", description: "Max results", default: 10 },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
@@ -25,38 +25,51 @@ export async function getGifSearch(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const query = typeof args.query === 'string' ? args.query : '';
|
||||
if (!query) return { content: [{ type: 'text', text: 'query is required' }], isError: true };
|
||||
const limit = optNumber(args, 'limit', 10);
|
||||
const query = typeof args.query === "string" ? args.query : "";
|
||||
if (!query)
|
||||
return {
|
||||
content: [{ type: "text", text: "query is required" }],
|
||||
isError: true,
|
||||
};
|
||||
const limit = optNumber(args, "limit", 10);
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
const bot = await client.getInputEntity('gif');
|
||||
const bot = await client.getInputEntity("gif");
|
||||
return client.invoke(
|
||||
new Api.messages.GetInlineBotResults({
|
||||
bot: toInputUser(bot),
|
||||
peer: new Api.InputPeerSelf(),
|
||||
query,
|
||||
offset: '',
|
||||
offset: "",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const results = narrow<InlineBotResults>(result)?.results;
|
||||
if (!results || !Array.isArray(results) || results.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No GIFs found for: ' + query }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "No GIFs found for: " + query }],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = results.slice(0, limit).map((r, i: number) => {
|
||||
const title = r.title ?? r.description ?? 'GIF ' + (i + 1);
|
||||
return (i + 1) + '. ' + title;
|
||||
const title = r.title ?? r.description ?? "GIF " + (i + 1);
|
||||
return i + 1 + ". " + title;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.length + ' GIFs found:\n' + lines.join('\n') }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: lines.length + " GIFs found:\n" + lines.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_gif_search',
|
||||
"get_gif_search",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MEDIA,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById, getMessages, formatMessage } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { optNumber } from '../args';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById, getMessages, formatMessage } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { optNumber } from "../args";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_history',
|
||||
description: 'Get message history from a chat',
|
||||
name: "get_history",
|
||||
description: "Get message history from a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
limit: { type: 'number', description: 'Number of messages', default: 20 },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
limit: { type: "number", description: "Number of messages", default: 20 },
|
||||
},
|
||||
required: ['chat_id'],
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,29 +23,34 @@ export async function getHistory(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const limit = optNumber(args, 'limit', 20);
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const limit = optNumber(args, "limit", 20);
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const messages = await getMessages(chatId, limit, 0);
|
||||
if (!messages || messages.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No messages found in this chat.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "No messages found in this chat." }],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = messages.map((msg) => {
|
||||
const f = formatMessage(msg);
|
||||
const from = msg.fromName ?? msg.fromId ?? 'Unknown';
|
||||
return `ID: ${f.id} | ${from} | ${f.date} | ${f.text || '[Media/No text]'}`;
|
||||
const from = msg.fromName ?? msg.fromId ?? "Unknown";
|
||||
return `ID: ${f.id} | ${from} | ${f.date} | ${f.text || "[Media/No text]"}`;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_history',
|
||||
"get_history",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById, getMessages, formatMessage } from '../telegramApi';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById, getMessages, formatMessage } from "../telegramApi";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_last_interaction',
|
||||
description: 'Get the last message exchanged with a user or chat',
|
||||
name: "get_last_interaction",
|
||||
description: "Get the last message exchanged with a user or chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
},
|
||||
required: ['chat_id'],
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,31 +21,46 @@ export async function getLastInteraction(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const messages = await getMessages(chatId, 1, 0);
|
||||
if (!messages || messages.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No messages found in this chat.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "No messages found in this chat." }],
|
||||
};
|
||||
}
|
||||
|
||||
const msg = messages[0];
|
||||
const f = formatMessage(msg);
|
||||
const from = msg.fromName ?? msg.fromId ?? 'Unknown';
|
||||
const from = msg.fromName ?? msg.fromId ?? "Unknown";
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Last message in ' + (chat.title ?? chatId) + ':\nFrom: ' + from + ' | Date: ' + f.date + ' | ' + (f.text || '[Media/No text]'),
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Last message in " +
|
||||
(chat.title ?? chatId) +
|
||||
":\nFrom: " +
|
||||
from +
|
||||
" | Date: " +
|
||||
f.date +
|
||||
" | " +
|
||||
(f.text || "[Media/No text]"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_last_interaction',
|
||||
"get_last_interaction",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById, getMessages } from '../telegramApi';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById, getMessages } from "../telegramApi";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "get_media_info",
|
||||
@@ -22,35 +22,61 @@ export async function getMediaInfo(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const messages = await getMessages(chatId, 200, 0);
|
||||
if (!messages) return { content: [{ type: 'text', text: 'No messages found.' }] };
|
||||
if (!messages)
|
||||
return { content: [{ type: "text", text: "No messages found." }] };
|
||||
|
||||
const msg = messages.find((m) => String(m.id) === String(messageId));
|
||||
if (!msg) return { content: [{ type: 'text', text: 'Message ' + messageId + ' not found in cache.' }], isError: true };
|
||||
if (!msg)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Message " + messageId + " not found in cache.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
if (!msg.media) {
|
||||
return { content: [{ type: 'text', text: 'No media in message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "No media in message " + messageId + "." },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const info = {
|
||||
...msg.media,
|
||||
type: msg.media.type ?? 'unknown',
|
||||
type: msg.media.type ?? "unknown",
|
||||
};
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
|
||||
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_media_info',
|
||||
"get_media_info",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MEDIA,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById, getMessages, formatMessage } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { optNumber } from '../args';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById, getMessages, formatMessage } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { optNumber } from "../args";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_message_context',
|
||||
description: 'Get context around a specific message',
|
||||
name: "get_message_context",
|
||||
description: "Get context around a specific message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
limit: { type: 'number', description: 'Number of messages before/after', default: 5 },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Number of messages before/after",
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,33 +28,48 @@ export async function getMessageContext(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const contextSize = optNumber(args, 'limit', 5);
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const contextSize = optNumber(args, "limit", 5);
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const allMessages = await getMessages(chatId, 200, 0);
|
||||
if (!allMessages || allMessages.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No messages found in this chat.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "No messages found in this chat." }],
|
||||
};
|
||||
}
|
||||
|
||||
const targetIndex = allMessages.findIndex((m) => String(m.id) === String(messageId));
|
||||
const targetIndex = allMessages.findIndex(
|
||||
(m) => String(m.id) === String(messageId),
|
||||
);
|
||||
if (targetIndex === -1) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} not found in cached messages.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Message ${messageId} not found in cached messages.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -61,15 +80,15 @@ export async function getMessageContext(
|
||||
|
||||
const lines = contextMessages.map((msg) => {
|
||||
const f = formatMessage(msg);
|
||||
const from = msg.fromName ?? msg.fromId ?? 'Unknown';
|
||||
const marker = String(msg.id) === String(messageId) ? ' >>> ' : ' ';
|
||||
return `${marker}ID: ${f.id} | ${from} | ${f.date} | ${f.text || '[Media/No text]'}`;
|
||||
const from = msg.fromName ?? msg.fromId ?? "Unknown";
|
||||
const marker = String(msg.id) === String(messageId) ? " >>> " : " ";
|
||||
return `${marker}ID: ${f.id} | ${from} | ${f.date} | ${f.text || "[Media/No text]"}`;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_message_context',
|
||||
"get_message_context",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { UpdatesResult } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { UpdatesResult } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_message_reactions',
|
||||
description: 'Get reactions on a message',
|
||||
name: "get_message_reactions",
|
||||
description: "Get reactions on a message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,15 +26,27 @@ export async function getMessageReactions(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -51,28 +63,49 @@ export async function getMessageReactions(
|
||||
|
||||
const updates = narrow<UpdatesResult>(result);
|
||||
if (!updates || !updates.updates || updates.updates.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No reactions found on message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No reactions found on message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
for (const update of updates.updates) {
|
||||
if (update.reactions && update.reactions.results) {
|
||||
for (const r of update.reactions.results) {
|
||||
const emoji = r.reaction?.emoticon ?? r.reaction?.className ?? '?';
|
||||
const emoji = r.reaction?.emoticon ?? r.reaction?.className ?? "?";
|
||||
const count = r.count ?? 0;
|
||||
lines.push(emoji + ': ' + count);
|
||||
lines.push(emoji + ": " + count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No reactions found on message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No reactions found on message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: 'Reactions on message ' + messageId + ':\n' + lines.join('\n') }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reactions on message " + messageId + ":\n" + lines.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_message_reactions',
|
||||
"get_message_reactions",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById, getMessages, formatMessage } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import type { ApiMessage } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById, getMessages, formatMessage } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import type { ApiMessage } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_pinned_messages',
|
||||
description: 'Get pinned messages from a chat',
|
||||
name: "get_pinned_messages",
|
||||
description: "Get pinned messages from a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { chat_id: { type: 'string', description: 'Chat ID or username' } },
|
||||
required: ['chat_id'],
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
},
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,11 +26,14 @@ export async function getPinnedMessages(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -41,7 +46,7 @@ export async function getPinnedMessages(
|
||||
const result = await client.invoke(
|
||||
new Api.messages.Search({
|
||||
peer: inputPeer,
|
||||
q: '',
|
||||
q: "",
|
||||
filter: new Api.InputMessagesFilterPinned(),
|
||||
minDate: 0,
|
||||
maxDate: 0,
|
||||
@@ -54,11 +59,13 @@ export async function getPinnedMessages(
|
||||
}),
|
||||
);
|
||||
|
||||
if ('messages' in result && Array.isArray(result.messages)) {
|
||||
if ("messages" in result && Array.isArray(result.messages)) {
|
||||
pinnedLines = narrow<ApiMessage[]>(result.messages).map((msg) => {
|
||||
const id = msg.id ?? '?';
|
||||
const text = msg.message ?? '[Media/No text]';
|
||||
const date = msg.date ? new Date(msg.date * 1000).toISOString() : 'unknown';
|
||||
const id = msg.id ?? "?";
|
||||
const text = msg.message ?? "[Media/No text]";
|
||||
const date = msg.date
|
||||
? new Date(msg.date * 1000).toISOString()
|
||||
: "unknown";
|
||||
return `ID: ${id} | Date: ${date} | ${text}`;
|
||||
});
|
||||
}
|
||||
@@ -69,19 +76,19 @@ export async function getPinnedMessages(
|
||||
const pinned = allMessages.filter((m) => narrow<ApiMessage>(m).pinned);
|
||||
pinnedLines = pinned.map((msg) => {
|
||||
const f = formatMessage(msg);
|
||||
return `ID: ${f.id} | Date: ${f.date} | ${f.text || '[Media/No text]'}`;
|
||||
return `ID: ${f.id} | Date: ${f.date} | ${f.text || "[Media/No text]"}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (pinnedLines.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No pinned messages found.' }] };
|
||||
return { content: [{ type: "text", text: "No pinned messages found." }] };
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: pinnedLines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: pinnedLines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_pinned_messages',
|
||||
"get_pinned_messages",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { PrivacyResult } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { PrivacyResult } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -12,7 +12,12 @@ export const tool: MCPTool = {
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Privacy key: phone_number, last_seen, profile_photo, forwards, phone_call, chat_invite', default: 'last_seen' },
|
||||
key: {
|
||||
type: "string",
|
||||
description:
|
||||
"Privacy key: phone_number, last_seen, profile_photo, forwards, phone_call, chat_invite",
|
||||
default: "last_seen",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -22,7 +27,7 @@ export async function getPrivacySettings(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const keyStr = typeof args.key === 'string' ? args.key : 'last_seen';
|
||||
const keyStr = typeof args.key === "string" ? args.key : "last_seen";
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const keyMap: Record<string, Api.TypeInputPrivacyKey> = {
|
||||
@@ -36,7 +41,19 @@ export async function getPrivacySettings(
|
||||
|
||||
const key = keyMap[keyStr];
|
||||
if (!key) {
|
||||
return { content: [{ type: 'text', text: 'Unknown privacy key: ' + keyStr + '. Valid keys: ' + Object.keys(keyMap).join(', ') }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Unknown privacy key: " +
|
||||
keyStr +
|
||||
". Valid keys: " +
|
||||
Object.keys(keyMap).join(", "),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -45,14 +62,25 @@ export async function getPrivacySettings(
|
||||
|
||||
const rules = narrow<PrivacyResult>(result)?.rules;
|
||||
if (!rules || !Array.isArray(rules)) {
|
||||
return { content: [{ type: 'text', text: 'No privacy rules found for ' + keyStr + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "No privacy rules found for " + keyStr + "." },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = rules.map((r) => r.className ?? 'Unknown rule');
|
||||
return { content: [{ type: 'text', text: 'Privacy settings for ' + keyStr + ':\n' + lines.join('\n') }] };
|
||||
const lines = rules.map((r) => r.className ?? "Unknown rule");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Privacy settings for " + keyStr + ":\n" + lines.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_privacy_settings',
|
||||
"get_privacy_settings",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import { optNumber } from '../args';
|
||||
import type { AdminLogResult } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import { optNumber } from "../args";
|
||||
import type { AdminLogResult } from "../apiResultTypes";
|
||||
import { toInputChannel, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -28,14 +28,26 @@ export async function getRecentActions(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const limit = optNumber(args, 'limit', 20);
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const limit = optNumber(args, "limit", 20);
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
if (chat.type !== 'channel' && chat.type !== 'supergroup') {
|
||||
return { content: [{ type: 'text', text: 'Recent actions are only available for channels/supergroups.' }], isError: true };
|
||||
if (chat.type !== "channel" && chat.type !== "supergroup") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Recent actions are only available for channels/supergroups.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
@@ -46,7 +58,7 @@ export async function getRecentActions(
|
||||
return client.invoke(
|
||||
new Api.channels.GetAdminLog({
|
||||
channel: toInputChannel(inputChannel),
|
||||
q: '',
|
||||
q: "",
|
||||
maxId: bigInt(0),
|
||||
minId: bigInt(0),
|
||||
limit,
|
||||
@@ -56,19 +68,19 @@ export async function getRecentActions(
|
||||
|
||||
const events = narrow<AdminLogResult>(result)?.events;
|
||||
if (!events || !Array.isArray(events) || events.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No recent actions found.' }] };
|
||||
return { content: [{ type: "text", text: "No recent actions found." }] };
|
||||
}
|
||||
|
||||
const lines = events.map((e) => {
|
||||
const date = e.date ? new Date(e.date * 1000).toISOString() : 'unknown';
|
||||
const action = e.action?.className ?? 'unknown';
|
||||
return date + ' | ' + action;
|
||||
const date = e.date ? new Date(e.date * 1000).toISOString() : "unknown";
|
||||
const action = e.action?.className ?? "unknown";
|
||||
return date + " | " + action;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_recent_actions',
|
||||
"get_recent_actions",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.ADMIN,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import type { StickerSetsResult } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import type { StickerSetsResult } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -21,22 +21,39 @@ export async function getStickerSets(
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.invoke(new Api.messages.GetAllStickers({ hash: bigInt(0) }));
|
||||
return client.invoke(
|
||||
new Api.messages.GetAllStickers({ hash: bigInt(0) }),
|
||||
);
|
||||
});
|
||||
|
||||
const sets = narrow<StickerSetsResult>(result)?.sets;
|
||||
if (!sets || !Array.isArray(sets) || sets.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No sticker sets found.' }] };
|
||||
return { content: [{ type: "text", text: "No sticker sets found." }] };
|
||||
}
|
||||
|
||||
const lines = sets.map((s) => {
|
||||
return 'ID: ' + s.id + ' | ' + (s.title ?? 'Untitled') + ' (' + (s.count ?? 0) + ' stickers)';
|
||||
return (
|
||||
"ID: " +
|
||||
s.id +
|
||||
" | " +
|
||||
(s.title ?? "Untitled") +
|
||||
" (" +
|
||||
(s.count ?? 0) +
|
||||
" stickers)"
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.length + ' sticker sets:\n' + lines.join('\n') }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: lines.length + " sticker sets:\n" + lines.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_sticker_sets',
|
||||
"get_sticker_sets",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MEDIA,
|
||||
);
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import { optNumber } from '../args';
|
||||
import type { ApiPhoto } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import { optNumber } from "../args";
|
||||
import type { ApiPhoto } from "../apiResultTypes";
|
||||
import { toInputUser, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_user_photos',
|
||||
description: 'Get profile photos of a user',
|
||||
name: "get_user_photos",
|
||||
description: "Get profile photos of a user",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID' },
|
||||
limit: { type: 'number', description: 'Max photos', default: 10 },
|
||||
user_id: { type: "string", description: "User ID" },
|
||||
limit: { type: "number", description: "Max photos", default: 10 },
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,8 +27,8 @@ export async function getUserPhotos(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const limit = optNumber(args, 'limit', 10);
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const limit = optNumber(args, "limit", 10);
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -43,19 +43,33 @@ export async function getUserPhotos(
|
||||
);
|
||||
});
|
||||
|
||||
if (!result || !('photos' in result) || !Array.isArray(result.photos) || result.photos.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No photos found.' }] };
|
||||
if (
|
||||
!result ||
|
||||
!("photos" in result) ||
|
||||
!Array.isArray(result.photos) ||
|
||||
result.photos.length === 0
|
||||
) {
|
||||
return { content: [{ type: "text", text: "No photos found." }] };
|
||||
}
|
||||
|
||||
const lines = narrow<ApiPhoto[]>(result.photos).map((photo, i: number) => {
|
||||
const date = photo.date ? new Date(photo.date * 1000).toISOString() : 'unknown';
|
||||
return 'Photo ' + (i + 1) + ': ID ' + photo.id + ' | Date: ' + date;
|
||||
const date = photo.date
|
||||
? new Date(photo.date * 1000).toISOString()
|
||||
: "unknown";
|
||||
return "Photo " + (i + 1) + ": ID " + photo.id + " | Date: " + date;
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.length + ' photos found:\n' + lines.join('\n') }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: lines.length + " photos found:\n" + lines.join("\n"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_user_photos',
|
||||
"get_user_photos",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { ApiUser } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { ApiUser } from "../apiResultTypes";
|
||||
import { toInputUser, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'get_user_status',
|
||||
description: 'Get online status of a user',
|
||||
name: "get_user_status",
|
||||
description: "Get online status of a user",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID' },
|
||||
user_id: { type: "string", description: "User ID" },
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getUserStatus(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -35,28 +35,41 @@ export async function getUserStatus(
|
||||
});
|
||||
|
||||
if (!result || !Array.isArray(result) || result.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'User ' + userId + ' not found.' }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "User " + userId + " not found." }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const user = narrow<ApiUser>(result[0]);
|
||||
const name = [user.firstName, user.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
let statusText = 'unknown';
|
||||
const name =
|
||||
[user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
let statusText = "unknown";
|
||||
|
||||
if (user.status) {
|
||||
const s = user.status;
|
||||
if (s.className === 'UserStatusOnline') statusText = 'Online';
|
||||
else if (s.className === 'UserStatusOffline')
|
||||
statusText = 'Offline (last seen: ' + (s.wasOnline ? new Date(s.wasOnline * 1000).toISOString() : 'unknown') + ')';
|
||||
else if (s.className === 'UserStatusRecently') statusText = 'Recently';
|
||||
else if (s.className === 'UserStatusLastWeek') statusText = 'Last week';
|
||||
else if (s.className === 'UserStatusLastMonth') statusText = 'Last month';
|
||||
else statusText = s.className ?? 'unknown';
|
||||
if (s.className === "UserStatusOnline") statusText = "Online";
|
||||
else if (s.className === "UserStatusOffline")
|
||||
statusText =
|
||||
"Offline (last seen: " +
|
||||
(s.wasOnline
|
||||
? new Date(s.wasOnline * 1000).toISOString()
|
||||
: "unknown") +
|
||||
")";
|
||||
else if (s.className === "UserStatusRecently") statusText = "Recently";
|
||||
else if (s.className === "UserStatusLastWeek") statusText = "Last week";
|
||||
else if (s.className === "UserStatusLastMonth") statusText = "Last month";
|
||||
else statusText = s.className ?? "unknown";
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: name + ' (ID: ' + user.id + '): ' + statusText }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: name + " (ID: " + user.id + "): " + statusText },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'get_user_status',
|
||||
"get_user_status",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -39,7 +39,8 @@ export async function importChatInvite(
|
||||
return client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||
});
|
||||
|
||||
const chatTitle = narrow<ResultWithChats>(result)?.chats?.[0]?.title ?? "unknown";
|
||||
const chatTitle =
|
||||
narrow<ResultWithChats>(result)?.chats?.[0]?.title ?? "unknown";
|
||||
return { content: [{ type: "text", text: `Joined chat: ${chatTitle}` }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
|
||||
@@ -67,7 +67,8 @@ export async function importContacts(
|
||||
);
|
||||
});
|
||||
|
||||
const imported = narrow<ImportContactsResult>(result)?.imported?.length ?? 0;
|
||||
const imported =
|
||||
narrow<ImportContactsResult>(result)?.imported?.length ?? 0;
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
|
||||
@@ -3,91 +3,154 @@
|
||||
* Each tool exports a 'tool' definition and a handler function
|
||||
*/
|
||||
|
||||
export { getChats, tool as getChatsTool } from './getChats';
|
||||
export { listChats, tool as listChatsTool } from './listChats';
|
||||
export { getChat, tool as getChatTool } from './getChat';
|
||||
export { createGroup, tool as createGroupTool } from './createGroup';
|
||||
export { inviteToGroup, tool as inviteToGroupTool } from './inviteToGroup';
|
||||
export { createChannel, tool as createChannelTool } from './createChannel';
|
||||
export { editChatTitle, tool as editChatTitleTool } from './editChatTitle';
|
||||
export { deleteChatPhoto, tool as deleteChatPhotoTool } from './deleteChatPhoto';
|
||||
export { leaveChat, tool as leaveChatTool } from './leaveChat';
|
||||
export { getParticipants, tool as getParticipantsTool } from './getParticipants';
|
||||
export { getAdmins, tool as getAdminsTool } from './getAdmins';
|
||||
export { getBannedUsers, tool as getBannedUsersTool } from './getBannedUsers';
|
||||
export { promoteAdmin, tool as promoteAdminTool } from './promoteAdmin';
|
||||
export { demoteAdmin, tool as demoteAdminTool } from './demoteAdmin';
|
||||
export { banUser, tool as banUserTool } from './banUser';
|
||||
export { unbanUser, tool as unbanUserTool } from './unbanUser';
|
||||
export { getInviteLink, tool as getInviteLinkTool } from './getInviteLink';
|
||||
export { exportChatInvite, tool as exportChatInviteTool } from './exportChatInvite';
|
||||
export { importChatInvite, tool as importChatInviteTool } from './importChatInvite';
|
||||
export { joinChatByLink, tool as joinChatByLinkTool } from './joinChatByLink';
|
||||
export { subscribePublicChannel, tool as subscribePublicChannelTool } from './subscribePublicChannel';
|
||||
export { muteChat, tool as muteChatTool } from './muteChat';
|
||||
export { unmuteChat, tool as unmuteChatTool } from './unmuteChat';
|
||||
export { archiveChat, tool as archiveChatTool } from './archiveChat';
|
||||
export { unarchiveChat, tool as unarchiveChatTool } from './unarchiveChat';
|
||||
export { getChats, tool as getChatsTool } from "./getChats";
|
||||
export { listChats, tool as listChatsTool } from "./listChats";
|
||||
export { getChat, tool as getChatTool } from "./getChat";
|
||||
export { createGroup, tool as createGroupTool } from "./createGroup";
|
||||
export { inviteToGroup, tool as inviteToGroupTool } from "./inviteToGroup";
|
||||
export { createChannel, tool as createChannelTool } from "./createChannel";
|
||||
export { editChatTitle, tool as editChatTitleTool } from "./editChatTitle";
|
||||
export {
|
||||
deleteChatPhoto,
|
||||
tool as deleteChatPhotoTool,
|
||||
} from "./deleteChatPhoto";
|
||||
export { leaveChat, tool as leaveChatTool } from "./leaveChat";
|
||||
export {
|
||||
getParticipants,
|
||||
tool as getParticipantsTool,
|
||||
} from "./getParticipants";
|
||||
export { getAdmins, tool as getAdminsTool } from "./getAdmins";
|
||||
export { getBannedUsers, tool as getBannedUsersTool } from "./getBannedUsers";
|
||||
export { promoteAdmin, tool as promoteAdminTool } from "./promoteAdmin";
|
||||
export { demoteAdmin, tool as demoteAdminTool } from "./demoteAdmin";
|
||||
export { banUser, tool as banUserTool } from "./banUser";
|
||||
export { unbanUser, tool as unbanUserTool } from "./unbanUser";
|
||||
export { getInviteLink, tool as getInviteLinkTool } from "./getInviteLink";
|
||||
export {
|
||||
exportChatInvite,
|
||||
tool as exportChatInviteTool,
|
||||
} from "./exportChatInvite";
|
||||
export {
|
||||
importChatInvite,
|
||||
tool as importChatInviteTool,
|
||||
} from "./importChatInvite";
|
||||
export { joinChatByLink, tool as joinChatByLinkTool } from "./joinChatByLink";
|
||||
export {
|
||||
subscribePublicChannel,
|
||||
tool as subscribePublicChannelTool,
|
||||
} from "./subscribePublicChannel";
|
||||
export { muteChat, tool as muteChatTool } from "./muteChat";
|
||||
export { unmuteChat, tool as unmuteChatTool } from "./unmuteChat";
|
||||
export { archiveChat, tool as archiveChatTool } from "./archiveChat";
|
||||
export { unarchiveChat, tool as unarchiveChatTool } from "./unarchiveChat";
|
||||
|
||||
export { getMessages, tool as getMessagesTool } from './getMessages';
|
||||
export { listMessages, tool as listMessagesTool } from './listMessages';
|
||||
export { listTopics, tool as listTopicsTool } from './listTopics';
|
||||
export { sendMessage, tool as sendMessageTool } from './sendMessage';
|
||||
export { replyToMessage, tool as replyToMessageTool } from './replyToMessage';
|
||||
export { editMessage, tool as editMessageTool } from './editMessage';
|
||||
export { deleteMessage, tool as deleteMessageTool } from './deleteMessage';
|
||||
export { forwardMessage, tool as forwardMessageTool } from './forwardMessage';
|
||||
export { pinMessage, tool as pinMessageTool } from './pinMessage';
|
||||
export { unpinMessage, tool as unpinMessageTool } from './unpinMessage';
|
||||
export { markAsRead, tool as markAsReadTool } from './markAsRead';
|
||||
export { getMessageContext, tool as getMessageContextTool } from './getMessageContext';
|
||||
export { getHistory, tool as getHistoryTool } from './getHistory';
|
||||
export { getPinnedMessages, tool as getPinnedMessagesTool } from './getPinnedMessages';
|
||||
export { sendReaction, tool as sendReactionTool } from './sendReaction';
|
||||
export { removeReaction, tool as removeReactionTool } from './removeReaction';
|
||||
export { getMessageReactions, tool as getMessageReactionsTool } from './getMessageReactions';
|
||||
export { listInlineButtons, tool as listInlineButtonsTool } from './listInlineButtons';
|
||||
export { pressInlineButton, tool as pressInlineButtonTool } from './pressInlineButton';
|
||||
export { saveDraft, tool as saveDraftTool } from './saveDraft';
|
||||
export { getDrafts, tool as getDraftsTool } from './getDrafts';
|
||||
export { clearDraft, tool as clearDraftTool } from './clearDraft';
|
||||
export { getMessages, tool as getMessagesTool } from "./getMessages";
|
||||
export { listMessages, tool as listMessagesTool } from "./listMessages";
|
||||
export { listTopics, tool as listTopicsTool } from "./listTopics";
|
||||
export { sendMessage, tool as sendMessageTool } from "./sendMessage";
|
||||
export { replyToMessage, tool as replyToMessageTool } from "./replyToMessage";
|
||||
export { editMessage, tool as editMessageTool } from "./editMessage";
|
||||
export { deleteMessage, tool as deleteMessageTool } from "./deleteMessage";
|
||||
export { forwardMessage, tool as forwardMessageTool } from "./forwardMessage";
|
||||
export { pinMessage, tool as pinMessageTool } from "./pinMessage";
|
||||
export { unpinMessage, tool as unpinMessageTool } from "./unpinMessage";
|
||||
export { markAsRead, tool as markAsReadTool } from "./markAsRead";
|
||||
export {
|
||||
getMessageContext,
|
||||
tool as getMessageContextTool,
|
||||
} from "./getMessageContext";
|
||||
export { getHistory, tool as getHistoryTool } from "./getHistory";
|
||||
export {
|
||||
getPinnedMessages,
|
||||
tool as getPinnedMessagesTool,
|
||||
} from "./getPinnedMessages";
|
||||
export { sendReaction, tool as sendReactionTool } from "./sendReaction";
|
||||
export { removeReaction, tool as removeReactionTool } from "./removeReaction";
|
||||
export {
|
||||
getMessageReactions,
|
||||
tool as getMessageReactionsTool,
|
||||
} from "./getMessageReactions";
|
||||
export {
|
||||
listInlineButtons,
|
||||
tool as listInlineButtonsTool,
|
||||
} from "./listInlineButtons";
|
||||
export {
|
||||
pressInlineButton,
|
||||
tool as pressInlineButtonTool,
|
||||
} from "./pressInlineButton";
|
||||
export { saveDraft, tool as saveDraftTool } from "./saveDraft";
|
||||
export { getDrafts, tool as getDraftsTool } from "./getDrafts";
|
||||
export { clearDraft, tool as clearDraftTool } from "./clearDraft";
|
||||
|
||||
export { listContacts, tool as listContactsTool } from './listContacts';
|
||||
export { searchContacts, tool as searchContactsTool } from './searchContacts';
|
||||
export { addContact, tool as addContactTool } from './addContact';
|
||||
export { deleteContact, tool as deleteContactTool } from './deleteContact';
|
||||
export { blockUser, tool as blockUserTool } from './blockUser';
|
||||
export { unblockUser, tool as unblockUserTool } from './unblockUser';
|
||||
export { getBlockedUsers, tool as getBlockedUsersTool } from './getBlockedUsers';
|
||||
export { listContacts, tool as listContactsTool } from "./listContacts";
|
||||
export { searchContacts, tool as searchContactsTool } from "./searchContacts";
|
||||
export { addContact, tool as addContactTool } from "./addContact";
|
||||
export { deleteContact, tool as deleteContactTool } from "./deleteContact";
|
||||
export { blockUser, tool as blockUserTool } from "./blockUser";
|
||||
export { unblockUser, tool as unblockUserTool } from "./unblockUser";
|
||||
export {
|
||||
getBlockedUsers,
|
||||
tool as getBlockedUsersTool,
|
||||
} from "./getBlockedUsers";
|
||||
|
||||
export { getMe, tool as getMeTool } from './getMe';
|
||||
export { updateProfile, tool as updateProfileTool } from './updateProfile';
|
||||
export { getUserPhotos, tool as getUserPhotosTool } from './getUserPhotos';
|
||||
export { getUserStatus, tool as getUserStatusTool } from './getUserStatus';
|
||||
export { getMe, tool as getMeTool } from "./getMe";
|
||||
export { updateProfile, tool as updateProfileTool } from "./updateProfile";
|
||||
export { getUserPhotos, tool as getUserPhotosTool } from "./getUserPhotos";
|
||||
export { getUserStatus, tool as getUserStatusTool } from "./getUserStatus";
|
||||
|
||||
export { searchPublicChats, tool as searchPublicChatsTool } from './searchPublicChats';
|
||||
export { searchMessages, tool as searchMessagesTool } from './searchMessages';
|
||||
export { resolveUsername, tool as resolveUsernameTool } from './resolveUsername';
|
||||
export {
|
||||
searchPublicChats,
|
||||
tool as searchPublicChatsTool,
|
||||
} from "./searchPublicChats";
|
||||
export { searchMessages, tool as searchMessagesTool } from "./searchMessages";
|
||||
export {
|
||||
resolveUsername,
|
||||
tool as resolveUsernameTool,
|
||||
} from "./resolveUsername";
|
||||
|
||||
export { getMediaInfo, tool as getMediaInfoTool } from './getMediaInfo';
|
||||
export { getRecentActions, tool as getRecentActionsTool } from './getRecentActions';
|
||||
export { createPoll, tool as createPollTool } from './createPoll';
|
||||
export { getBotInfo, tool as getBotInfoTool } from './getBotInfo';
|
||||
export { setBotCommands, tool as setBotCommandsTool } from './setBotCommands';
|
||||
export { getMediaInfo, tool as getMediaInfoTool } from "./getMediaInfo";
|
||||
export {
|
||||
getRecentActions,
|
||||
tool as getRecentActionsTool,
|
||||
} from "./getRecentActions";
|
||||
export { createPoll, tool as createPollTool } from "./createPoll";
|
||||
export { getBotInfo, tool as getBotInfoTool } from "./getBotInfo";
|
||||
export { setBotCommands, tool as setBotCommandsTool } from "./setBotCommands";
|
||||
|
||||
export { getPrivacySettings, tool as getPrivacySettingsTool } from './getPrivacySettings';
|
||||
export { setPrivacySettings, tool as setPrivacySettingsTool } from './setPrivacySettings';
|
||||
export {
|
||||
getPrivacySettings,
|
||||
tool as getPrivacySettingsTool,
|
||||
} from "./getPrivacySettings";
|
||||
export {
|
||||
setPrivacySettings,
|
||||
tool as setPrivacySettingsTool,
|
||||
} from "./setPrivacySettings";
|
||||
|
||||
export { setProfilePhoto, tool as setProfilePhotoTool } from './setProfilePhoto';
|
||||
export { deleteProfilePhoto, tool as deleteProfilePhotoTool } from './deleteProfilePhoto';
|
||||
export { editChatPhoto, tool as editChatPhotoTool } from './editChatPhoto';
|
||||
export {
|
||||
setProfilePhoto,
|
||||
tool as setProfilePhotoTool,
|
||||
} from "./setProfilePhoto";
|
||||
export {
|
||||
deleteProfilePhoto,
|
||||
tool as deleteProfilePhotoTool,
|
||||
} from "./deleteProfilePhoto";
|
||||
export { editChatPhoto, tool as editChatPhotoTool } from "./editChatPhoto";
|
||||
|
||||
export { getStickerSets, tool as getStickerSetsTool } from './getStickerSets';
|
||||
export { getGifSearch, tool as getGifSearchTool } from './getGifSearch';
|
||||
export { getStickerSets, tool as getStickerSetsTool } from "./getStickerSets";
|
||||
export { getGifSearch, tool as getGifSearchTool } from "./getGifSearch";
|
||||
|
||||
export { getContactIds, tool as getContactIdsTool } from './getContactIds';
|
||||
export { importContacts, tool as importContactsTool } from './importContacts';
|
||||
export { exportContacts, tool as exportContactsTool } from './exportContacts';
|
||||
export { getDirectChatByContact, tool as getDirectChatByContactTool } from './getDirectChatByContact';
|
||||
export { getContactChats, tool as getContactChatsTool } from './getContactChats';
|
||||
export { getLastInteraction, tool as getLastInteractionTool } from './getLastInteraction';
|
||||
export { getContactIds, tool as getContactIdsTool } from "./getContactIds";
|
||||
export { importContacts, tool as importContactsTool } from "./importContacts";
|
||||
export { exportContacts, tool as exportContactsTool } from "./exportContacts";
|
||||
export {
|
||||
getDirectChatByContact,
|
||||
tool as getDirectChatByContactTool,
|
||||
} from "./getDirectChatByContact";
|
||||
export {
|
||||
getContactChats,
|
||||
tool as getContactChatsTool,
|
||||
} from "./getContactChats";
|
||||
export {
|
||||
getLastInteraction,
|
||||
tool as getLastInteractionTool,
|
||||
} from "./getLastInteraction";
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { ResultWithChats } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { ResultWithChats } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'join_chat_by_link',
|
||||
description: 'Join a chat using an invite link',
|
||||
name: "join_chat_by_link",
|
||||
description: "Join a chat using an invite link",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
link: { type: 'string', description: 'Invite link (e.g. https://t.me/+HASH or https://t.me/joinchat/HASH)' },
|
||||
link: {
|
||||
type: "string",
|
||||
description:
|
||||
"Invite link (e.g. https://t.me/+HASH or https://t.me/joinchat/HASH)",
|
||||
},
|
||||
},
|
||||
required: ['link'],
|
||||
required: ["link"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,8 +27,12 @@ export async function joinChatByLink(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const link = typeof args.link === 'string' ? args.link : '';
|
||||
if (!link) return { content: [{ type: 'text', text: 'link is required' }], isError: true };
|
||||
const link = typeof args.link === "string" ? args.link : "";
|
||||
if (!link)
|
||||
return {
|
||||
content: [{ type: "text", text: "link is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
// Extract hash from link
|
||||
let hash = link;
|
||||
@@ -39,11 +47,12 @@ export async function joinChatByLink(
|
||||
return client.invoke(new Api.messages.ImportChatInvite({ hash }));
|
||||
});
|
||||
|
||||
const chatTitle = narrow<ResultWithChats>(result)?.chats?.[0]?.title ?? 'unknown';
|
||||
return { content: [{ type: 'text', text: `Joined chat: ${chatTitle}` }] };
|
||||
const chatTitle =
|
||||
narrow<ResultWithChats>(result)?.chats?.[0]?.title ?? "unknown";
|
||||
return { content: [{ type: "text", text: `Joined chat: ${chatTitle}` }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'join_chat_by_link',
|
||||
"join_chat_by_link",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.GROUP,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import bigInt from 'big-integer';
|
||||
import type { ApiUser } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import bigInt from "big-integer";
|
||||
import type { ApiUser } from "../apiResultTypes";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'list_contacts',
|
||||
description: 'List all contacts in your Telegram account',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
name: "list_contacts",
|
||||
description: "List all contacts in your Telegram account",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
export async function listContacts(
|
||||
@@ -23,25 +23,26 @@ export async function listContacts(
|
||||
return client.invoke(new Api.contacts.GetContacts({ hash: bigInt(0) }));
|
||||
});
|
||||
|
||||
if (!result || !('users' in result) || !Array.isArray(result.users)) {
|
||||
return { content: [{ type: 'text', text: 'No contacts found.' }] };
|
||||
if (!result || !("users" in result) || !Array.isArray(result.users)) {
|
||||
return { content: [{ type: "text", text: "No contacts found." }] };
|
||||
}
|
||||
|
||||
const lines = result.users.map((u: ApiUser) => {
|
||||
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
const username = u.username ? `@${u.username}` : '';
|
||||
const phone = u.phone ? `+${u.phone}` : '';
|
||||
const name =
|
||||
[u.firstName, u.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
const username = u.username ? `@${u.username}` : "";
|
||||
const phone = u.phone ? `+${u.phone}` : "";
|
||||
return `ID: ${u.id} | ${name} ${username} ${phone}`.trim();
|
||||
});
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No contacts found.' }] };
|
||||
return { content: [{ type: "text", text: "No contacts found." }] };
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'list_contacts',
|
||||
"list_contacts",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById, getMessages } from '../telegramApi';
|
||||
import type { MessageWithReplyMarkup, ReplyMarkupRow } from '../apiResultTypes';
|
||||
import { narrow } from '../apiCastHelpers';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById, getMessages } from "../telegramApi";
|
||||
import type { MessageWithReplyMarkup, ReplyMarkupRow } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "list_inline_buttons",
|
||||
@@ -24,44 +24,82 @@ export async function listInlineButtons(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const messages = await getMessages(chatId, 200, 0);
|
||||
if (!messages) return { content: [{ type: 'text', text: 'No messages found.' }] };
|
||||
if (!messages)
|
||||
return { content: [{ type: "text", text: "No messages found." }] };
|
||||
|
||||
const found = messages.find((m) => String(m.id) === String(messageId));
|
||||
const msg = found ? narrow<MessageWithReplyMarkup>(found) : undefined;
|
||||
if (!msg) return { content: [{ type: 'text', text: 'Message ' + messageId + ' not found in cache.' }], isError: true };
|
||||
if (!msg)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Message " + messageId + " not found in cache.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
if (!msg.replyMarkup || !msg.replyMarkup.rows) {
|
||||
return { content: [{ type: 'text', text: 'No inline buttons on message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No inline buttons on message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
msg.replyMarkup.rows.forEach((row: ReplyMarkupRow, ri: number) => {
|
||||
if (row.buttons) {
|
||||
row.buttons.forEach((btn, bi: number) => {
|
||||
lines.push('Row ' + ri + ', Button ' + bi + ': "' + (btn.text ?? '?') + '"');
|
||||
lines.push(
|
||||
"Row " + ri + ", Button " + bi + ': "' + (btn.text ?? "?") + '"',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (lines.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No inline buttons on message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No inline buttons on message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'list_inline_buttons',
|
||||
"list_inline_buttons",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { ForumTopicsResult } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { ForumTopicsResult } from "../apiResultTypes";
|
||||
import { toInputChannel, narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'list_topics',
|
||||
description: 'List topics in a forum chat',
|
||||
inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, required: ['chat_id'] },
|
||||
name: "list_topics",
|
||||
description: "List topics in a forum chat",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
},
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
export async function listTopics(
|
||||
@@ -19,13 +25,25 @@ export async function listTopics(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
if (chat.type !== 'channel' && chat.type !== 'supergroup') {
|
||||
return { content: [{ type: 'text', text: 'Forum topics are only available for channels/supergroups.' }], isError: true };
|
||||
if (chat.type !== "channel" && chat.type !== "supergroup") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Forum topics are only available for channels/supergroups.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
@@ -46,17 +64,17 @@ export async function listTopics(
|
||||
|
||||
const topics = narrow<ForumTopicsResult>(result)?.topics;
|
||||
if (!topics || !Array.isArray(topics) || topics.length === 0) {
|
||||
return { content: [{ type: 'text', text: 'No forum topics found.' }] };
|
||||
return { content: [{ type: "text", text: "No forum topics found." }] };
|
||||
}
|
||||
|
||||
const lines = topics.map((t) => {
|
||||
return 'ID: ' + t.id + ' | ' + (t.title ?? 'Untitled');
|
||||
return "ID: " + t.id + " | " + (t.title ?? "Untitled");
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'list_topics',
|
||||
"list_topics",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.GROUP,
|
||||
);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'mark_as_read',
|
||||
description: 'Mark messages as read in a chat',
|
||||
name: "mark_as_read",
|
||||
description: "Mark messages as read in a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { chat_id: { type: 'string', description: 'Chat ID or username' } },
|
||||
required: ['chat_id'],
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
},
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,11 +22,14 @@ export async function markAsRead(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -35,11 +40,13 @@ export async function markAsRead(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Messages in chat ${chatId} marked as read.` }],
|
||||
content: [
|
||||
{ type: "text", text: `Messages in chat ${chatId} marked as read.` },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'mark_as_read',
|
||||
"mark_as_read",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'mute_chat',
|
||||
description: 'Mute notifications for a chat',
|
||||
name: "mute_chat",
|
||||
description: "Mute notifications for a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
duration: { type: 'number', description: 'Mute duration in seconds (0 = forever)', default: 0 },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
duration: {
|
||||
type: "number",
|
||||
description: "Mute duration in seconds (0 = forever)",
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
required: ['chat_id'],
|
||||
required: ["chat_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,16 +28,21 @@ export async function muteChat(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const duration = typeof args.duration === 'number' ? args.duration : 0;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const duration = typeof args.duration === "number" ? args.duration : 0;
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
|
||||
const muteUntil = duration === 0 ? 2147483647 : Math.floor(Date.now() / 1000) + duration;
|
||||
const muteUntil =
|
||||
duration === 0 ? 2147483647 : Math.floor(Date.now() / 1000) + duration;
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
const inputPeer = await client.getInputEntity(entity);
|
||||
@@ -47,10 +56,10 @@ export async function muteChat(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: `Chat ${chatId} muted.` }] };
|
||||
return { content: [{ type: "text", text: `Chat ${chatId} muted.` }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'mute_chat',
|
||||
"mute_chat",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CHAT,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MCPToolResult } from '../../types';
|
||||
import type { MCPToolResult } from "../../types";
|
||||
|
||||
export function notImplemented(name: string): MCPToolResult {
|
||||
return {
|
||||
content: [{ type: 'text', text: `${name} is not implemented yet.` }],
|
||||
content: [{ type: "text", text: `${name} is not implemented yet.` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'pin_message',
|
||||
description: 'Pin a message in a chat',
|
||||
name: "pin_message",
|
||||
description: "Pin a message in a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,21 +23,27 @@ export async function pinMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -48,11 +54,16 @@ export async function pinMessage(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} pinned in chat ${chatId}.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Message ${messageId} pinned in chat ${chatId}.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'pin_message',
|
||||
"pin_message",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { BotCallbackAnswer } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { BotCallbackAnswer } from "../apiResultTypes";
|
||||
import { narrow } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
@@ -27,17 +27,33 @@ export async function pressInlineButton(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const data = typeof args.button_text === 'string' ? args.button_text : '';
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const data = typeof args.button_text === "string" ? args.button_text : "";
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (!data) return { content: [{ type: 'text', text: 'button_text is required' }], isError: true };
|
||||
if (!data)
|
||||
return {
|
||||
content: [{ type: "text", text: "button_text is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -48,16 +64,18 @@ export async function pressInlineButton(
|
||||
new Api.messages.GetBotCallbackAnswer({
|
||||
peer: inputPeer,
|
||||
msgId: messageId,
|
||||
data: Buffer.from(data, 'base64'),
|
||||
data: Buffer.from(data, "base64"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const answer = narrow<BotCallbackAnswer>(result)?.message ?? 'Button pressed (no response message).';
|
||||
return { content: [{ type: 'text', text: answer }] };
|
||||
const answer =
|
||||
narrow<BotCallbackAnswer>(result)?.message ??
|
||||
"Button pressed (no response message).";
|
||||
return { content: [{ type: "text", text: answer }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'press_inline_button',
|
||||
"press_inline_button",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'remove_reaction',
|
||||
description: 'Remove a reaction from a message',
|
||||
name: "remove_reaction",
|
||||
description: "Remove a reaction from a message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
reaction: { type: 'string', description: 'Reaction to remove' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
reaction: { type: "string", description: "Reaction to remove" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,15 +25,27 @@ export async function removeReaction(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -49,10 +61,17 @@ export async function removeReaction(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Reaction removed from message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reaction removed from message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'remove_reaction',
|
||||
"remove_reaction",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -34,21 +34,22 @@ export async function replyToMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const text = typeof args.text === 'string' ? args.text : '';
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const text = typeof args.text === "string" ? args.text : "";
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be an integer' }],
|
||||
content: [{ type: "text", text: "message_id must be an integer" }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
if (!text) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'text is required' }],
|
||||
content: [{ type: "text", text: "text is required" }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export async function replyToMessage(
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
type: "text",
|
||||
text: `Failed to reply to message ${messageId} in chat ${chatId}.`,
|
||||
},
|
||||
],
|
||||
@@ -77,7 +78,7 @@ export async function replyToMessage(
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
type: "text",
|
||||
text: `Replied to message ${messageId} in chat ${chatId}.`,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "save_draft",
|
||||
@@ -24,12 +24,20 @@ export async function saveDraft(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const text = typeof args.text === 'string' ? args.text : '';
|
||||
if (!text) return { content: [{ type: 'text', text: 'text is required' }], isError: true };
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const text = typeof args.text === "string" ? args.text : "";
|
||||
if (!text)
|
||||
return {
|
||||
content: [{ type: "text", text: "text is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -44,10 +52,12 @@ export async function saveDraft(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Draft saved in chat ' + chatId + '.' }] };
|
||||
return {
|
||||
content: [{ type: "text", text: "Draft saved in chat " + chatId + "." }],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'save_draft',
|
||||
"save_draft",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.DRAFT,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { optNumber } from '../args';
|
||||
import type { ApiUser } from '../apiResultTypes';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { optNumber } from "../args";
|
||||
import type { ApiUser } from "../apiResultTypes";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'search_contacts',
|
||||
description: 'Search contacts by name or username',
|
||||
name: "search_contacts",
|
||||
description: "Search contacts by name or username",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
limit: { type: 'number', description: 'Max results', default: 20 },
|
||||
query: { type: "string", description: "Search query" },
|
||||
limit: { type: "number", description: "Max results", default: 20 },
|
||||
},
|
||||
required: ['query'],
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,31 +24,42 @@ export async function searchContacts(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const query = typeof args.query === 'string' ? args.query : '';
|
||||
const query = typeof args.query === "string" ? args.query : "";
|
||||
if (!query) {
|
||||
return { content: [{ type: 'text', text: 'query is required' }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "query is required" }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
const limit = optNumber(args, 'limit', 20);
|
||||
const limit = optNumber(args, "limit", 20);
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const result = await mtprotoService.withFloodWaitHandling(async () => {
|
||||
return client.invoke(new Api.contacts.Search({ q: query, limit }));
|
||||
});
|
||||
|
||||
if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) {
|
||||
return { content: [{ type: 'text', text: `No contacts found for "${query}".` }] };
|
||||
if (
|
||||
!result ||
|
||||
!("users" in result) ||
|
||||
!Array.isArray(result.users) ||
|
||||
result.users.length === 0
|
||||
) {
|
||||
return {
|
||||
content: [{ type: "text", text: `No contacts found for "${query}".` }],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = result.users.map((u: ApiUser) => {
|
||||
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown';
|
||||
const username = u.username ? `@${u.username}` : '';
|
||||
const name =
|
||||
[u.firstName, u.lastName].filter(Boolean).join(" ") || "Unknown";
|
||||
const username = u.username ? `@${u.username}` : "";
|
||||
return `ID: ${u.id} | ${name} ${username}`.trim();
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'search_contacts',
|
||||
"search_contacts",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'send_reaction',
|
||||
description: 'Send a reaction to a message',
|
||||
name: "send_reaction",
|
||||
description: "Send a reaction to a message",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
reaction: { type: 'string', description: 'Reaction emoji' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
reaction: { type: "string", description: "Reaction emoji" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,16 +25,29 @@ export async function sendReaction(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id) ? args.message_id : undefined;
|
||||
const emoji = typeof args.reaction === 'string' ? args.reaction : '\ud83d\udc4d';
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const emoji =
|
||||
typeof args.reaction === "string" ? args.reaction : "\ud83d\udc4d";
|
||||
|
||||
if (messageId === undefined) {
|
||||
return { content: [{ type: 'text', text: 'message_id must be a positive integer' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: 'Chat not found: ' + chatId }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: "Chat not found: " + chatId }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -50,10 +63,17 @@ export async function sendReaction(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Reaction ' + emoji + ' sent to message ' + messageId + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Reaction " + emoji + " sent to message " + messageId + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'send_reaction',
|
||||
"send_reaction",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { BotCommandInput } from '../apiResultTypes';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import type { BotCommandInput } from "../apiResultTypes";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "set_bot_commands",
|
||||
@@ -24,28 +24,43 @@ export async function setBotCommands(
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const cmds = Array.isArray(args.commands) ? args.commands : [];
|
||||
if (cmds.length === 0) return { content: [{ type: 'text', text: 'commands array is required' }], isError: true };
|
||||
if (cmds.length === 0)
|
||||
return {
|
||||
content: [{ type: "text", text: "commands array is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
const botCommands = cmds.map((c: BotCommandInput) =>
|
||||
new Api.BotCommand({ command: String(c.command ?? ''), description: String(c.description ?? '') }),
|
||||
const botCommands = cmds.map(
|
||||
(c: BotCommandInput) =>
|
||||
new Api.BotCommand({
|
||||
command: String(c.command ?? ""),
|
||||
description: String(c.description ?? ""),
|
||||
}),
|
||||
);
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
await client.invoke(
|
||||
new Api.bots.SetBotCommands({
|
||||
scope: new Api.BotCommandScopeDefault(),
|
||||
langCode: '',
|
||||
langCode: "",
|
||||
commands: botCommands,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Bot commands updated: ' + cmds.length + ' commands.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Bot commands updated: " + cmds.length + " commands.",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'set_bot_commands',
|
||||
"set_bot_commands",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: "set_privacy_settings",
|
||||
@@ -22,11 +22,19 @@ export async function setPrivacySettings(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const keyStr = typeof args.setting === 'string' ? args.setting : '';
|
||||
const ruleStr = typeof args.value === 'string' ? args.value : '';
|
||||
const keyStr = typeof args.setting === "string" ? args.setting : "";
|
||||
const ruleStr = typeof args.value === "string" ? args.value : "";
|
||||
|
||||
if (!keyStr) return { content: [{ type: 'text', text: 'setting is required' }], isError: true };
|
||||
if (!ruleStr) return { content: [{ type: 'text', text: 'value is required' }], isError: true };
|
||||
if (!keyStr)
|
||||
return {
|
||||
content: [{ type: "text", text: "setting is required" }],
|
||||
isError: true,
|
||||
};
|
||||
if (!ruleStr)
|
||||
return {
|
||||
content: [{ type: "text", text: "value is required" }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
@@ -41,7 +49,10 @@ export async function setPrivacySettings(
|
||||
|
||||
const key = keyMap[keyStr];
|
||||
if (!key) {
|
||||
return { content: [{ type: 'text', text: 'Unknown privacy key: ' + keyStr }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: "Unknown privacy key: " + keyStr }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const ruleMap: Record<string, Api.TypeInputPrivacyRule> = {
|
||||
@@ -52,17 +63,35 @@ export async function setPrivacySettings(
|
||||
|
||||
const rule = ruleMap[ruleStr];
|
||||
if (!rule) {
|
||||
return { content: [{ type: 'text', text: 'Unknown rule: ' + ruleStr + '. Valid: allow_all, allow_contacts, disallow_all' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
"Unknown rule: " +
|
||||
ruleStr +
|
||||
". Valid: allow_all, allow_contacts, disallow_all",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
await client.invoke(new Api.account.SetPrivacy({ key, rules: [rule] }));
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: 'Privacy setting ' + keyStr + ' set to ' + ruleStr + '.' }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Privacy setting " + keyStr + " set to " + ruleStr + ".",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'set_privacy_settings',
|
||||
"set_privacy_settings",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -7,9 +7,9 @@ export const tool: MCPTool = {
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
file_path: { type: 'string', description: 'Path to the photo file' },
|
||||
file_path: { type: "string", description: "Path to the photo file" },
|
||||
},
|
||||
required: ['file_path'],
|
||||
required: ["file_path"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,7 +18,12 @@ export async function setProfilePhoto(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'set_profile_photo requires file upload which is not supported via MCP text interface. Use the Telegram client directly.' }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "set_profile_photo requires file upload which is not supported via MCP text interface. Use the Telegram client directly.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { toInputChannel, toInputPeer } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { toInputChannel, toInputPeer } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'unban_user',
|
||||
description: 'Unban a user from a group or channel',
|
||||
name: "unban_user",
|
||||
description: "Unban a user from a group or channel",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
user_id: { type: 'string', description: 'User ID to unban' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
user_id: { type: "string", description: "User ID to unban" },
|
||||
},
|
||||
required: ['chat_id', 'user_id'],
|
||||
required: ["chat_id", "user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,14 +25,26 @@ export async function unbanUser(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
if (!chat)
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
|
||||
if (chat.type !== 'channel' && chat.type !== 'supergroup') {
|
||||
return { content: [{ type: 'text', text: 'Unban is only available for channels/supergroups.' }], isError: true };
|
||||
if (chat.type !== "channel" && chat.type !== "supergroup") {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Unban is only available for channels/supergroups.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
@@ -50,10 +62,17 @@ export async function unbanUser(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: `User ${userId} unbanned from ${chat.title ?? chatId}.` }] };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User ${userId} unbanned from ${chat.title ?? chatId}.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'unban_user',
|
||||
"unban_user",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.ADMIN,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { toInputPeer } from '../apiCastHelpers';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { toInputPeer } from "../apiCastHelpers";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'unblock_user',
|
||||
description: 'Unblock a user',
|
||||
name: "unblock_user",
|
||||
description: "Unblock a user",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: { type: 'string', description: 'User ID to unblock' },
|
||||
user_id: { type: "string", description: "User ID to unblock" },
|
||||
},
|
||||
required: ['user_id'],
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function unblockUser(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const userId = validateId(args.user_id, 'user_id');
|
||||
const userId = validateId(args.user_id, "user_id");
|
||||
const client = mtprotoService.getClient();
|
||||
|
||||
await mtprotoService.withFloodWaitHandling(async () => {
|
||||
@@ -33,10 +33,10 @@ export async function unblockUser(
|
||||
);
|
||||
});
|
||||
|
||||
return { content: [{ type: 'text', text: `User ${userId} unblocked.` }] };
|
||||
return { content: [{ type: "text", text: `User ${userId} unblocked.` }] };
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'unblock_user',
|
||||
"unblock_user",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.CONTACT,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { getChatById } from '../telegramApi';
|
||||
import { validateId } from '../../validation';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { getChatById } from "../telegramApi";
|
||||
import { validateId } from "../../validation";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'unpin_message',
|
||||
description: 'Unpin a message in a chat',
|
||||
name: "unpin_message",
|
||||
description: "Unpin a message in a chat",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID or username' },
|
||||
message_id: { type: 'number', description: 'Message ID' },
|
||||
chat_id: { type: "string", description: "Chat ID or username" },
|
||||
message_id: { type: "number", description: "Message ID" },
|
||||
},
|
||||
required: ['chat_id', 'message_id'],
|
||||
required: ["chat_id", "message_id"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,21 +24,27 @@ export async function unpinMessage(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const chatId = validateId(args.chat_id, 'chat_id');
|
||||
const messageId = typeof args.message_id === 'number' && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
const chatId = validateId(args.chat_id, "chat_id");
|
||||
const messageId =
|
||||
typeof args.message_id === "number" && Number.isInteger(args.message_id)
|
||||
? args.message_id
|
||||
: undefined;
|
||||
|
||||
if (messageId === undefined) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'message_id must be a positive integer' }],
|
||||
content: [
|
||||
{ type: "text", text: "message_id must be a positive integer" },
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const chat = getChatById(chatId);
|
||||
if (!chat) {
|
||||
return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true };
|
||||
return {
|
||||
content: [{ type: "text", text: `Chat not found: ${chatId}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entity = chat.username ? chat.username : chat.id;
|
||||
@@ -56,11 +62,16 @@ export async function unpinMessage(
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: `Message ${messageId} unpinned in chat ${chatId}.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Message ${messageId} unpinned in chat ${chatId}.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'unpin_message',
|
||||
"unpin_message",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.MSG,
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { MCPTool, MCPToolResult } from '../../types';
|
||||
import type { TelegramMCPContext } from '../types';
|
||||
import { ErrorCategory, logAndFormatError } from '../../errorHandler';
|
||||
import { mtprotoService } from '../../../../services/mtprotoService';
|
||||
import { Api } from 'telegram';
|
||||
import { optString } from '../args';
|
||||
import type { MCPTool, MCPToolResult } from "../../types";
|
||||
import type { TelegramMCPContext } from "../types";
|
||||
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
|
||||
import { mtprotoService } from "../../../../services/mtprotoService";
|
||||
import { Api } from "telegram";
|
||||
import { optString } from "../args";
|
||||
|
||||
export const tool: MCPTool = {
|
||||
name: 'update_profile',
|
||||
description: 'Update your Telegram profile',
|
||||
name: "update_profile",
|
||||
description: "Update your Telegram profile",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
first_name: { type: 'string', description: 'New first name' },
|
||||
last_name: { type: 'string', description: 'New last name' },
|
||||
about: { type: 'string', description: 'New bio/about text' },
|
||||
first_name: { type: "string", description: "New first name" },
|
||||
last_name: { type: "string", description: "New last name" },
|
||||
about: { type: "string", description: "New bio/about text" },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -23,12 +23,20 @@ export async function updateProfile(
|
||||
_context: TelegramMCPContext,
|
||||
): Promise<MCPToolResult> {
|
||||
try {
|
||||
const firstName = optString(args, 'first_name');
|
||||
const lastName = optString(args, 'last_name');
|
||||
const about = optString(args, 'about');
|
||||
const firstName = optString(args, "first_name");
|
||||
const lastName = optString(args, "last_name");
|
||||
const about = optString(args, "about");
|
||||
|
||||
if (!firstName && !lastName && !about) {
|
||||
return { content: [{ type: 'text', text: 'At least one of first_name, last_name, or about is required.' }], isError: true };
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "At least one of first_name, last_name, or about is required.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const client = mtprotoService.getClient();
|
||||
@@ -44,14 +52,18 @@ export async function updateProfile(
|
||||
});
|
||||
|
||||
const updates: string[] = [];
|
||||
if (firstName) updates.push('first_name: ' + firstName);
|
||||
if (lastName) updates.push('last_name: ' + lastName);
|
||||
if (about) updates.push('about: ' + about);
|
||||
if (firstName) updates.push("first_name: " + firstName);
|
||||
if (lastName) updates.push("last_name: " + lastName);
|
||||
if (about) updates.push("about: " + about);
|
||||
|
||||
return { content: [{ type: 'text', text: 'Profile updated: ' + updates.join(', ') }] };
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "Profile updated: " + updates.join(", ") },
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return logAndFormatError(
|
||||
'update_profile',
|
||||
"update_profile",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
ErrorCategory.PROFILE,
|
||||
);
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
* Handles communication between frontend MCP server and backend MCP client
|
||||
*/
|
||||
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import type { MCPRequest, MCPResponse, SocketIOMCPTransport } from './types';
|
||||
import { mcpWarn } from './logger';
|
||||
import type { Socket } from "socket.io-client";
|
||||
import type { MCPRequest, MCPResponse, SocketIOMCPTransport } from "./types";
|
||||
import { mcpWarn } from "./logger";
|
||||
|
||||
export class SocketIOMCPTransportImpl implements SocketIOMCPTransport {
|
||||
private socket: Socket | null | undefined;
|
||||
private requestHandlers = new Map<string | number, (response: MCPResponse) => void>();
|
||||
private readonly eventPrefix = 'mcp:';
|
||||
private requestHandlers = new Map<
|
||||
string | number,
|
||||
(response: MCPResponse) => void
|
||||
>();
|
||||
private readonly eventPrefix = "mcp:";
|
||||
private responseHandler = (response: MCPResponse): void => {
|
||||
const handler = this.requestHandlers.get(response.id);
|
||||
if (handler) {
|
||||
@@ -35,7 +38,7 @@ export class SocketIOMCPTransportImpl implements SocketIOMCPTransport {
|
||||
|
||||
emit(event: string, data: unknown): void {
|
||||
if (!this.socket?.connected) {
|
||||
mcpWarn('Cannot emit MCP event: socket not connected', { event });
|
||||
mcpWarn("Cannot emit MCP event: socket not connected", { event });
|
||||
return;
|
||||
}
|
||||
this.socket.emit(`${this.eventPrefix}${event}`, data);
|
||||
@@ -53,7 +56,7 @@ export class SocketIOMCPTransportImpl implements SocketIOMCPTransport {
|
||||
|
||||
async request(request: MCPRequest, timeoutMs = 30000): Promise<MCPResponse> {
|
||||
if (!this.socket?.connected) {
|
||||
throw new Error('Socket not connected');
|
||||
throw new Error("Socket not connected");
|
||||
}
|
||||
|
||||
return new Promise<MCPResponse>((resolve, reject) => {
|
||||
@@ -71,7 +74,7 @@ export class SocketIOMCPTransportImpl implements SocketIOMCPTransport {
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('request', request);
|
||||
this.emit("request", request);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user