Skip to content

Latest commit

 

History

History
2720 lines (2289 loc) · 76.4 KB

Project-code.md

File metadata and controls

2720 lines (2289 loc) · 76.4 KB

Directory Structure:

└── ./ ├── .vercel │ └── project.json ├── src │ ├── app │ │ ├── api │ │ │ ├── auth │ │ │ │ └── telegram │ │ │ │ └── route.js │ │ │ └── extract-data │ │ │ └── route.js │ │ ├── contacts │ │ │ └── page.js │ │ ├── contacts-list │ │ │ └── page.jsx │ │ ├── dashboard │ │ │ └── page.js │ │ ├── groups │ │ │ └── page.js │ │ ├── groups-list │ │ │ └── page.jsx │ │ ├── global.css │ │ ├── layout.js │ │ └── page.js │ ├── components │ │ ├── ui │ │ │ ├── alert.jsx │ │ │ ├── avatar.jsx │ │ │ ├── badge.jsx │ │ │ ├── button.jsx │ │ │ ├── card.jsx │ │ │ ├── checkbox.jsx │ │ │ ├── input.jsx │ │ │ ├── label.jsx │ │ │ ├── radio-group.jsx │ │ │ ├── table.jsx │ │ │ └── tabs.jsx │ │ ├── ClientTelegramManager.js │ │ ├── ContactsList.jsx │ │ ├── GroupsList.jsx │ │ └── TelegramManager.jsx │ └── lib │ ├── apiUtils.js │ ├── csvUtils.js │ ├── supabase.js │ └── utils.js ├── utils │ └── config.js ├── .eslintrc.js ├── .gitignore ├── .vercelignore ├── components.json ├── jsconfig.json ├── next.config.js ├── package.json ├── postcss.config.js ├── route-working.js ├── tailwind.config.js ├── TelegramManager-working.jsx └── vercel.json


File: /.vercel/project.json

{ "projectId": "prj_367vtzPiZn5QirP92psBaoa7hv06", "orgId": "team_ArZ7x4AdcyPVl4hDW9jRBNul", "settings": { "createdAt": 1727471692304, "framework": null, "devCommand": null, "installCommand": null, "buildCommand": null, "outputDirectory": null, "rootDirectory": null, "directoryListing": false, "nodeVersion": "20.x" } }


File: /src/app/api/auth/telegram/route.js

import { NextResponse } from 'next/server'; import { createClient } from '@supabase/supabase-js'; import crypto from 'crypto';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabase = createClient(supabaseUrl, supabaseKey);

export async function POST(req) { try { const { id, first_name, last_name, username, photo_url, auth_date, hash } = await req.json(); const botToken = process.env.TELEGRAM_BOT_TOKEN;

// Check authenticity of the request
const checkString = Object.entries({ auth_date, first_name, id, username })
  .sort()
  .map(([key, value]) => `${key}=${value}`)
  .join('\n');

const secret = crypto.createHash('sha256').update(botToken).digest();
const expectedHash = crypto.createHmac('sha256', secret).update(checkString).digest('hex');

if (expectedHash !== hash) {
  return NextResponse.json({ error: 'Invalid data from Telegram' }, { status: 400 });
}

// Upsert user into Supabase
const { data: user, error } = await supabase
  .from('users')
  .upsert({
    telegram_id: id,
    first_name,
    last_name,
    username,
    photo_url,
  })
  .select()
  .single();

if (error) {
  console.error('Supabase error:', error);
  return NextResponse.json({ error: 'Database error' }, { status: 500 });
}

return NextResponse.json({ success: true, user });

} catch (error) { console.error('Server error:', error); return NextResponse.json({ error: 'Server error' }, { status: 500 }); } }


File: /src/app/api/extract-data/route.js

import { NextResponse } from 'next/server'; import { TelegramClient } from 'telegram'; import { StringSession } from 'telegram/sessions'; import { Api } from 'telegram/tl'; import { checkRateLimit, handleTelegramError, handleErrorResponse } from '@/lib/apiUtils'; import { createClient } from '@supabase/supabase-js';

// Initialize Supabase client const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabase = createClient(supabaseUrl, supabaseKey);

const CODE_EXPIRATION_TIME = 30 * 60 * 1000; // 30 minutes in milliseconds

// Persistent TelegramClient instance let persistentClient;

async function getPersistentClient(apiId, apiHash, session = '') { if (!persistentClient) { const stringSession = new StringSession(session); persistentClient = new TelegramClient(stringSession, parseInt(apiId), apiHash, { connectionRetries: 3, useWSS: true, timeout: 15000, // Reduced timeout to 15 seconds dev: false, }); try { await Promise.race([ persistentClient.connect(), new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), 20000)) ]); } catch (error) { console.error('[CONNECTION ERROR]:', error); persistentClient = null; throw error; } } return persistentClient; }

export async function POST(req) { try { console.log('[START]: Handling API Request'); let body; try { body = await req.json(); } catch (error) { console.error('[ERROR]: Failed to parse request body', error); return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); }

const { apiId, apiHash, phoneNumber, extractType, validationCode, action } = body;

console.log('[DEBUG]: Received payload:', { 
  apiId, apiHash, phoneNumber, extractType, action,
  validationCode: validationCode ? 'Provided' : 'Not provided',
});

// Check for existing session
if (action === 'checkSession') {
  const { data: userData, error } = await supabase
    .from('users')
    .select('session_string')
    .eq('phone_number', phoneNumber)
    .single();

  if (userData?.session_string) {
    return NextResponse.json({ hasSession: true });
  } else {
    return NextResponse.json({ hasSession: false });
  }
}

// Input Validation
if (!apiId || isNaN(apiId) || parseInt(apiId) <= 0) {
  return handleErrorResponse('API ID is invalid or missing. Please provide a valid positive number.', 400);
}
if (!apiHash || !/^[a-f0-9]{32}$/.test(apiHash)) {
  return handleErrorResponse('API Hash is invalid. It should be a 32-character hexadecimal string.', 400);
}
if (!phoneNumber || typeof phoneNumber !== 'string' || phoneNumber.trim() === '') {
  return handleErrorResponse('Phone number is missing or invalid. Please enter a valid phone number.', 400);
}

const validPhoneNumber = phoneNumber.trim();
console.log('[DEBUG]: Valid phone number:', validPhoneNumber);

checkRateLimit();

// Retrieve user data from Supabase
const { data: userData, error: fetchError } = await supabase
  .from('users')
  .select('id, session_string, phoneCodeHash, code_request_time, phone_registered')
  .eq('phone_number', validPhoneNumber)
  .single();

if (fetchError && fetchError.code !== 'PGRST116') {
  console.error('[FETCH ERROR]:', fetchError);
  return handleErrorResponse('Error retrieving user data. Please try again.', 500);
}

try {
  const client = await getPersistentClient(apiId, apiHash, userData?.session_string || '');

  if (!client.connected) {
    throw new Error('Failed to connect to Telegram');
  }

  // If we have a session and an extract type, proceed with extraction
  if (userData?.session_string && extractType) {
    return await handleDataExtraction(client, validPhoneNumber, extractType, userData.id);
  }

  // Step 1: Request validation code if not provided
  if (!validationCode) {
    return await handleCodeRequest(client, apiId, apiHash, validPhoneNumber);
  }

  // Step 2: Sign in or Sign up with the provided validation code
  return await handleSignInOrSignUp(client, validPhoneNumber, userData, validationCode, extractType);

} catch (error) {
  console.error('[TELEGRAM CONNECTION ERROR]:', error);
  return NextResponse.json({ 
    error: 'Failed to connect to Telegram servers. Please try again later.',
    details: error.message
  }, { status: 503 });
}

} catch (error) { console.error('[GENERAL API ERROR]: Error in extract-data API:', error); return handleErrorResponse('An unexpected error occurred. Please try again later.', 500, error); } }

async function handleCodeRequest(client, apiId, apiHash, phoneNumber) { console.log('[PROCESS]: Requesting validation code'); try { const result = await client.sendCode( { apiId: parseInt(apiId), apiHash: apiHash, }, phoneNumber ); console.log('[SUCCESS]: Validation code requested successfully');

const userData = {
  phone_number: phoneNumber,
  api_id: parseInt(apiId),
  api_hash: apiHash,
  phoneCodeHash: result.phoneCodeHash,
  code_request_time: new Date().toISOString(),
  phone_registered: result.phone_registered !== false
};

const { error: upsertError } = await supabase
  .from('users')
  .upsert(userData)
  .eq('phone_number', phoneNumber);

if (upsertError) {
  console.error('[UPSERT ERROR]:', upsertError);
  throw upsertError;
}

console.log('[DEBUG]: Updated user with phoneCodeHash and code_request_time');

return NextResponse.json({
  success: true,
  message: 'Validation code sent to your Telegram app. Please provide it in the next step.',
  requiresValidation: true,
  phoneRegistered: result.phone_registered !== false
});

} catch (error) { console.error('[SEND CODE ERROR]:', error); return handleTelegramError(error); } }

async function handleSignInOrSignUp(client, phoneNumber, userData, validationCode, extractType) { console.log('[PROCESS]: Attempting to sign in/up with provided code'); try { const { phoneCodeHash, code_request_time: codeRequestTime, phone_registered: phoneRegistered } = userData;

if (!phoneCodeHash || !codeRequestTime) {
  return handleErrorResponse('Validation code not requested or expired. Please request a new code.', 400);
}

// Check if the code has expired
const codeRequestDate = new Date(codeRequestTime).getTime();
const currentTime = Date.now();
if ((currentTime - codeRequestDate) > CODE_EXPIRATION_TIME) {
  return NextResponse.json({
    success: false,
    message: 'The verification code has expired. Please request a new code.',
    code: 'PHONE_CODE_EXPIRED'
  });
}

let signInResult;
if (phoneRegistered) {
  signInResult = await client.invoke(new Api.auth.SignIn({
    phoneNumber: phoneNumber,
    phoneCodeHash: phoneCodeHash,
    phoneCode: validationCode
  }));
} else {
  signInResult = await client.invoke(new Api.auth.SignUp({
    phoneNumber: phoneNumber,
    phoneCodeHash: phoneCodeHash,
    phoneCode: validationCode,
    firstName: 'New',
    lastName: 'User'
  }));
}

if (!signInResult.user) {
  throw new Error('Failed to sign in/up. Please check your validation code and try again.');
}

console.log('[SUCCESS]: Signed in/up successfully');

// Save the session string
const sessionString = client.session.save();
await supabase
  .from('users')
  .update({ session_string: sessionString, phone_registered: true, phoneCodeHash: null, code_request_time: null })
  .eq('phone_number', phoneNumber);

if (extractType) {
  return await handleDataExtraction(client, phoneNumber, extractType, userData.id);
}

return NextResponse.json({
  success: true,
  message: 'Authentication successful',
});

} catch (error) { console.error('[SIGN IN/UP ERROR]:', error); if (error.errorMessage === 'PHONE_CODE_EXPIRED') { return NextResponse.json({ success: false, message: 'The verification code has expired. Please request a new code.', code: 'PHONE_CODE_EXPIRED' }); } return handleTelegramError(error); } }

async function handleDataExtraction(client, phoneNumber, extractType, userId) { let extractedData = []; try { // First, get the user's ID from the users table const { data: userData, error: userError } = await supabase .from('users') .select('id') .eq('phone_number', phoneNumber) .single();

if (userError) throw userError;

const ownerUserId = userData.id;

if (extractType === 'groups') {
  const dialogs = await client.getDialogs({limit: 50});
  extractedData = dialogs.map(dialog => ({
    group_name: dialog.title,
    group_id: dialog.id.toString(),
    participant_count: dialog.participantsCount || 0,
    type: dialog.isChannel ? 'channel' : 'group',
    is_public: !!dialog.username,
    owner_id: ownerUserId, // Use the user's ID from the users table
    creation_date: new Date().toISOString(),
    description: dialog.about || '',
    invite_link: dialog.inviteLink || '',
  }));
} else if (extractType === 'contacts') {
  const result = await client.invoke(new Api.contacts.GetContacts({
    hash: 0
  }));
  extractedData = result.users.map(user => ({
    user_id: user.id.toString(),
    first_name: user.firstName || '',
    last_name: user.lastName || '',
    username: user.username || '',
    phone_number: user.phone || '',
    owner_id: ownerUserId, // Use the user's ID from the users table
    extracted_at: new Date().toISOString(),
  }));
} else {
  throw new Error('Invalid extract type specified');
}

console.log(`[DEBUG]: Extracted ${extractedData.length} ${extractType}`);

// Insert extracted data into Supabase
const { error: insertError } = await supabase
  .from(extractType)
  .insert(extractedData);

if (insertError) {
  console.error('[INSERT ERROR]:', insertError);
  throw insertError;
}

return NextResponse.json({
  success: true,
  message: `${extractType} extracted successfully`,
  data: extractedData,
});

} catch (error) { console.error('[DATA EXTRACTION ERROR]:', error); return handleErrorResponse(Failed to extract ${extractType}. Please try again., 500); } }

// Route segment config export const runtime = 'nodejs'; export const dynamic = 'force-dynamic';


File: /src/app/contacts/page.js

'use client'

import ContactsList from '@/components/ContactsList'

export default function ContactsPage() { return (

Telegram Contacts

) }


File: /src/app/contacts-list/page.jsx

import React from 'react';

const ContactsListPage = () => { return (

Contacts List

This page will display the extracted contacts.

); };

export default ContactsListPage;


File: /src/app/dashboard/page.js

'use client'

import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { supabase } from '@/lib/supabase';

export default function Dashboard() { const [user, setUser] = useState(null); const router = useRouter();

useEffect(() => { const checkUser = async () => { const { data: { user } } = await supabase.auth.getUser(); if (user) { setUser(user); } else { router.push('/'); } };

checkUser();

}, [router]);

if (!user) { return

Loading...
; }

return (

Welcome, {user.username || user.first_name}!

{/* Add dashboard content here */}
); }


File: /src/app/groups/page.js

'use client'

import GroupsList from '@/components/GroupsList'

export default function GroupsPage() { return (

Telegram Groups

) }


File: /src/app/groups-list/page.jsx

import React from 'react'; import { supabase } from '@/lib/apiUtils';

const GroupsList = async () => { const { data: groups, error } = await supabase.from('groups').select('*');

if (error) { console.error('Error fetching groups:', error); return

Error loading groups
; }

return (

Groups List

    {groups.map((group) => (
  • {group.name}
  • ))}
); };

export default GroupsList;


File: /src/app/global.css

@tailwind base; @tailwind components; @tailwind utilities;


File: /src/app/layout.js

import './global.css'

export const metadata = { title: 'Telegram Groups and Contacts Extractor', description: 'Extract Telegram Groups and Contacts easily', }

export default function RootLayout({ children }) { return ( {children} ) }


File: /src/app/page.js

'use client'

import TelegramManager from '../components/TelegramManager';

export default function Home() { return (

Telegram Extractor

) }


File: /src/components/ui/alert.jsx

import * as React from "react"; import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils";

const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", { variants: { variant: { default: "bg-background text-foreground", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, }, defaultVariants: { variant: "default", }, } );

const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (

)); Alert.displayName = "Alert";

const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (

)); AlertTitle.displayName = "AlertTitle";

const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (

)); AlertDescription.displayName = "AlertDescription";

export { Alert, AlertTitle, AlertDescription };


File: /src/components/ui/avatar.jsx

import * as React from "react"

export const Avatar = ({ children }) =>

{children}
export const AvatarImage = ({ src, alt }) => {alt} export const AvatarFallback = ({ children }) =>
{children}


File: /src/components/ui/badge.jsx

import * as React from "react"

export const Badge = ({ children, variant = "default" }) => { const variants = { default: "bg-gray-200 text-gray-800", success: "bg-green-200 text-green-800", error: "bg-red-200 text-red-800", }

return ( <span className={inline-flex items-center px-2 py-1 rounded ${variants[variant]}}> {children} ) }


File: /src/components/ui/button.jsx

import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils";

const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", }, }, defaultVariants: { variant: "default", size: "default", }, } );

const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( <Comp className={cn(buttonVariants({ variant, size }), className)} ref={ref} {...props} /> ); } );

Button.displayName = "Button";

export { Button, buttonVariants };


File: /src/components/ui/card.jsx

import * as React from "react" import PropTypes from "prop-types" import { cn } from "@/lib/utils"

const Card = React.forwardRef(({ className, ...props }, ref) => (

)) Card.displayName = "Card"

const CardHeader = React.forwardRef(({ className, ...props }, ref) => (

)) CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef(({ className, ...props }, ref) => (

)) CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef(({ className, ...props }, ref) => (

)) CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef(({ className, ...props }, ref) => (

)) CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef(({ className, ...props }, ref) => (

)) CardFooter.displayName = "CardFooter"

// PropTypes const sharedPropTypes = { className: PropTypes.string, }

Card.propTypes = sharedPropTypes CardHeader.propTypes = sharedPropTypes CardTitle.propTypes = sharedPropTypes CardDescription.propTypes = sharedPropTypes CardContent.propTypes = sharedPropTypes CardFooter.propTypes = sharedPropTypes

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }


File: /src/components/ui/checkbox.jsx

import * as React from "react" import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { CheckIcon } from "@radix-ui/react-icons" import PropTypes from "prop-types" import { cn } from "@/lib/utils"

const Checkbox = React.forwardRef(({ className, ...props }, ref) => ( <CheckboxPrimitive.Root ref={ref} className={cn( "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", className )} {...props}

<CheckboxPrimitive.Indicator
  className={cn("flex items-center justify-center text-current")}
>
  <CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>

</CheckboxPrimitive.Root> ))

Checkbox.displayName = CheckboxPrimitive.Root.displayName

Checkbox.propTypes = { className: PropTypes.string, }

export { Checkbox }


File: /src/components/ui/input.jsx

import * as React from "react" import PropTypes from "prop-types" import { cn } from "@/lib/utils"

const Input = React.forwardRef(({ className, type, ...props }, ref) => { return ( <input type={type} className={cn( "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className )} ref={ref} {...props} /> ); });

Input.displayName = "Input";

Input.propTypes = { className: PropTypes.string, type: PropTypes.string, };

export { Input };


File: /src/components/ui/label.jsx

import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { cva } from "class-variance-authority" import PropTypes from "prop-types"

import { cn } from "@/lib/utils"

const labelVariants = cva( "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" )

const Label = React.forwardRef(({ className, ...props }, ref) => ( <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> ))

Label.displayName = LabelPrimitive.Root.displayName

Label.propTypes = { className: PropTypes.string, // Add other prop types as needed }

export { Label }


File: /src/components/ui/radio-group.jsx

import * as React from "react"; import { CheckIcon } from "@radix-ui/react-icons"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import PropTypes from "prop-types"; import { cn } from "@/lib/utils";

const RadioGroup = React.forwardRef(({ className, ...props }, ref) => { return ( <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} /> ); });

RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;

RadioGroup.propTypes = { className: PropTypes.string, };

const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => { return ( <RadioGroupPrimitive.Item ref={ref} className={cn( "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className )} {...props} > <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> </RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Item> ); });

RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;

RadioGroupItem.propTypes = { className: PropTypes.string, };

export { RadioGroup, RadioGroupItem };


File: /src/components/ui/table.jsx

import * as React from "react"; import { cn } from "@/lib/utils";

const Table = React.forwardRef(({ className, ...props }, ref) => (

)); Table.displayName = "Table";

const TableHeader = React.forwardRef(({ className, ...props }, ref) => (

)); TableHeader.displayName = "TableHeader";

const TableBody = React.forwardRef(({ className, ...props }, ref) => (

)); TableBody.displayName = "TableBody";

const TableFooter = React.forwardRef(({ className, ...props }, ref) => (

)); TableFooter.displayName = "TableFooter";

const TableRow = React.forwardRef(({ className, ...props }, ref) => (

)); TableRow.displayName = "TableRow";

const TableHead = React.forwardRef(({ className, ...props }, ref) => (

)); TableCaption.displayName = "TableCaption";

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, };


File: /src/components/ui/tabs.jsx

// src/components/ui/tabs.jsx "use client"

import * as React from "react" import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef(({ className, ...props }, ref) => ( <TabsPrimitive.List ref={ref} className={cn( "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className )} {...props} /> )) TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => ( <TabsPrimitive.Trigger ref={ref} className={cn( "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", className )} {...props} /> )) TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef(({ className, ...props }, ref) => ( <TabsPrimitive.Content ref={ref} className={cn( "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className )} {...props} /> )) TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }


File: /src/components/ClientTelegramManager.js

'use client';

import TelegramManager from './TelegramManager';

export default function ClientTelegramManager() { return ; }


File: /src/components/ContactsList.jsx

'use client'

import { useState, useEffect } from 'react'; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { supabase } from '@/lib/supabase'; import { generateCSV } from '@/lib/csvUtils';

export default function ContactsList() { const [contacts, setContacts] = useState([]); const [selectedContacts, setSelectedContacts] = useState([]); const [selectAll, setSelectAll] = useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null);

useEffect(() => { const fetchContacts = async () => { try { const { data, error } = await supabase .from('contacts') .select('*'); // Ensure filtering by user_id if necessary

    if (error) throw error;
    setContacts(data);
  } catch (error) {
    console.error('Error fetching contacts:', error);
    setError('Failed to load contacts. Please try again.');
  } finally {
    setIsLoading(false);
  }
};

fetchContacts();

}, []);

const handleSelectAll = () => { setSelectAll(!selectAll); setSelectedContacts(selectAll ? [] : contacts.map(contact => contact.id)); };

const handleSelectContact = (contactId) => { setSelectedContacts(prevSelected => prevSelected.includes(contactId) ? prevSelected.filter(id => id !== contactId) : [...prevSelected, contactId] ); };

const handleExtract = async () => { try { const selectedData = contacts.filter(contact => selectedContacts.includes(contact.id)); const csvContent = generateCSV(selectedData, ['id', 'first_name', 'last_name', 'username', 'phone_number', 'bio', 'online_status']); downloadCSV(csvContent, 'extracted_contacts.csv'); } catch (error) { console.error('Error extracting contacts:', error); setError('Failed to extract contacts. Please try again.'); } };

const downloadCSV = (content, fileName) => { const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', fileName); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } };

if (isLoading) { return

Loading...
; }

if (error) { return

Error: {error}
; }

return (

Contacts List

<Checkbox id="select-all" checked={selectAll} onCheckedChange={handleSelectAll} label={selectAll ? "Unselect All" : "Select All"} /> Select Name Username Phone Bio Status {contacts.map((contact) => ( <Checkbox checked={selectedContacts.includes(contact.id)} onCheckedChange={() => handleSelectContact(contact.id)} /> {${contact.first_name} ${contact.last_name}} {contact.username} {contact.phone_number} {contact.bio} <span className={contact.online_status === 'Online' ? 'text-green-500' : 'text-gray-500'}> {contact.online_status || 'Offline'} ))}
)); TableHead.displayName = "TableHead";

const TableCell = React.forwardRef(({ className, ...props }, ref) => (

)); TableCell.displayName = "TableCell";

const TableCaption = React.forwardRef(({ className, ...props }, ref) => (

<Button onClick={handleExtract} disabled={selectedContacts.length === 0}>Extract Selected Contacts
); }


File: /src/components/GroupsList.jsx

'use client'

import { useState, useEffect } from 'react'; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { supabase } from '@/lib/supabase'; import { generateCSV } from '@/lib/csvUtils';

export default function GroupsList() { const [groups, setGroups] = useState([]); const [selectedGroups, setSelectedGroups] = useState([]); const [selectAll, setSelectAll] = useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null);

useEffect(() => { const fetchGroups = async () => { try { const { data, error } = await supabase .from('groups') .select('*'); // Ensure filtering by user_id if necessary

    if (error) throw error;
    setGroups(data);
  } catch (error) {
    console.error('Error fetching groups:', error);
    setError('Failed to load groups. Please try again.');
  } finally {
    setIsLoading(false);
  }
};

fetchGroups();

}, []);

const handleSelectAll = () => { setSelectAll(!selectAll); setSelectedGroups(selectAll ? [] : groups.map(group => group.id)); };

const handleSelectGroup = (groupId) => { setSelectedGroups(prevSelected => prevSelected.includes(groupId) ? prevSelected.filter(id => id !== groupId) : [...prevSelected, groupId] ); };

const handleExtract = async () => { try { const selectedData = groups.filter(group => selectedGroups.includes(group.id)); const csvContent = generateCSV(selectedData, ['id', 'group_name', 'description', 'invite_link']); downloadCSV(csvContent, 'extracted_groups.csv'); } catch (error) { console.error('Error extracting groups:', error); setError('Failed to extract groups. Please try again.'); } };

const downloadCSV = (content, fileName) => { const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', fileName); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } };

if (isLoading) { return

Loading...
; }

if (error) { return

Error: {error}
; }

return (

Groups List

<Checkbox id="select-all" checked={selectAll} onCheckedChange={handleSelectAll} label={selectAll ? "Unselect All" : "Select All"} /> Select Group Name Description Invite Link {groups.map((group) => ( <Checkbox checked={selectedGroups.includes(group.id)} onCheckedChange={() => handleSelectGroup(group.id)} /> {group.group_name} {group.description} {group.invite_link} ))}
<Button onClick={handleExtract} disabled={selectedGroups.length === 0}>Extract Selected Groups
); }


File: /src/components/TelegramManager.jsx

import React, { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Loader2 } from 'lucide-react' import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

const CODE_EXPIRATION_TIME = 30 * 60; // 30 minutes in seconds

export default function TelegramManager() { const router = useRouter() const [apiId, setApiId] = useState('') const [apiHash, setApiHash] = useState('') const [phoneNumber, setPhoneNumber] = useState('') const [extractType, setExtractType] = useState('groups') const [validationCode, setValidationCode] = useState('') const [showValidationInput, setShowValidationInput] = useState(false) const [error, setError] = useState(null) const [successMessage, setSuccessMessage] = useState(null) const [isLoading, setIsLoading] = useState(false) const [codeRequestTime, setCodeRequestTime] = useState(null) const [isAuthenticated, setIsAuthenticated] = useState(false) const [timeRemaining, setTimeRemaining] = useState(CODE_EXPIRATION_TIME) const [isPhoneRegistered, setIsPhoneRegistered] = useState(null) const [hasExistingSession, setHasExistingSession] = useState(false)

useEffect(() => { const checkExistingSession = async () => { if (phoneNumber && apiId && apiHash) { setIsLoading(true) try { const response = await fetch('/api/extract-data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'checkSession', phoneNumber: phoneNumber.trim(), apiId: parseInt(apiId), apiHash }), }); if (!response.ok) { throw new Error('Failed to check session'); } const data = await response.json(); setHasExistingSession(data.hasSession); if (data.hasSession) { setIsAuthenticated(true); setSuccessMessage('You have an existing session. You can proceed with data extraction.'); } } catch (error) { console.error('Failed to check session:', error); setError('Failed to check for existing session. Please try again.'); } finally { setIsLoading(false); } } };

checkExistingSession();

}, [phoneNumber, apiId, apiHash]);

useEffect(() => { let timer if (showValidationInput && codeRequestTime) { timer = setInterval(() => { const elapsed = Math.floor((Date.now() - codeRequestTime) / 1000) const remaining = Math.max(CODE_EXPIRATION_TIME - elapsed, 0) setTimeRemaining(remaining) if (remaining === 0) { setError('Code expired. Please request a new one.') setShowValidationInput(false) setValidationCode('') setCodeRequestTime(null) } }, 1000) } return () => clearInterval(timer) }, [showValidationInput, codeRequestTime])

const validateInputs = () => { if (!apiId || isNaN(apiId) || parseInt(apiId) <= 0) { setError('API ID must be a valid positive number') return false } if (!apiHash || !/^[a-f0-9]{32}$/.test(apiHash)) { setError('API Hash should be a 32-character hexadecimal string') return false } if (!phoneNumber || !/^+[1-9]\d{1,14}$/.test(phoneNumber.trim())) { setError('Please enter a valid phone number with country code (e.g., +1234567890)') return false } return true }

const handleSubmit = async (e) => { e.preventDefault() setError(null) setSuccessMessage(null) setIsLoading(true)

if (!validateInputs()) {
  setIsLoading(false)
  return
}

try {
  const payload = {
    apiId: parseInt(apiId),
    apiHash,
    phoneNumber: phoneNumber.trim(),
    extractType,
    validationCode: showValidationInput ? validationCode : undefined,
  }

  console.log('[DEBUG]: Submitting request with:', {
    ...payload,
    apiHash: '******',
    phoneNumber: '*******' + payload.phoneNumber.slice(-4),
    validationCode: payload.validationCode ? '******' : undefined,
  })

  const response = await fetch('/api/extract-data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  })

  if (!response.ok) {
    if (response.status === 503) {
      throw new Error('Unable to connect to Telegram servers. Please try again later.');
    }
    throw new Error('Server returned an error. Please try again.');
  }

  const data = await response.json();
  console.log('[DEBUG]: Received response:', data)

  if (data.requiresValidation) {
    setShowValidationInput(true)
    setCodeRequestTime(Date.now())
    setTimeRemaining(CODE_EXPIRATION_TIME)
    setIsPhoneRegistered(data.phoneRegistered)
    setSuccessMessage(`Validation code sent to your Telegram app. ${data.phoneRegistered ? 'Your phone is registered.' : 'Your phone is not registered and will be signed up.'}`)
  } else if (data.success) {
    if (showValidationInput || hasExistingSession) {
      setIsAuthenticated(true)
      setShowValidationInput(false)
      setSuccessMessage('Authentication successful. You can now extract data.')
    } else if (data.data) {
      setSuccessMessage(`Extracted ${data.data.length} ${extractType}`)
      setTimeout(() => router.push(`/${extractType}-list`), 2000)
    }
  } else if (data.code === 'PHONE_CODE_EXPIRED') {
    setError('The verification code has expired. Please request a new code.')
    setShowValidationInput(false)
    setValidationCode('')
    setCodeRequestTime(null)
  } else {
    setError(data.message || 'An unexpected error occurred. Please try again.')
  }
} catch (error) {
  console.error('[ERROR]: Submit failed:', error)
  setError(error.message || 'An unexpected error occurred. Please try again.')
} finally {
  setIsLoading(false)
}

}

const renderTimer = () => { const minutes = Math.floor(timeRemaining / 60) const seconds = timeRemaining % 60 return ${minutes}:${seconds.toString().padStart(2, '0')} }

return (

Telegram Extractor

Telegram Extractor
API ID <Input id="api-id" value={apiId} onChange={(e) => setApiId(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your API ID" />
API Hash <Input id="api-hash" value={apiHash} onChange={(e) => setApiHash(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your API Hash" />
Phone Number <Input id="phone-number" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your phone number (with country code)" />
<RadioGroupItem value="groups" id="groups" disabled={isLoading || !isAuthenticated} /> Extract Groups
<RadioGroupItem value="contacts" id="contacts" disabled={isLoading || !isAuthenticated} /> Extract Contacts
{showValidationInput && !hasExistingSession && (
Validation Code <Input id="validation-code" value={validationCode} onChange={(e) => setValidationCode(e.target.value)} required disabled={isLoading} placeholder="Enter the code sent to your Telegram app" />

Code expires in: {renderTimer()}

Please check your Telegram app for the verification code.

{isPhoneRegistered !== null && (

{isPhoneRegistered ? 'Phone is registered.' : 'Phone is not registered and will be signed up.'}

)}
)} {isLoading ? : (isAuthenticated ? 'Extract Data' : (showValidationInput ? 'Verify Code' : 'Request Code'))} {error && ( Error {error} )} {successMessage && ( Success {successMessage} )}
) }


File: /src/lib/apiUtils.js

import { FloodWaitError, errors } from 'telegram'; import { NextResponse } from 'next/server'; import { createClient } from '@supabase/supabase-js';

const MAX_REQUESTS_PER_MINUTE = 20; const MAX_BACKOFF_TIME = 60000; // 1 minute

let requestCount = 0; let lastRequestTime = Date.now(); let backoffTime = 2000; // Start with 2 seconds

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

export function checkRateLimit() { const currentTime = Date.now(); if (currentTime - lastRequestTime > 60000) { requestCount = 0; lastRequestTime = currentTime; }

if (requestCount >= MAX_REQUESTS_PER_MINUTE) { throw new Error('Rate limit exceeded'); }

requestCount++; }

export async function handleTelegramError(error) { console.error('Telegram API error:', error);

if (error.message.includes('PHONE_NUMBER_INVALID')) { return NextResponse.json({ success: false, error: { code: 400, message: 'Invalid phone number. Please check and try again.' } }, { status: 400 }); } if (error.message.includes('PHONE_CODE_INVALID')) { return NextResponse.json({ success: false, error: { code: 400, message: 'Invalid verification code. Please try again.' } }, { status: 400 }); } if (error.message.includes('PHONE_CODE_EXPIRED')) { return NextResponse.json({ success: false, error: { code: 400, message: 'Verification code has expired. Please request a new one.' } }, { status: 400 }); } if (error.message.includes('SESSION_PASSWORD_NEEDED')) { return NextResponse.json({ success: false, error: { code: 400, message: 'Two-factor authentication is enabled. Please disable it temporarily to use this service.' } }, { status: 400 }); } else if (error.message.includes('AUTH_KEY_UNREGISTERED')) { return NextResponse.json({ success: false, error: { code: 401, message: 'The provided API credentials are invalid or have been revoked.' } }, { status: 401 }); } else { return NextResponse.json({ success: false, error: { code: 500, message: 'An unexpected error occurred. Please try again later.', details: error.toString() } }, { status: 500 }); } }

export function handleSupabaseError(error) { console.error('Supabase error:', error); return NextResponse.json({ success: false, error: { code: 500, message: 'Database operation failed', details: error.toString() } }, { status: 500 }); }

export function handleErrorResponse(message, status = 500, error = null) { console.error('[ERROR RESPONSE]:', message); if (error && typeof error === 'object' && 'stack' in error) { console.error('[ERROR STACK]:', error.stack); } return NextResponse.json({ success: false, error: { code: status, message, details: error ? error.toString() : undefined, }, }, { status }); }


File: /src/lib/csvUtils.js

import { Parser } from 'json2csv';

export function generateCSV(data, fields) { const json2csvParser = new Parser({ fields }); return json2csvParser.parse(data); }


File: /src/lib/supabase.js

import { createClient } from '@supabase/supabase-js';

// Read the environment variables const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

// Check for missing environment variables if (!supabaseUrl || !supabaseAnonKey) { console.error('Missing Supabase environment variables'); } else { console.log('Supabase environment variables loaded successfully'); }

// Create a supabase client instance export const supabase = createClient( supabaseUrl, supabaseAnonKey );

// Log successful client creation console.log('Supabase client created successfully');

// Remove or comment out these console.log statements // console.log('Supabase client created:', supabase); // console.log("Supabase Config Loaded");


File: /src/lib/utils.js

// lib/utils.js export function cn(...classes) { return classes.filter(Boolean).join(" "); }


File: /utils/config.js

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://tg-groups-contacts-managerv2.vercel.app/api';

export { API_BASE_URL };


File: /.eslintrc.js

module.exports = { extends: ['next/core-web-vitals'], rules: { // Disable TypeScript-specific rules '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }


File: /.gitignore

Ignore Vercel deployment configurations

.vercel

Ignore dependencies

node_modules/

Ignore environment variables (for security)

.env .env.local .env.production .env.development

Ignore Python virtual environment (if applicable)

venv/

Ignore Next.js build outputs

.next/

Ignore any local build or distribution folders

dist/ build/

Ignore IDE or editor configurations

.vscode/ .idea/ .editorconfig # Only exclude if you don't need it in the repo

Ignore macOS system files

.DS_Store

Ignore log files and other temporary files

.log npm-debug.log yarn-debug.log* yarn-error.log*

Ignore coverage reports

coverage/ *.lcov

Ignore test configurations and reports

*.test.js *.spec.js jest.config.js

Ignore project documentation files

Project-code.md .cursorrules

Optional: Lock files should generally be included for consistent dependency versions

package-lock.json

yarn.lock

Exclude specific unnecessary folders/files (modify if not needed)

.project-code/ .node_modules/

Optional: Ignore temporary files created by your OS or editors

Thumbs.db


File: /.vercelignore

Node modules should not be included in the repository, Vercel handles this

node_modules/

Environment files (make sure to use environment variables on Vercel)

.env .env.local .env.production .env.development

Build output directories

.next/ dist/ build/

Test and coverage reports (not needed in production)

tests/ coverage/ jest.config.js *.test.js *.spec.js

IDE/Editor configurations (local settings)

.vscode/ .idea/

Log files and temporary files

.log npm-debug.log yarn-debug.log* yarn-error.log* *.cursorrules

Documentation and unnecessary files

README.md *.md

Python virtual environment (if applicable)

venv/

OS-specific files

.DS_Store # macOS Thumbs.db # Windows

Optional: include lock files for consistent dependency versions across environments

package-lock.json

yarn.lock


File: /components.json

{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": false, "tailwind": { "config": "tailwind.config.js", "css": "styles/global.css", "baseColor": "slate", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } }


File: /jsconfig.json

{ "compilerOptions": { "baseUrl": ".", "paths": { "@/": ["src/"] } } }


File: /next.config.js

/** @type {import('next').NextConfig} / const nextConfig = { reactStrictMode: true, env: { NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, NEXT_PUBLIC_TELEGRAM_BOT_USERNAME: process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME, }, typescript: { // It's recommended to remove this in production and fix type errors ignoreBuildErrors: true, }, experimental: { esmExternals: 'loose', }, webpack: (config, { isServer }) => { if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false, }; } return config; }, async headers() { return [ { source: '/api/:path', headers: [ { key: 'Access-Control-Allow-Credentials', value: 'true' }, { key: 'Access-Control-Allow-Origin', value: '*' }, { key: 'Access-Control-Allow-Methods', value: 'GET,DELETE,PATCH,POST,PUT' }, { key: 'Access-Control-Allow-Headers', value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version' }, ], }, ]; }, };

module.exports = nextConfig;


File: /package.json

{ "name": "tg-groups-and-contacts-extractor", "version": "1.0.0", "description": "Telegram Groups and Contacts extractor", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "author": "ElPi", "license": "MIT", "engines": { "node": ">=16.0.0" }, "dependencies": { "@mtproto/core": "^6.3.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@supabase/supabase-js": "^2.45.4", "class-variance-authority": "^0.7.0", "csv-stringify": "^6.4.4", "date-fns": "^2.30.0", "json2csv": "^6.0.0-alpha.2", "lucide-react": "^0.294.0", "next": "^14.0.3", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "telegram": "^2.25.15" }, "devDependencies": { "@types/node": "^22.7.4", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "autoprefixer": "^10.4.16", "eslint": "^8.0.0", "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.0.0" } }


File: /postcss.config.js

module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }


File: /route-working.js

import { NextResponse } from 'next/server'; import { TelegramClient } from 'telegram'; import { StringSession } from 'telegram/sessions'; import { Api } from 'telegram/tl'; import { checkRateLimit, handleTelegramError, handleErrorResponse } from '@/lib/apiUtils'; import { createClient } from '@supabase/supabase-js';

// Initialize Supabase client const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabase = createClient(supabaseUrl, supabaseKey);

export async function POST(req) { let client; try { console.log('[START]: Handling API Request'); const { apiId, apiHash, phoneNumber, extractType, validationCode } = await req.json();

console.log('[DEBUG]: Received payload:', { 
  apiId, apiHash, phoneNumber, extractType, 
  validationCode: validationCode ? 'Provided' : 'Not provided',
});

// Input Validation
if (!apiId || isNaN(apiId) || parseInt(apiId) <= 0) {
  return handleErrorResponse('API ID is invalid or missing. Please provide a valid positive number.', 400);
}
if (!apiHash || !/^[a-f0-9]{32}$/.test(apiHash)) {
  return handleErrorResponse('API Hash is invalid. It should be a 32-character hexadecimal string.', 400);
}
if (!phoneNumber || typeof phoneNumber !== 'string' || phoneNumber.trim() === '') {
  return handleErrorResponse('Phone number is missing or invalid. Please enter a valid phone number.', 400);
}

const validPhoneNumber = phoneNumber.trim();
console.log('[DEBUG]: Valid phone number:', validPhoneNumber);

checkRateLimit();

const stringSession = new StringSession('');
client = new TelegramClient(stringSession, parseInt(apiId), apiHash, {
  connectionRetries: 5,
  useWSS: true,
  timeout: 30000,
});

console.log('[PROCESS]: Connecting to Telegram');
await client.connect();

if (!client.connected) {
  throw new Error('Failed to connect to Telegram');
}

// Check if user exists, if not create a new user in Supabase
let { data: user, error: userError } = await supabase
  .from('users')
  .select('*')
  .eq('phone_number', validPhoneNumber)
  .single();

if (userError && userError.code === 'PGRST116') {
  // User not found, create a new user
  const { data: newUser, error: createError } = await supabase
    .from('users')
    .insert({ phone_number: validPhoneNumber, api_id: apiId, api_hash: apiHash })
    .select()
    .single();

  if (createError) {
    console.error('[USER CREATE ERROR]:', createError);
    throw createError;
  }
  user = newUser;
  console.log('[DEBUG]: New user created:', user.id);
} else if (userError) {
  console.error('[USER FETCH ERROR]:', userError);
  throw userError;
}

// Step 1: Request validation code if not provided
if (!validationCode) {
  console.log('[PROCESS]: Requesting validation code');
  try {
    const result = await client.sendCode(
      {
        apiId: parseInt(apiId),
        apiHash: apiHash,
      },
      validPhoneNumber
    );
    console.log('[SUCCESS]: Validation code requested successfully');
    
    // Store phoneCodeHash in Supabase
    const { error: updateError } = await supabase
      .from('users')
      .update({ 
        phoneCodeHash: result.phoneCodeHash, 
        code_request_time: new Date().toISOString() 
      })
      .eq('id', user.id);

    if (updateError) {
      console.error('[UPDATE ERROR]:', updateError);
      throw updateError;
    }

    console.log('[DEBUG]: Updated user with phoneCodeHash and code_request_time');

    return NextResponse.json({
      success: true,
      message: 'Validation code sent to your phone. Please provide it in the next step.',
      requiresValidation: true,
    });
  } catch (error) {
    console.error('[SEND CODE ERROR]:', error);
    return handleTelegramError(error);
  }
}

// Retrieve phoneCodeHash from Supabase
const { data: userData, error: fetchError } = await supabase
  .from('users')
  .select('phoneCodeHash, code_request_time')
  .eq('id', user.id)
  .single();

if (fetchError) {
  console.error('[FETCH ERROR]:', fetchError);
  throw fetchError;
}

console.log('[DEBUG]: Retrieved user data:', userData);

const { phoneCodeHash, code_request_time: codeRequestTime } = userData;

if (!phoneCodeHash || !codeRequestTime) {
  return handleErrorResponse('Validation code not requested or expired. Please request a new code.', 400);
}

// Check if the code has expired
const codeRequestDate = new Date(codeRequestTime);
const currentTime = new Date();
const timeDifference = currentTime - codeRequestDate;
if (timeDifference > 120000) { // 2 minutes
  return NextResponse.json({
    success: false,
    message: 'The verification code has expired. Please request a new code.',
    code: 'PHONE_CODE_EXPIRED'
  });
}

// Step 2: Sign in with the provided validation code and extract data
console.log('[PROCESS]: Attempting to sign in with provided code');
try {
  const signInResult = await client.invoke(new Api.auth.SignIn({
    phoneNumber: validPhoneNumber,
    phoneCodeHash: phoneCodeHash,
    phoneCode: validationCode
  }));

  if (!signInResult.user) {
    throw new Error('Failed to sign in. Please check your validation code and try again.');
  }

  console.log('[SUCCESS]: Signed in successfully');

  // Perform data extraction based on extractType
  let extractedData = [];
  if (extractType === 'groups') {
    const dialogs = await client.getDialogs();
    extractedData = dialogs.map(dialog => ({
      group_name: dialog.title,
      group_id: dialog.id.toString(),
      participant_count: dialog.participantsCount || 0,
      type: dialog.isChannel ? 'channel' : 'group',
      is_public: !!dialog.username,
      owner_id: user.id,
    }));
  } else if (extractType === 'contacts') {
    const contacts = await client.getContacts();
    extractedData = contacts.map(contact => ({
      user_id: contact.id.toString(),
      first_name: contact.firstName,
      last_name: contact.lastName,
      username: contact.username,
      phone_number: contact.phone,
      is_mutual_contact: contact.mutualContact,
      owner_id: user.id,
    }));
  } else {
    throw new Error('Invalid extract type specified');
  }

  console.log(`[DEBUG]: Extracted ${extractedData.length} ${extractType}`);

  // Insert extracted data into Supabase
  const { error: insertError } = await supabase
    .from(extractType)
    .insert(extractedData);

  if (insertError) {
    console.error('[INSERT ERROR]:', insertError);
    throw insertError;
  }

  // Clear the phoneCodeHash after successful sign-in
  const { error: clearError } = await supabase
    .from('users')
    .update({ phoneCodeHash: null, code_request_time: null })
    .eq('id', user.id);

  if (clearError) {
    console.error('[CLEAR HASH ERROR]:', clearError);
    // Not throwing here as it's not critical
  }

  return NextResponse.json({
    success: true,
    message: `${extractType} extracted successfully`,
    sessionString: client.session.save(),
  });
} catch (error) {
  console.error('[SIGN IN ERROR]:', error);
  if (error.errorMessage === 'PHONE_CODE_EXPIRED') {
    return NextResponse.json({
      success: false,
      message: 'The verification code has expired. Please request a new code.',
      code: 'PHONE_CODE_EXPIRED'
    });
  }
  return handleTelegramError(error);
}

} catch (error) { console.error('[GENERAL API ERROR]: Error in extract-data API:', error); return handleErrorResponse('An unexpected error occurred. Please try again later.', 500, error); } finally { if (client && client.connected) { try { await client.disconnect(); console.log('[CLEANUP]: Telegram client disconnected successfully'); } catch (disconnectError) { console.error('[DISCONNECT ERROR]: Error disconnecting Telegram client:', disconnectError); } } } }


File: /tailwind.config.js

/** @type {import('tailwindcss').Config} / module.exports = { content: [ "./src/pages/**/.{js,jsx}", "./src/components//*.{js,jsx}", "./src/app//*.{js,jsx}", ], theme: { extend: {}, }, plugins: [], }


File: /TelegramManager-working.jsx

import React, { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Loader2 } from 'lucide-react' import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

export default function TelegramManager() { const router = useRouter() const [apiId, setApiId] = useState('') const [apiHash, setApiHash] = useState('') const [phoneNumber, setPhoneNumber] = useState('') const [extractType, setExtractType] = useState('groups') const [validationCode, setValidationCode] = useState('') const [showValidationInput, setShowValidationInput] = useState(false) const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) const [codeRequestTime, setCodeRequestTime] = useState(null) const [isAuthenticated, setIsAuthenticated] = useState(false)

useEffect(() => { if (showValidationInput && codeRequestTime) { const timer = setTimeout(() => { setError('Code expired. Please request a new one.') setShowValidationInput(false) setValidationCode('') setCodeRequestTime(null) }, 120000) // 2 minutes expiration return () => clearTimeout(timer) } }, [showValidationInput, codeRequestTime])

const validateInputs = () => { if (!apiId || isNaN(apiId) || parseInt(apiId) <= 0) { setError('API ID must be a valid positive number') return false } if (!apiHash || !/^[a-f0-9]{32}$/.test(apiHash)) { setError('API Hash should be a 32-character hexadecimal string') return false } if (!phoneNumber || phoneNumber.trim() === '') { setError('Please enter a valid phone number') return false } return true }

const handleSubmit = async (e) => { e.preventDefault() setError(null) setIsLoading(true)

if (!validateInputs()) {
  setIsLoading(false)
  return
}

try {
  const payload = {
    apiId: parseInt(apiId),
    apiHash,
    phoneNumber: phoneNumber.trim(),
    extractType,
    validationCode: showValidationInput ? validationCode : undefined,
  }

  console.log('[DEBUG]: Submitting request with:', {
    ...payload,
    apiHash: '******',
    phoneNumber: '*******' + payload.phoneNumber.slice(-4),
    validationCode: payload.validationCode ? '******' : undefined,
  })

  const response = await fetch('/api/telegram-extract', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  })

  const data = await response.json()
  console.log('[DEBUG]: Received response:', data)

  if (!response.ok) {
    throw new Error(data.error || 'Failed to process request')
  }

  if (data.requiresValidation) {
    setShowValidationInput(true)
    setCodeRequestTime(new Date())
    setError(null)
    alert('Please enter the validation code sent to your Telegram app.')
  } else if (data.success) {
    setIsAuthenticated(true)
    setShowValidationInput(false)
    if (data.data) {
      alert(`Extracted ${data.data.length} ${extractType}`)
      // Here you might want to save the data or redirect to a results page
      router.push(`/${extractType}-list`)
    } else {
      alert('Authentication successful. You can now extract data.')
    }
  } else {
    setError('An unexpected error occurred. Please try again.')
  }
} catch (error) {
  console.error('[ERROR]: Submit failed:', error)
  setError(error.message)
} finally {
  setIsLoading(false)
}

}

return (

Telegram Extractor

Telegram Extractor
API ID <Input id="api-id" value={apiId} onChange={(e) => setApiId(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your API ID" />
API Hash <Input id="api-hash" value={apiHash} onChange={(e) => setApiHash(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your API Hash" />
Phone Number <Input id="phone-number" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} required disabled={isLoading || showValidationInput || isAuthenticated} placeholder="Enter your phone number (with country code)" />
<RadioGroupItem value="groups" id="groups" disabled={isLoading || !isAuthenticated} /> Extract Groups
<RadioGroupItem value="contacts" id="contacts" disabled={isLoading || !isAuthenticated} /> Extract Contacts
{showValidationInput && (
Validation Code <Input id="validation-code" value={validationCode} onChange={(e) => setValidationCode(e.target.value)} required disabled={isLoading} placeholder="Enter the code sent to your Telegram app" />
)} {isLoading ? : (isAuthenticated ? 'Extract Data' : (showValidationInput ? 'Verify Code' : 'Request Code'))} {error && ( Error {error} )}
) }


File: /vercel.json

{ "version": 2, "builds": [ { "src": "next.config.js", "use": "@vercel/next" } ], "routes": [ { "src": "/api/(.)", "dest": "/api/$1", "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], "headers": { "Access-Control-Allow-Origin": "" } }, { "src": "/(.*)", "dest": "/$1" } ] }