🧑‍💻 feat: Add telegram api debugging capabilities

This commit is contained in:
jarvis2f 2025-01-11 22:51:15 +08:00
parent 2454da15ff
commit 23c5758e0f
9 changed files with 394 additions and 42 deletions

View file

@ -24,6 +24,7 @@ import io.vertx.ext.web.handler.CorsHandler;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.sstore.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore;
import org.drinkless.tdlib.TdApi;
import telegram.files.repository.SettingKey;
import telegram.files.repository.SettingRecord;
import telegram.files.repository.TelegramRecord;
@ -130,6 +131,8 @@ public class HttpVerticle extends AbstractVerticle {
router.post("/telegram/create").handler(this::handleTelegramCreate);
router.post("/telegram/:telegramId/delete").handler(this::handleTelegramDelete);
router.get("/telegram/api/methods").handler(this::handleTelegramApiMethods);
router.get("/telegram/api/:method/parameters").handler(this::handleTelegramApiMethodParameters);
router.post("/telegram/api/:method").handler(this::handleTelegramApi);
router.get("/telegrams").handler(this::handleTelegrams);
router.get("/telegram/:telegramId/chats").handler(this::handleTelegramChats);
@ -415,7 +418,7 @@ public class HttpVerticle extends AbstractVerticle {
String type = ctx.request().getParam("type");
String timeRange = ctx.request().getParam("timeRange");
(Objects.equals(type, "phase") ? telegramVerticle. getDownloadStatisticsByPhase(Convert.toInt(timeRange, 1)) :
(Objects.equals(type, "phase") ? telegramVerticle.getDownloadStatisticsByPhase(Convert.toInt(timeRange, 1)) :
telegramVerticle.getDownloadStatistics())
.onSuccess(ctx::json)
.onFailure(ctx::fail);
@ -459,6 +462,16 @@ public class HttpVerticle extends AbstractVerticle {
);
}
private void handleTelegramApiMethods(RoutingContext ctx) {
Map<String, Class<TdApi.Function<?>>> functions = TdApiHelp.getFunctions();
ctx.json(JsonObject.of("methods", functions.keySet()));
}
private void handleTelegramApiMethodParameters(RoutingContext ctx) {
String method = ctx.pathParam("method");
ctx.json(JsonObject.of("parameters", TdApiHelp.getFunction(method, null)));
}
private void handleTelegramApi(RoutingContext ctx) {
String method = ctx.pathParam("method");
if (method == null) {

View file

@ -71,6 +71,10 @@ public class TdApiHelp {
});
}
public static Map<String, Class<TdApi.Function<?>>> getFunctions() {
return FUNCTIONS;
}
public static TdApi.Function<?> getFunction(String method, Object params) {
Class<TdApi.Function<?>> func = FUNCTIONS.get(method);
if (func == null) {

View file

@ -80,6 +80,14 @@ Cookie: {{tf}}
GET http://{{server}}/telegram/{{telegramId}}/download-statistics?type=phase&timeRange=3
Cookie: {{tf}}
### Telegram api methods
GET http://{{server}}/telegram/api/methods
Cookie: {{tf}}
### Telegram api method parameters
GET http://{{server}}/telegram/api/GetChats/parameters
Cookie: {{tf}}
### Telegram GetMe
POST http://{{server}}/telegram/api/GetMe
Content-Type: application/json

View file

@ -0,0 +1,300 @@
import { useTelegramMethod } from "@/hooks/use-telegram-method";
import useSWR from "swr";
import { request } from "@/lib/api";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
import {
Check,
ChevronsUpDown,
CircleAlert,
Copy,
Ellipsis,
Loader2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { CommandLoading } from "cmdk";
import { useDebounce } from "use-debounce";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import Link from "next/link";
interface TelegramMethodData {
methods: string[];
}
interface TelegramMethodParameters {
parameters: Record<string, any>;
}
export default function DebugTelegramMethod() {
const { triggerMethod, isMethodExecuting, lastMethodCode, lastMethodResult } =
useTelegramMethod();
const [isDeMethodExecuting] = useDebounce(isMethodExecuting, 500, {
leading: true,
});
const [, copyToClipboard] = useCopyToClipboard();
const [method, setMethod] = useState<string>("");
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [parameters, setParameters] = useState<any>({});
const [parametersJson, setParametersJson] = useState<string>("");
const [error, setError] = useState<string>("");
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const { data: methodData, isLoading: isMethodsLoading } = useSWR<
TelegramMethodData,
Error
>("/telegram/api/methods", request, {
// 添加缓存以提升性能
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60000,
});
// 使用 useMemo 缓存过滤后的方法列表
const filteredMethods = useMemo(() => {
if (!methodData?.methods || isMethodsLoading) return [];
return methodData.methods
.filter((m) => m.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 100)
.sort();
}, [isMethodsLoading, methodData?.methods, searchQuery]);
const methodDocUrl = useMemo(() => {
if (!method) return "";
const methodSlug = method.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
return `https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1${methodSlug}.html`;
}, [method]);
const { isLoading: isParametersLoading } = useSWR<
TelegramMethodParameters,
Error
>(method ? `/telegram/api/${method}/parameters` : null, request, {
revalidateOnFocus: false,
onSuccess: (data) => {
const defaultParams = data.parameters;
setParameters(defaultParams);
setParametersJson(JSON.stringify(defaultParams, null, 2));
},
});
const handleMethodChange = (value: string) => {
setMethod(value);
setError("");
setOpen(false);
};
const handleParametersChange = (value: string) => {
setParametersJson(value);
try {
setParameters(JSON.parse(value));
setError("");
} catch (e) {
setError("Invalid JSON format");
}
};
const handleExecute = async () => {
try {
await triggerMethod({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data: parameters,
method,
});
setError("");
} catch (e) {
setError(e instanceof Error ? e.message : "An error occurred");
}
};
const isLoading = isMethodsLoading || isParametersLoading;
const isExecuteDisabled = !method || !!error || isMethodExecuting;
return (
<Card className="mx-auto h-full w-full overflow-y-scroll">
<CardHeader>
<CardTitle>Telegram API Debug</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* Method Selection */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Method</label>
{isMethodsLoading && (
<div className="flex items-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Loading methods...
</div>
)}
</div>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
disabled={isLoading}
>
{method ? method : "Select a method..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" modal={true} align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search method..."
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList className="relative">
{isMethodsLoading && (
<CommandLoading>
<div className="absolute left-1/2 top-1/2 -translate-x-2 -translate-y-2 transform">
<Ellipsis className="h-4 w-4 animate-pulse" />
</div>
</CommandLoading>
)}
{!isMethodsLoading && filteredMethods.length === 0 && (
<CommandEmpty>No methods found.</CommandEmpty>
)}
{!isMethodExecuting && searchQuery.length === 0 && (
<>
<CommandGroup>
<CommandItem disabled={true}>
<CircleAlert className="h-3 w-3" />
Only the first 100 methods are shown.
</CommandItem>
</CommandGroup>
<CommandSeparator />
</>
)}
<CommandGroup>
{filteredMethods.map((m) => (
<CommandItem
key={m}
value={m}
onSelect={handleMethodChange}
>
<Check
className={cn(
"h-4 w-4",
method === m ? "opacity-100" : "opacity-0",
)}
/>
{m}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{method && (
<Link
href={methodDocUrl}
target="_blank"
className="text-xs text-blue-500 underline"
>
{method} Documentation
</Link>
)}
</div>
{/* Parameters Editor */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Parameters</label>
{isParametersLoading && (
<div className="flex items-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Loading parameters...
</div>
)}
</div>
<Textarea
value={parametersJson}
onChange={(e) => handleParametersChange(e.target.value)}
placeholder="Enter parameters in JSON format"
className="h-48 font-mono text-sm"
disabled={!method || isLoading}
/>
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
</div>
{/* Execute Button */}
<Button
onClick={handleExecute}
disabled={isExecuteDisabled}
className="w-full"
>
{isDeMethodExecuting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Executing...
</>
) : (
"Execute Method"
)}
</Button>
{/* Result Display */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">
Result
<span className="text-xs text-muted-foreground">
{lastMethodCode ? `(${lastMethodCode})` : ""}
</span>
</label>
{isMethodExecuting && (
<div className="flex items-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
Processing...
</div>
)}
</div>
<div className="group relative">
<Textarea
value={
lastMethodResult
? JSON.stringify(lastMethodResult, null, 2)
: ""
}
readOnly
placeholder="Method execution result will appear here"
className="h-48 font-mono text-sm"
/>
<Button
size="sm"
variant="outline"
onClick={() => {
if (!lastMethodResult) return;
copyToClipboard(JSON.stringify(lastMethodResult, null, 2));
}}
className="absolute right-2 top-2 opacity-0 group-hover:opacity-100"
>
<Copy className="h-2 w-2" />
</Button>
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -33,30 +33,6 @@ export default function FileProgress({
return (
<div className="flex items-end justify-between gap-2">
{/*<div className="hidden min-w-20 md:flex">*/}
{/* <div className="group flex items-center gap-1 px-1">*/}
{/* <AnimatePresence mode="wait">*/}
{/* <motion.div*/}
{/* key="content"*/}
{/* className="flex items-center gap-1"*/}
{/* initial={{ opacity: 1 }}*/}
{/* exit={{ opacity: 0 }}*/}
{/* >*/}
{/* <Zap className="h-3 w-3 group-hover:hidden" />*/}
{/* <ClockArrowDown className="hidden h-3 w-3 group-hover:block" />*/}
{/* <span className="text-nowrap text-xs">*/}
{/* <span className="group-hover:hidden">*/}
{/* {`${prettyBytes(downloadSpeed, { bits: true })}/s`}*/}
{/* </span>*/}
{/* <span className="hidden group-hover:inline">*/}
{/* {prettyBytes(file.downloadedSize)}*/}
{/* </span>*/}
{/* </span>*/}
{/* </motion.div>*/}
{/* </AnimatePresence>*/}
{/* </div>*/}
{/*</div>*/}
<Progress value={progress} className="flex-1 rounded-none md:w-32" />
</div>
);

View file

@ -17,6 +17,7 @@ import About from "@/components/about";
import { ChartColumnIncreasingIcon } from "@/components/ui/chart-column-increasing";
import { LayoutPanelTopIcon } from "@/components/ui/layout-panel-top";
import FilePhaseStatistics from "@/components/file-phase-statistics";
import DebugTelegramMethod from "@/components/debug-telegram-method";
export const SettingsDialog: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
@ -50,6 +51,7 @@ export const SettingsDialog: React.FC = () => {
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="statistics">Statistics</TabsTrigger>
<TabsTrigger value="proxys">Proxys</TabsTrigger>
<TabsTrigger value="api">API</TabsTrigger>
<TabsTrigger value="about">About</TabsTrigger>
</TabsList>
<TabsContent value="general" className="overflow-hidden">
@ -60,10 +62,16 @@ export const SettingsDialog: React.FC = () => {
{accountId ? (
<Tabs defaultValue="panel">
<TabsList className="bg-none!">
<TabsTrigger value="panel" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
<TabsTrigger
value="panel"
className="h-10 w-10 scale-75 data-[state=active]:bg-accent"
>
<LayoutPanelTopIcon />
</TabsTrigger>
<TabsTrigger value="phase" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
<TabsTrigger
value="phase"
className="h-10 w-10 scale-75 data-[state=active]:bg-accent"
>
<ChartColumnIncreasingIcon />
</TabsTrigger>
</TabsList>
@ -92,6 +100,9 @@ export const SettingsDialog: React.FC = () => {
/>
</div>
</TabsContent>
<TabsContent value="api" className="h-full overflow-hidden">
<DebugTelegramMethod />
</TabsContent>
<TabsContent value="about" className="h-full overflow-hidden">
<About />
</TabsContent>

View file

@ -13,21 +13,33 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
modal?: boolean;
}
>(
(
{ className, align = "center", sideOffset = 4, modal = false, ...props },
ref,
) => {
const content = (
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
);
return !modal ? (
<PopoverPrimitive.Portal>{content}</PopoverPrimitive.Portal>
) : (
content
);
},
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View file

@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };

View file

@ -5,8 +5,10 @@ import { useEffect, useMemo, useState } from "react";
import { useWebsocket } from "@/hooks/use-websocket";
export function useTelegramMethod() {
const [lastMethodCode, setLastMethodCode] = useState<string>();
const [methodCodes, setMethodCodes] = useState<string[]>([]);
const [methodCompleteCodes, setMethodCompleteCodes] = useState<string[]>([]);
const [lastMethodResult, setLastMethodResult] = useState<unknown>();
const { lastJsonMessage } = useWebsocket();
const { trigger: triggerMethod, isMutating: isMethodMutating } =
@ -15,6 +17,7 @@ export function useTelegramMethod() {
telegramApi,
{
onSuccess: (data) => {
setLastMethodCode(data.code);
setMethodCodes((prev) => [...prev, data.code]);
},
},
@ -24,6 +27,7 @@ export function useTelegramMethod() {
if (!lastJsonMessage) return;
if (lastJsonMessage.code) {
setMethodCompleteCodes((prev) => [...prev, lastJsonMessage.code]);
setLastMethodResult(lastJsonMessage.data)
}
}, [lastJsonMessage]);
@ -37,5 +41,7 @@ export function useTelegramMethod() {
return {
triggerMethod,
isMethodExecuting,
lastMethodCode,
lastMethodResult,
};
}