Initial commit
This commit is contained in:
57
scripts/rsw/internal/wiki/client.go
Normal file
57
scripts/rsw/internal/wiki/client.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const userAgent = "rsw-cli/1.0 (RuneScape Wiki CLI tool; https://github.com/runescape-wiki/rsw)"
|
||||
|
||||
// Client wraps HTTP requests to the MediaWiki API.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a wiki API client for the given base URL.
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// get performs a GET request with the given parameters and decodes JSON into dest.
|
||||
func (c *Client) get(params url.Values, dest interface{}) error {
|
||||
params.Set("format", "json")
|
||||
|
||||
reqURL := fmt.Sprintf("%s?%s", c.baseURL, params.Encode())
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
|
||||
return fmt.Errorf("decoding response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
135
scripts/rsw/internal/wiki/parse.go
Normal file
135
scripts/rsw/internal/wiki/parse.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Section represents a section of a wiki page.
|
||||
type Section struct {
|
||||
Index string `json:"index"`
|
||||
Level string `json:"level"`
|
||||
Line string `json:"line"`
|
||||
Number string `json:"number"`
|
||||
Anchor string `json:"anchor"`
|
||||
}
|
||||
|
||||
// ParsedPage contains the result of parsing a wiki page.
|
||||
type ParsedPage struct {
|
||||
Title string `json:"title"`
|
||||
PageID int `json:"pageid"`
|
||||
Wikitext string // Raw wikitext of the page or section
|
||||
HTML string // Rendered HTML
|
||||
Sections []Section // Table of contents
|
||||
}
|
||||
|
||||
// parseResponse wraps the API response for action=parse.
|
||||
type parseResponse struct {
|
||||
Parse struct {
|
||||
Title string `json:"title"`
|
||||
PageID int `json:"pageid"`
|
||||
Wikitext struct {
|
||||
Content string `json:"*"`
|
||||
} `json:"wikitext"`
|
||||
Text struct {
|
||||
Content string `json:"*"`
|
||||
} `json:"text"`
|
||||
Sections []Section `json:"sections"`
|
||||
} `json:"parse"`
|
||||
Error *struct {
|
||||
Code string `json:"code"`
|
||||
Info string `json:"info"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// GetPage fetches the full wikitext and section list for a page.
|
||||
func (c *Client) GetPage(title string) (*ParsedPage, error) {
|
||||
params := url.Values{
|
||||
"action": {"parse"},
|
||||
"page": {title},
|
||||
"prop": {"wikitext|sections"},
|
||||
"redirects": {"1"},
|
||||
}
|
||||
|
||||
var resp parseResponse
|
||||
if err := c.get(params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
|
||||
}
|
||||
|
||||
return &ParsedPage{
|
||||
Title: resp.Parse.Title,
|
||||
PageID: resp.Parse.PageID,
|
||||
Wikitext: resp.Parse.Wikitext.Content,
|
||||
Sections: resp.Parse.Sections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPageSection fetches the wikitext for a specific section of a page.
|
||||
func (c *Client) GetPageSection(title string, sectionIndex int) (*ParsedPage, error) {
|
||||
params := url.Values{
|
||||
"action": {"parse"},
|
||||
"page": {title},
|
||||
"prop": {"wikitext"},
|
||||
"section": {strconv.Itoa(sectionIndex)},
|
||||
}
|
||||
|
||||
var resp parseResponse
|
||||
if err := c.get(params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
|
||||
}
|
||||
|
||||
return &ParsedPage{
|
||||
Title: resp.Parse.Title,
|
||||
PageID: resp.Parse.PageID,
|
||||
Wikitext: resp.Parse.Wikitext.Content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPageHTML fetches the rendered HTML for a page.
|
||||
func (c *Client) GetPageHTML(title string) (*ParsedPage, error) {
|
||||
params := url.Values{
|
||||
"action": {"parse"},
|
||||
"page": {title},
|
||||
"prop": {"text|sections"},
|
||||
}
|
||||
|
||||
var resp parseResponse
|
||||
if err := c.get(params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
|
||||
}
|
||||
|
||||
return &ParsedPage{
|
||||
Title: resp.Parse.Title,
|
||||
PageID: resp.Parse.PageID,
|
||||
HTML: resp.Parse.Text.Content,
|
||||
Sections: resp.Parse.Sections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FindSectionIndex searches the section list for a section matching the given name.
|
||||
// Returns -1 if not found.
|
||||
func FindSectionIndex(sections []Section, name string) int {
|
||||
for _, s := range sections {
|
||||
if s.Line == name {
|
||||
idx, err := strconv.Atoi(s.Index)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
66
scripts/rsw/internal/wiki/search.go
Normal file
66
scripts/rsw/internal/wiki/search.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SearchResult represents a single search hit from the wiki.
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"`
|
||||
Snippet string `json:"snippet"`
|
||||
PageID int `json:"pageid"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// searchResponse is the raw API response shape for action=query&list=search.
|
||||
type searchResponse struct {
|
||||
Query struct {
|
||||
Search []SearchResult `json:"search"`
|
||||
} `json:"query"`
|
||||
}
|
||||
|
||||
// Search performs a full-text search across wiki pages.
|
||||
func (c *Client) Search(query string, limit int) ([]SearchResult, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
params := url.Values{
|
||||
"action": {"query"},
|
||||
"list": {"search"},
|
||||
"srsearch": {query},
|
||||
"srlimit": {strconv.Itoa(limit)},
|
||||
"srprop": {"snippet|size"},
|
||||
}
|
||||
|
||||
var resp searchResponse
|
||||
if err := c.get(params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Strip HTML tags from snippets (the API returns <span class="searchmatch">...</span>)
|
||||
for i := range resp.Query.Search {
|
||||
resp.Query.Search[i].Snippet = stripHTML(resp.Query.Search[i].Snippet)
|
||||
}
|
||||
|
||||
return resp.Query.Search, nil
|
||||
}
|
||||
|
||||
// stripHTML removes HTML tags from a string. Lightweight, no external dep.
|
||||
func stripHTML(s string) string {
|
||||
var b strings.Builder
|
||||
inTag := false
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r == '<':
|
||||
inTag = true
|
||||
case r == '>':
|
||||
inTag = false
|
||||
case !inTag:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user