// Copyright 2013 James McGuire // This code is covered under the MIT License // Please refer to the LICENSE file in the root of this // repository for any information. // go-mediawiki provides a wrapper for interacting with the Mediawiki API // // Please see http://www.mediawiki.org/wiki/API:Main_page // for any API specific information or refer to any of the // functions defined for the MWApi struct for information // regarding this specific implementation. // // The client subdirectory contains an example application // that uses this API. package mediawiki import ( "bytes" "encoding/json" "errors" "io" "io/ioutil" "mime/multipart" "net/http" "net/http/cookiejar" "net/url" "strings" ) // The main mediawiki API struct, this is generated via mwapi.New() type MWApi struct { Username string Password string Domain string userAgent string debug bool url *url.URL client *http.Client format string edittoken string } // This is used for passing data to the mediawiki API via key=value in a POST type Values map[string]string // Unmarshal login data... type outerLogin struct { Login loginResponse } type loginResponse struct { Result string Token string } // Unmarshall response from page edits... type outerEdit struct { Edit edit } type edit struct { Result string PageId int Title string OldRevId int NewRevId int } // General query response from mediawiki type mwQuery struct { Query query } type query struct { Pages map[string]page } type page struct { Pageid int Ns float64 Title string Touched string Lastrevid float64 // This will appear as both a string and a number... and the JSON unmarshaler // will crap out if this isn't set to a string. //Counter string Length float64 Edittoken string Revisions []revision Imageinfo []image } type revision struct { Body string `json:"*"` User string Timestamp string comment string } type image struct { Url string Descriptionurl string } type mwError struct { Error errorType } type errorType struct { Code string Info string } type uploadResponse struct { Upload uploadResult } type uploadResult struct { Result string } // Helper function for translating mediawiki errors in to golang errors. func CheckError(response []byte) error { var mwerror mwError err := json.Unmarshal(response, &mwerror) if err != nil { return nil } else if mwerror.Error.Code != "" { return errors.New(mwerror.Error.Code + ": " + mwerror.Error.Info) } else { return nil } } // Generate a new mediawiki API struct // // Example: mwapi.New("http://en.wikipedia.org/w/api.php", "My Mediawiki Bot") // Returns errors if the URL is invalid func New(wikiUrl, userAgent string) (*MWApi, error) { cookiejar, err := cookiejar.New(nil) if err != nil { return nil, err } client := http.Client{ Transport: nil, CheckRedirect: nil, Jar: cookiejar, } clientUrl, err := url.Parse(wikiUrl) if err != nil { return nil, err } return &MWApi{ url: clientUrl, client: &client, format: "json", userAgent: "go-mediawiki https://github.com/sadbox/go-mediawiki " + userAgent, debug: false, }, nil } // This will automatically add the user agent and encode the http request properly func (m *MWApi) postForm(query url.Values) ([]byte, error) { request, err := http.NewRequest("POST", m.url.String(), strings.NewReader(query.Encode())) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("user-agent", m.userAgent) resp, err := m.client.Do(request) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if err = CheckError(body); err != nil { return nil, err } return body, nil } // Download a file. // // Returns a readcloser that must be closed manually. Refer to the // example app for additional usage. func (m *MWApi) Download(filename string) (io.ReadCloser, error) { // First get the direct url of the file query := Values{ "action": "query", "prop": "imageinfo", "iiprop": "url", "titles": filename, } body, _, err := m.API(query) if err != nil { return nil, err } var response mwQuery err = json.Unmarshal(body, &response) if err != nil { return nil, err } var fileurl string for _, page := range response.Query.Pages { if len(page.Imageinfo) < 1 { return nil, errors.New("No file found") } fileurl = page.Imageinfo[0].Url break } // Then return the body of the response request, err := http.NewRequest("GET", fileurl, nil) if err != nil { return nil, err } request.Header.Set("user-agent", m.userAgent) resp, err := m.client.Do(request) if err != nil { return nil, err } return resp.Body, nil } // Upload a file // // This does a simple, but more error-prone upload. Mediawiki // has a chunked upload version but it is only available in newer // versions of the API. // // Automatically retrieves an edit token if necessary. func (m *MWApi) Upload(dstFilename string, file io.Reader) error { if m.edittoken == "" { err := m.GetEditToken() if err != nil { return err } } query := Values{ "action": "upload", "filename": dstFilename, "token": m.edittoken, "format": m.format, } buffer := &bytes.Buffer{} writer := multipart.NewWriter(buffer) for key, value := range query { err := writer.WriteField(key, value) if err != nil { return err } } part, err := writer.CreateFormFile("file", dstFilename) _, err = io.Copy(part, file) if err != nil { return err } err = writer.Close() if err != nil { return err } request, err := http.NewRequest("POST", m.url.String(), buffer) if err != nil { return err } request.Header.Set("Content-Type", "multipart/form-data; boundary="+writer.Boundary()) request.Header.Set("user-agent", m.userAgent) resp, err := m.client.Do(request) if err != nil { return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } if err = CheckError(body); err != nil { return err } var response uploadResponse err = json.Unmarshal(body, &response) if err != nil { return err } if response.Upload.Result == "Success" || response.Upload.Result == "Warning" { return nil } else { return errors.New(response.Upload.Result) } } // Login to the Mediawiki Website // // This will throw an error if you didn't define a username // or password. func (m *MWApi) Login() error { if m.Username == "" || m.Password == "" { return errors.New("Username or password not set.") } query := Values{ "action": "login", "lgname": m.Username, "lgpassword": m.Password, } if m.Domain != "" { query["lgdomain"] = m.Domain } body, _, err := m.API(query) if err != nil { return err } var response outerLogin err = json.Unmarshal(body, &response) if err != nil { return err } if response.Login.Result == "Success" { return nil } else if response.Login.Result != "NeedToken" { return errors.New("Error logging in: " + response.Login.Result) } // Need to use the login token query["lgtoken"] = response.Login.Token body, _, err = m.API(query) if err != nil { return err } err = json.Unmarshal(body, &response) if err != nil { return err } if response.Login.Result == "Success" { return nil } else { return errors.New("Error logging in: " + response.Login.Result) } } // Get an edit token // // This is necessary for editing any page. // // The Edit() function will call this automatically // but it is available if you want to make direct // calls to API(). func (m *MWApi) GetEditToken() error { query := Values{ "action": "query", "prop": "info|revisions", "intoken": "edit", "titles": "Main Page", } body, _, err := m.API(query) if err != nil { return err } var response mwQuery err = json.Unmarshal(body, &response) if err != nil { return err } for _, value := range response.Query.Pages { m.edittoken = value.Edittoken break } return nil } // Log out of the mediawiki website func (m *MWApi) Logout() { m.API(Values{"action": "logout"}) } // Edit a page // // This function will automatically grab an Edit Token if there // is not one currently stored. // // Example: // // editConfig := mediawiki.Values{ // "title": "SOME PAGE", // "summary": "THIS IS WHAT SHOWS UP IN THE LOG", // "text": "THE ENTIRE TEXT OF THE PAGE", // } // err = client.Edit(editConfig) func (m *MWApi) Edit(values Values) error { if m.edittoken == "" { err := m.GetEditToken() if err != nil { return err } } query := Values{ "action": "edit", "token": m.edittoken, } body, _, err := m.API(query, values) if err != nil { return err } var response outerEdit err = json.Unmarshal(body, &response) if err != nil { return err } if response.Edit.Result == "Success" { return nil } else { return errors.New(response.Edit.Result) } } // Request a wiki page and it's metadata. func (m *MWApi) Read(pageName string) (*revision, error) { query := Values{ "action": "query", "prop": "revisions", "titles": pageName, "rvlimit": "1", "rvprop": "content|timestamp|user|comment", } body, _, err := m.API(query) var response mwQuery err = json.Unmarshal(body, &response) if err != nil { return nil, err } for _, page := range response.Query.Pages { return &page.Revisions[0], nil } return nil, errors.New("No revisions found") } // A generic interface to the Mediawiki API // Refer to the mediawiki API reference for any information regarding // what to pass to this function // // This is used by all internal functions to interact with the API // // The second return is simply the json data decoded in to an empty interface // that can be used by something like https://github.com/jmoiron/jsonq func (m *MWApi) API(values ...Values) ([]byte, interface{}, error) { query := m.url.Query() for _, valuemap := range values { for key, value := range valuemap { query.Set(key, value) } } query.Set("format", m.format) body, err := m.postForm(query) if err != nil { return nil, nil, err } var unmarshalto interface{} err = json.Unmarshal(body, &unmarshalto) if err != nil { return nil, nil, err } return body, unmarshalto, nil }