File size: 7,703 Bytes
e03f82d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Typed API client helpers for the dashboard.

export interface Pattern {
  id: string;
  name: string;
  expression: string;
  regex: string;
  description: string | null;
  enabled: boolean;
  createdAt: string;
  updatedAt: string;
}

export type PatternMode = "all" | "specific" | "none";
export type SaleType = "auction" | "sale";
export type ImageMode = "none" | "generated" | "url";
export type ImageTemplate = "default" | "minimal" | "telegram";

export interface PageConfig {
  id: string;
  name: string;
  recipient: string;
  enabled: boolean;
  patternMode: PatternMode;
  patternIds: string[];
  minBidTon: number;
  maxBidTon: number;
  sources: string[];
  saleTypes: SaleType[];
  imageMode: ImageMode;
  imageTemplate: ImageTemplate;
  imageUrl?: string;
  messageTemplate: string;
  lastRunAt?: string | null;
  lastNotified: number;
  createdAt: string;
}

export interface AppSettings {
  botToken: string;
  intervalSeconds: number;
  realtimeMode: boolean;
  minBidTon: number;
  maxBidTon: number;
  sourceFragment: boolean;
  sourceGetgems: boolean;
  sourceMock: boolean;
  fetchSaleType: "auction" | "sale" | "both";
  messageTemplate: string;
}

export interface Auction {
  id: string;
  username: string;
  source: string;
  saleType: string;
  bidAmount: number;
  currency: string;
  bidder: string | null;
  endTime: string | null;
  url: string | null;
  fetchedAt: string;
  matched: boolean;
  notified: boolean;
  patternId: string | null;
  pattern: Pattern | null;
}

export interface Notification {
  id: string;
  auctionId: string | null;
  username: string;
  pattern: string | null;
  bidAmount: number;
  source: string;
  saleType: string;
  recipient: string;
  pageId: string | null;
  pageName: string | null;
  status: string;
  message: string;
  hasImage: boolean;
  error: string | null;
  sentAt: string;
}

export interface MonitorStatus {
  running: boolean;
  enabled: boolean;
  lastRunAt: string | null;
  nextRunAt: string | null;
  lastRunDurationMs: number | null;
  lastError: string | null;
  lastRunStats: { fetched: number; matched: number; notified: number; errors: number } | null;
  intervalSeconds: number;
  enabledPageCount: number;
}

async function parseJson<T>(res: Response): Promise<T> {
  if (!res.ok) {
    const t = await res.text().catch(() => "");
    throw new Error(t || `HTTP ${res.status}`);
  }
  return res.json() as Promise<T>;
}

export const api = {
  patterns: {
    list: () => fetch("/api/patterns").then((r) => parseJson<{ patterns: Pattern[] }>(r)),
    create: (data: { name?: string; expression: string; description?: string; enabled?: boolean }) =>
      fetch("/api/patterns", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ pattern: Pattern }>(r)),
    remove: (id: string) => fetch(`/api/patterns/${id}`, { method: "DELETE" }).then((r) => parseJson<{ ok: boolean }>(r)),
    patch: (id: string, data: { enabled?: boolean; name?: string; description?: string }) =>
      fetch(`/api/patterns/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ pattern: Pattern }>(r)),
    preview: (expression: string) =>
      fetch("/api/patterns/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode: "preview", expression }) }).then((r) => parseJson<{ ok: boolean; regex: string; length: number; variables: number; examples: string[] } | { error: string }>(r)),
    match: (username: string) =>
      fetch("/api/patterns/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode: "match", username }) }).then((r) => parseJson<{ ok: boolean; match: boolean; matches: { id: string; name: string; expression: string }[] }>(r)),
  },
  pages: {
    list: () => fetch("/api/pages").then((r) => parseJson<{ pages: PageConfig[] }>(r)),
    create: (data: Partial<PageConfig>) =>
      fetch("/api/pages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ page: PageConfig }>(r)),
    update: (id: string, data: Partial<PageConfig>) =>
      fetch(`/api/pages/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ page: PageConfig }>(r)),
    remove: (id: string) => fetch(`/api/pages/${id}`, { method: "DELETE" }).then((r) => parseJson<{ ok: boolean }>(r)),
    start: (id: string) => fetch(`/api/pages/${id}/start`, { method: "POST" }).then((r) => parseJson<{ ok: boolean; page: PageConfig }>(r)),
    stop: (id: string) => fetch(`/api/pages/${id}/stop`, { method: "POST" }).then((r) => parseJson<{ ok: boolean; page: PageConfig }>(r)),
    test: (id: string) => fetch(`/api/pages/${id}/test`, { method: "POST" }).then((r) => parseJson<{ ok: boolean; error?: string; hasImage?: boolean }>(r)),
  },
  settings: {
    get: () => fetch("/api/settings").then((r) => parseJson<{ settings: AppSettings }>(r)),
    update: (data: Partial<AppSettings>) =>
      fetch("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ settings: AppSettings }>(r)),
  },
  auctions: (params: { matched?: boolean; source?: string; saleType?: string; limit?: number; username?: string } = {}) => {
    const q = new URLSearchParams();
    if (params.matched !== undefined) q.set("matched", String(params.matched));
    if (params.source) q.set("source", params.source);
    if (params.saleType) q.set("saleType", params.saleType);
    if (params.limit) q.set("limit", String(params.limit));
    if (params.username) q.set("username", params.username);
    return fetch(`/api/auctions?${q.toString()}`).then((r) => parseJson<{ auctions: Auction[]; count: number }>(r));
  },
  notifications: (params: { limit?: number; status?: string } = {}) => {
    const q = new URLSearchParams();
    if (params.limit) q.set("limit", String(params.limit));
    if (params.status) q.set("status", params.status);
    return fetch(`/api/notifications?${q.toString()}`).then((r) => parseJson<{ notifications: Notification[]; count: number }>(r));
  },
  monitor: {
    status: () => fetch("/api/monitor/status").then((r) => parseJson<{ status: MonitorStatus }>(r)),
    run: () => fetch("/api/monitor/run", { method: "POST" }).then((r) => parseJson<{ result: { fetched: number; matched: number; notified: number; errors: number; error?: string }; status: MonitorStatus }>(r)),
  },
  telegram: {
    test: (mode: "ping" | "broadcast", chatId?: string, withImage?: boolean) =>
      fetch("/api/telegram/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode, chatId, withImage }) }).then((r) => parseJson<any>(r)),
    sendCustom: (data: { recipient: string; text: string; imageUrl?: string }) =>
      fetch("/api/telegram/send-custom", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ ok: boolean; error?: string; messageId?: number }>(r)),
  },
  search: (data: { expression?: string; minBid?: number; maxBid?: number; saleType?: string; hasDigits?: string; hasUnderscore?: string; minLen?: number; maxLen?: number; source?: string }) =>
    fetch("/api/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }).then((r) => parseJson<{ results: Auction[]; count: number } | { error: string }>(r)),
  seed: () => fetch("/api/seed", { method: "POST" }).then((r) => parseJson<{ ok: boolean; seeded: number }>(r)),
};