419 lines
13 KiB
TypeScript
419 lines
13 KiB
TypeScript
import type { User, UserBasic, UserPresence, Chat, Message, MediaItem, StoryGroup, FriendRequest, FriendWithId, FriendshipStatus } from './types';
|
|
|
|
const API_BASE = '/api';
|
|
|
|
class ApiClient {
|
|
private token: string | null = null;
|
|
|
|
setToken(token: string | null) {
|
|
this.token = token;
|
|
}
|
|
|
|
private async request<T>(endpoint: string, options: RequestInit & { timeout?: number } = {}): Promise<T> {
|
|
const { timeout = 30_000, ...fetchOptions } = options;
|
|
const controller = new AbortController();
|
|
const timer = timeout > 0 ? setTimeout(() => controller.abort(), timeout) : undefined;
|
|
|
|
const isFormData = fetchOptions.body instanceof FormData;
|
|
const computedHeaders: Record<string, string> = {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
...(fetchOptions.headers as Record<string, string>),
|
|
};
|
|
|
|
if (!isFormData && !computedHeaders['Content-Type']) {
|
|
computedHeaders['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${API_BASE}${endpoint}`, {
|
|
...fetchOptions,
|
|
headers: computedHeaders,
|
|
signal: controller.signal,
|
|
});
|
|
} catch (err) {
|
|
clearTimeout(timer);
|
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
throw new Error('Время ожидания запроса истекло');
|
|
}
|
|
throw err;
|
|
}
|
|
clearTimeout(timer);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
const errorMessage = errorData.error || errorData.message || 'Ошибка запроса';
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f
|
|
async login(username: string, password: string) {
|
|
return this.request<{ token: string; user: User }>('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
}
|
|
|
|
async register(username: string, displayName: string, password: string, bio?: string) {
|
|
return this.request<{ token: string; user: User }>('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, displayName, password, bio }),
|
|
});
|
|
}
|
|
|
|
async getMe() {
|
|
return this.request<{ user: User }>('/auth/me');
|
|
}
|
|
|
|
async getConfig() {
|
|
return this.request<any>('/config');
|
|
}
|
|
|
|
// \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438
|
|
async searchUsers(query: string) {
|
|
return this.request<UserPresence[]>(`/users/search?q=${encodeURIComponent(query)}`);
|
|
}
|
|
|
|
async getUser(id: string) {
|
|
return this.request<User>(`/users/${id}`);
|
|
}
|
|
|
|
async updateProfile(data: { displayName?: string; bio?: string; birthday?: string }) {
|
|
return this.request<User>('/users/profile', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async uploadAvatar(file: File) {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 120_000);
|
|
const response = await fetch(`${API_BASE}/users/avatar`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timer);
|
|
|
|
if (!response.ok) throw new Error('Ошибка загрузки аватара');
|
|
return response.json() as Promise<User>;
|
|
}
|
|
|
|
async cropAvatar(file: File, cropData: { x: number; y: number; width: number; height: number }) {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
formData.append('cropX', cropData.x.toString());
|
|
formData.append('cropY', cropData.y.toString());
|
|
formData.append('cropWidth', cropData.width.toString());
|
|
formData.append('cropHeight', cropData.height.toString());
|
|
|
|
const response = await fetch(`${API_BASE}/users/avatar/crop`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Ошибка кропа аватара');
|
|
return response.json() as Promise<User>;
|
|
}
|
|
|
|
async removeAvatar() {
|
|
return this.request<User>('/users/avatar', { method: 'DELETE' });
|
|
}
|
|
|
|
async searchMessages(query: string, chatId?: string) {
|
|
const params = new URLSearchParams({ q: query });
|
|
if (chatId) params.append('chatId', chatId);
|
|
return this.request<Message[]>(`/messages/search?${params}`);
|
|
}
|
|
|
|
// \u0427\u0430\u0442\u044b
|
|
async getChats() {
|
|
return this.request<Chat[]>('/chats');
|
|
}
|
|
|
|
async createPersonalChat(userId: string) {
|
|
return this.request<Chat>('/chats/personal', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ userId }),
|
|
});
|
|
}
|
|
|
|
async createGroupChat(name: string, memberIds: string[]) {
|
|
return this.request<Chat>('/chats/group', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, memberIds }),
|
|
});
|
|
}
|
|
|
|
// \u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f
|
|
async getMessages(chatId: string, cursor?: string) {
|
|
const params = cursor ? `?cursor=${cursor}` : '';
|
|
return this.request<Message[]>(`/messages/chat/${chatId}${params}`);
|
|
}
|
|
|
|
async uploadFile(file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 120_000);
|
|
const response = await fetch(`${API_BASE}/messages/upload`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timer);
|
|
|
|
if (!response.ok) throw new Error('\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0430\u0439\u043b\u0430');
|
|
return response.json() as Promise<{ url: string; filename: string; size: number }>;
|
|
}
|
|
|
|
// \u0413\u0440\u0443\u043f\u043f\u044b
|
|
async updateGroup(chatId: string, data: { name?: string; description?: string }) {
|
|
return this.request<Chat>(`/chats/${chatId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async uploadGroupAvatar(chatId: string, file: File) {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 120_000);
|
|
const response = await fetch(`${API_BASE}/chats/${chatId}/avatar`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timer);
|
|
|
|
if (!response.ok) throw new Error('\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0430\u0432\u0430\u0442\u0430\u0440\u0430');
|
|
return response.json() as Promise<Chat>;
|
|
}
|
|
|
|
async cropGroupAvatar(chatId: string, file: File, cropData: { x: number; y: number; width: number; height: number }) {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
formData.append('x', cropData.x.toString());
|
|
formData.append('y', cropData.y.toString());
|
|
formData.append('width', cropData.width.toString());
|
|
formData.append('height', cropData.height.toString());
|
|
|
|
const response = await fetch(`${API_BASE}/chats/${chatId}/avatar/crop`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Ошибка кропа аватара');
|
|
return response.json() as Promise<Chat>;
|
|
}
|
|
|
|
async removeGroupAvatar(chatId: string) {
|
|
return this.request<Chat>(`/chats/${chatId}/avatar`, { method: 'DELETE' });
|
|
}
|
|
|
|
async addGroupMembers(chatId: string, userIds: string[]) {
|
|
return this.request<Chat>(`/chats/${chatId}/members`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ userIds }),
|
|
});
|
|
}
|
|
|
|
async removeGroupMember(chatId: string, userId: string) {
|
|
return this.request<Chat>(`/chats/${chatId}/members/${userId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async clearChat(chatId: string) {
|
|
return this.request<{ message: string }>(`/chats/${chatId}/clear`, { method: 'POST' });
|
|
}
|
|
|
|
async deleteChat(chatId: string) {
|
|
return this.request<{ message: string }>(`/chats/${chatId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async togglePinChat(chatId: string) {
|
|
return this.request<{ isPinned: boolean }>(`/chats/${chatId}/pin`, { method: 'POST' });
|
|
}
|
|
|
|
async getSharedMedia(chatId: string, type: 'media' | 'gifs' | 'files' | 'links') {
|
|
return this.request<any[]>(`/messages/chat/${chatId}/shared?type=${type}`);
|
|
}
|
|
|
|
|
|
// Stories
|
|
async getStories() {
|
|
return this.request<StoryGroup[]>('/stories');
|
|
}
|
|
|
|
async getUserStories(userId: string) {
|
|
return this.request<StoryGroup>(`/stories/user/${userId}`);
|
|
}
|
|
|
|
async createStory(data: { type: string; mediaUrl?: string; content?: string; bgColor?: string }) {
|
|
return this.request<{ id: string }>('/stories', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async uploadVideoToStory(file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const response = await fetch(`${API_BASE}/stories/video`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Ошибка загрузки видео истории');
|
|
return response.json() as Promise<{ url: string }>;
|
|
}
|
|
|
|
async viewStory(storyId: string) {
|
|
return this.request<{ message: string }>(`/stories/${storyId}/view`, { method: 'POST' });
|
|
}
|
|
|
|
async deleteStory(storyId: string) {
|
|
return this.request<{ message: string }>(`/stories/${storyId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async getStoryViewers(storyId: string) {
|
|
return this.request<Array<{ userId: string; username: string; displayName: string; avatar: string | null; viewedAt: string }>>(`/stories/${storyId}/viewers`);
|
|
}
|
|
|
|
async addStoryReaction(storyId: string, emoji: string) {
|
|
return this.request<{ message: string }>(`/stories/${storyId}/reaction`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
async removeStoryReaction(storyId: string, emoji: string) {
|
|
return this.request<{ message: string }>(`/stories/${storyId}/reaction`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ emoji }),
|
|
});
|
|
}
|
|
|
|
async addStoryReply(storyId: string, content: string) {
|
|
return this.request<{ message: string }>(`/stories/${storyId}/reply`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
}
|
|
|
|
async getStoryReplies(storyId: string) {
|
|
return this.request<Array<{ id: string; userId: string; username: string; displayName: string; avatar: string | null; content: string; createdAt: string }>>(`/stories/${storyId}/replies`);
|
|
}
|
|
|
|
// Favorites chat
|
|
async getOrCreateFavorites() {
|
|
return this.request<Chat>('/chats/favorites', { method: 'POST' });
|
|
}
|
|
|
|
// User settings
|
|
async updateSettings(settings: any) {
|
|
const res = await this.request('/users/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(settings),
|
|
});
|
|
return res;
|
|
}
|
|
|
|
async analyzeTelegramImport(file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const res = await this.request('/import/telegram/analyze', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
return res;
|
|
}
|
|
|
|
async executeTelegramImport(req: { token: string; mapping: Record<string, string>; groupName?: string }) {
|
|
return this.request('/import/telegram/execute', {
|
|
method: 'POST',
|
|
body: JSON.stringify(req),
|
|
});
|
|
}
|
|
|
|
// Friends
|
|
async getFriends() {
|
|
return this.request<FriendWithId[]>('/friends');
|
|
}
|
|
|
|
async getFriendRequests() {
|
|
return this.request<FriendRequest[]>('/friends/requests');
|
|
}
|
|
|
|
async getOutgoingRequests() {
|
|
return this.request<FriendRequest[]>('/friends/outgoing');
|
|
}
|
|
|
|
async getFriendshipStatus(userId: string) {
|
|
return this.request<FriendshipStatus>(`/friends/status/${userId}`);
|
|
}
|
|
|
|
async sendFriendRequest(friendId: string) {
|
|
return this.request<{ status: string }>('/friends/request', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ friendId }),
|
|
});
|
|
}
|
|
|
|
async acceptFriendRequest(friendshipId: string) {
|
|
return this.request<{ id: string }>(`/friends/${friendshipId}/accept`, { method: 'POST' });
|
|
}
|
|
|
|
async declineFriendRequest(friendshipId: string) {
|
|
return this.request<{ success: boolean }>(`/friends/${friendshipId}/decline`, { method: 'POST' });
|
|
}
|
|
|
|
async removeFriend(friendshipId: string) {
|
|
return this.request<{ success: boolean }>(`/friends/${friendshipId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async getIceServers() {
|
|
return this.request<{ iceServers: RTCIceServer[] }>('/webrtc/ice-servers');
|
|
}
|
|
|
|
// Klipy
|
|
async getTrendingGifs() {
|
|
return this.request<any>('/klipy/trending');
|
|
}
|
|
|
|
async searchKlipyGifs(query: string) {
|
|
return this.request<any>(`/klipy/search?q=${encodeURIComponent(query)}`);
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient();
|