MobileMkch/api/client.go
2025-08-05 23:54:04 +03:00

483 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}