483 lines
13 KiB
Go
483 lines
13 KiB
Go
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
|
||
}
|