package api import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/cookiejar" "net/url" "regexp" "time" "MobileMkch/cache" "MobileMkch/models" ) const ( BaseURL = "https://mkch.pooziqo.xyz" ApiURL = BaseURL + "/api" ) type Client struct { httpClient *http.Client baseURL string debug bool authKey string passcode string } func NewClient() *Client { jar, _ := cookiejar.New(nil) return &Client{ httpClient: &http.Client{ Timeout: 30 * time.Second, Jar: jar, }, baseURL: BaseURL, debug: false, } } func (c *Client) EnableDebug(enable bool) { c.debug = enable if c.debug { fmt.Printf("[DEBUG] Debug режим включен\n") } } func (c *Client) Authenticate(authKey string) error { resp, err := c.httpClient.Get(fmt.Sprintf("%s/key/auth/", c.baseURL)) if err != nil { return fmt.Errorf("ошибка получения формы аутентификации: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ошибка получения формы аутентификации: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("ошибка чтения формы аутентификации: %w", err) } csrfToken := extractCSRFToken(string(body)) if csrfToken == "" { return fmt.Errorf("не удалось извлечь CSRF токен для аутентификации") } formData := url.Values{} formData.Set("csrfmiddlewaretoken", csrfToken) formData.Set("key", authKey) req, err := http.NewRequest("POST", fmt.Sprintf("%s/key/auth/", c.baseURL), bytes.NewBufferString(formData.Encode())) if err != nil { return fmt.Errorf("ошибка создания запроса аутентификации: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Referer", fmt.Sprintf("%s/key/auth/", c.baseURL)) resp, err = c.httpClient.Do(req) if err != nil { return fmt.Errorf("ошибка отправки аутентификации: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { return fmt.Errorf("ошибка аутентификации: %d", resp.StatusCode) } c.authKey = authKey return nil } func (c *Client) LoginWithPasscode(passcode string) error { resp, err := c.httpClient.Get(fmt.Sprintf("%s/passcode/enter/", c.baseURL)) if err != nil { return fmt.Errorf("ошибка получения формы passcode: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ошибка получения формы passcode: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("ошибка чтения формы passcode: %w", err) } csrfToken := extractCSRFToken(string(body)) if csrfToken == "" { return fmt.Errorf("не удалось извлечь CSRF токен для passcode") } formData := url.Values{} formData.Set("csrfmiddlewaretoken", csrfToken) formData.Set("passcode", passcode) req, err := http.NewRequest("POST", fmt.Sprintf("%s/passcode/enter/", c.baseURL), bytes.NewBufferString(formData.Encode())) if err != nil { return fmt.Errorf("ошибка создания запроса passcode: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Referer", fmt.Sprintf("%s/passcode/enter/", c.baseURL)) resp, err = c.httpClient.Do(req) if err != nil { return fmt.Errorf("ошибка отправки passcode: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { return fmt.Errorf("ошибка входа с passcode: %d", resp.StatusCode) } c.passcode = passcode return nil } func (c *Client) makeRequest(url string) ([]byte, error) { if c.debug { fmt.Printf("[API] Запрос: %s\n", url) } resp, err := c.httpClient.Get(url) if err != nil { return nil, fmt.Errorf("ошибка запроса: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("ошибка чтения ответа: %w", err) } if c.debug { fmt.Printf("[API] Статус: %d, Длина ответа: %d байт\n", resp.StatusCode, len(body)) if len(body) < 500 { fmt.Printf("[API] Ответ: %s\n", string(body)) } } if resp.StatusCode != http.StatusOK { return nil, &models.APIError{ Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body)), Code: resp.StatusCode, } } return body, nil } func (c *Client) GetBoards() ([]models.Board, error) { // Поздравляю, кеш if boards, exists := cache.GetCache().GetBoards(); exists { if c.debug { fmt.Printf("[API] Доски загружены из кэша\n") } return boards, nil } url := ApiURL + "/boards/" body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения досок: %w", err) } var boards []models.Board if err := json.Unmarshal(body, &boards); err != nil { if c.debug { fmt.Printf("[API] Ошибка парсинга JSON: %v\n", err) fmt.Printf("[API] Ответ был: %s\n", string(body)) } return nil, fmt.Errorf("ошибка парсинга досок: %w", err) } // Всех победили cache.GetCache().SetBoards(boards) if c.debug { fmt.Printf("[API] Получено досок: %d\n", len(boards)) } return boards, nil } func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) { if threads, exists := cache.GetCache().GetThreads(boardCode); exists { if c.debug { fmt.Printf("[API] Треды загружены из кэша для /%s/\n", boardCode) } return threads, nil } url := fmt.Sprintf("%s/board/%s", ApiURL, boardCode) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения тредов доски %s: %w", boardCode, err) } var threads []models.Thread if err := json.Unmarshal(body, &threads); err != nil { if c.debug { fmt.Printf("[API] Ошибка парсинга JSON тредов: %v\n", err) fmt.Printf("[API] Ответ был: %s\n", string(body)) } return nil, fmt.Errorf("ошибка парсинга тредов: %w", err) } cache.GetCache().SetThreads(boardCode, threads) if c.debug { fmt.Printf("[API] Получено тредов в /%s/: %d\n", boardCode, len(threads)) } return threads, nil } func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail, error) { if thread, exists := cache.GetCache().GetThreadDetail(threadID); exists { if c.debug { fmt.Printf("[API] Тред %d загружен из кэша\n", threadID) } return thread, nil } url := fmt.Sprintf("%s/board/%s/thread/%d", ApiURL, boardCode, threadID) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения треда %d: %w", threadID, err) } var thread models.ThreadDetail if err := json.Unmarshal(body, &thread); err != nil { if c.debug { fmt.Printf("[API] Ошибка парсинга JSON треда: %v\n", err) fmt.Printf("[API] Ответ был: %s\n", string(body)) } return nil, fmt.Errorf("ошибка парсинга треда: %w", err) } cache.GetCache().SetThreadDetail(threadID, &thread) if c.debug { fmt.Printf("[API] Получен тред: ID=%d, Title=%s\n", thread.ID, thread.Title) } return &thread, nil } func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment, error) { if comments, exists := cache.GetCache().GetComments(threadID); exists { if c.debug { fmt.Printf("[API] Комментарии загружены из кэша для треда %d\n", threadID) } return comments, nil } url := fmt.Sprintf("%s/board/%s/thread/%d/comments", ApiURL, boardCode, threadID) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения комментариев треда %d: %w", threadID, err) } var comments []models.Comment if err := json.Unmarshal(body, &comments); err != nil { if c.debug { fmt.Printf("[API] Ошибка парсинга JSON комментариев: %v\n", err) fmt.Printf("[API] Ответ был: %s\n", string(body)) } return nil, fmt.Errorf("ошибка парсинга комментариев: %w", err) } cache.GetCache().SetComments(threadID, comments) if c.debug { fmt.Printf("[API] Получено комментариев: %d\n", len(comments)) } return comments, nil } func (c *Client) GetFullThread(boardCode string, threadID int) (*models.ThreadDetail, []models.Comment, error) { threadChan := make(chan *models.ThreadDetail, 1) commentsChan := make(chan []models.Comment, 1) errorChan := make(chan error, 2) go func() { thread, err := c.GetThread(boardCode, threadID) if err != nil { errorChan <- fmt.Errorf("ошибка получения треда: %w", err) return } threadChan <- thread }() go func() { comments, err := c.GetComments(boardCode, threadID) if err != nil { errorChan <- fmt.Errorf("ошибка получения комментариев: %w", err) return } commentsChan <- comments }() var thread *models.ThreadDetail var comments []models.Comment var errors []error for i := 0; i < 2; i++ { select { case t := <-threadChan: thread = t case c := <-commentsChan: comments = c case err := <-errorChan: errors = append(errors, err) case <-time.After(30 * time.Second): return nil, nil, fmt.Errorf("тайм-аут получения треда") } } if len(errors) > 0 { return nil, nil, errors[0] } return thread, comments, nil } func (c *Client) CreateThread(boardCode, title, text, passcode string) error { if passcode != "" { if err := c.LoginWithPasscode(passcode); err != nil { return fmt.Errorf("ошибка входа с passcode: %w", err) } } formURL := fmt.Sprintf("%s/boards/board/%s/new", c.baseURL, boardCode) resp, err := c.httpClient.Get(formURL) if err != nil { return fmt.Errorf("ошибка получения формы: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ошибка получения формы: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("ошибка чтения формы: %w", err) } csrfToken := extractCSRFToken(string(body)) if csrfToken == "" { return fmt.Errorf("не удалось извлечь CSRF токен") } formData := url.Values{} formData.Set("csrfmiddlewaretoken", csrfToken) formData.Set("title", title) formData.Set("text", text) req, err := http.NewRequest("POST", formURL, bytes.NewBufferString(formData.Encode())) if err != nil { return fmt.Errorf("ошибка создания запроса: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Referer", formURL) resp, err = c.httpClient.Do(req) if err != nil { return fmt.Errorf("ошибка отправки: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { return fmt.Errorf("ошибка сервера: %d", resp.StatusCode) } cache.GetCache().Delete("threads_" + boardCode) return nil } func (c *Client) AddComment(boardCode string, threadID int, text, passcode string) error { if passcode != "" { if err := c.LoginWithPasscode(passcode); err != nil { return fmt.Errorf("ошибка входа с passcode: %w", err) } } formURL := fmt.Sprintf("%s/boards/board/%s/thread/%d/comment", c.baseURL, boardCode, threadID) resp, err := c.httpClient.Get(formURL) if err != nil { return fmt.Errorf("ошибка получения формы: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ошибка получения формы: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("ошибка чтения формы: %w", err) } csrfToken := extractCSRFToken(string(body)) if csrfToken == "" { return fmt.Errorf("не удалось извлечь CSRF токен") } formData := url.Values{} formData.Set("csrfmiddlewaretoken", csrfToken) formData.Set("text", text) req, err := http.NewRequest("POST", formURL, bytes.NewBufferString(formData.Encode())) if err != nil { return fmt.Errorf("ошибка создания запроса: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Referer", formURL) resp, err = c.httpClient.Do(req) if err != nil { return fmt.Errorf("ошибка отправки: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { return fmt.Errorf("ошибка сервера: %d", resp.StatusCode) } cacheKey := fmt.Sprintf("comments_%d", threadID) cache.GetCache().Delete(cacheKey) return nil } func extractCSRFToken(html string) string { re := regexp.MustCompile(`name=['"]csrfmiddlewaretoken['"]\s+value=['"]([^'"]+)['"]`) matches := re.FindStringSubmatch(html) if len(matches) > 1 { return matches[1] } return "" } func min(a, b int) int { if a < b { return a } return b }