[C#] Mediawiki API

» опубликован
Под свои нужды понадобилось мне редактировать странички в Mediawiki из кода стороннего приложения.
Сначала я подумал пройтись по дорожке "Mediawiki -> MySQL -> C# -> Unity".
Минус такого подхода минимум в том, что данные в бд хранятся по разному, да и чтобы поставить MySQL под юньку - нужно импортировать библиотеки, а они вставали довольно криво туда.
Но потом понял, что эт всё не нужно - ведь есть чудесный api.php, прилагаемый к Mediawiki.
Искал хорошие решения - они были, но... не под второй дот.нет.
В результате пришлось написать простенький класс самому.
/* 
 * Hello from http://xgm.guru
 * Author: Extravert
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace MediaWikiAPI
{
    public class MediaWiki
    {
        public string addressWiki;
        public string addressAPI;
        
        public CookieContainer cookies;

        public MediaWiki(string address)
        {
            _Init(address);
        }

        public MediaWiki(string address, string user, string password) 
        {
            _Init(address);
            Login(user, password);
        }

        private void _Init(string address)
        {
            addressWiki = address;
            _RepairAddress();
            cookies = new CookieContainer();
        }

        private void _RepairAddress()
        {
            if (string.IsNullOrEmpty(addressWiki))
                throw new Exception("Address is empty");
            if (!addressWiki.StartsWith("http://"))
                addressWiki = "http://" + addressWiki;
            if (!addressWiki.EndsWith("/"))
                addressWiki = addressWiki + "/";
            addressAPI = addressWiki + "api.php?";
        }

        //Commands
        public void Login(string lgname, string lgpassword, string lgdomain = null)
        {
            log("LOGIN({0}, {1}, {2})", lgname, lgpassword, lgdomain);
            var command = CreateCommand(parameter_action.login);
            command.AddField("lgname", lgname);
            command.AddField("lgpassword", lgpassword);
            command.AddField("lgdomain", lgdomain);
            if (command.Go() == "Success") 
                return;
            command.AddField("lgtoken", command.GetAnswerValue("token"));
            var result = command.Go();
            if (result != "Success") 
                throw new Exception("Не получилось залогиниться. Result = " + result);
        }

        public void Logout()
        {
            log("LOGOUT()");
            CreateCommand(parameter_action.logout).Go();
        }

        public void Edit(string title, string text)
        {
            log("EDIT({0}, {1})", title, text);
            var query = CreateCommand(parameter_action.query);
            query.AddField("prop", "info|revisions");
            query.AddField("intoken", "edit");
            query.AddField("titles", title);
            query.Go();
            var token = query["edittoken"];
            var command = CreateCommand(parameter_action.edit);
            command.AddField("title", title);
            command.AddField("text", text);
            command.AddField("token", token);
            var result = command.Go();
            if (result != "Success")
                throw new Exception(result);
        }

        //Простая оболочка для отправки комманд
        private Command CreateCommand()
        {
            return new Command(this);
        }
        private Command CreateCommand(parameter_action action)
        {
            var command = CreateCommand();
            command.AddField(action);
            return command;
        }
        private Command CreateCommand(parameter_action action, string formatStringPostData, params object[] formatObjs)
        {
            var command = CreateCommand();
            command.AddField(action);
            command.AddField(formatStringPostData, formatObjs);
            return command;
        }
        private Command CreateCommand(string formatStringPostData, params object[] formatObjs)
        {
            var command = CreateCommand();
            command.AddField(formatStringPostData, formatObjs);
            return command;
        }
        private Command CreateCommand(string postData)
        {
            var command = CreateCommand();
            command.AddField(postData);
            return command;
        }

        private class Command
        {
            private MediaWiki mediaWiki;
            private List<string> postData = new List<string>();
            public string resultXml;

            public Command(MediaWiki mediaWiki, bool asXml = true)
            {
                this.mediaWiki = mediaWiki;
                if (asXml)
                    AddField(parameter_format.xml);
            }

            public string this[string keyNew]
            {
                get { return GetAnswerValue(keyNew); }
                set { AddField(keyNew, value); }
            }
            
            //Добавляем новые данные
            public void AddField(string key, string value)
            {
                if (value == null)
                    return;
                postData.Add(key + "=" + Uri.EscapeDataString(value));
            }

            public void AddField(parameter_action action)
            {
                postData.Add("action=" + action);
            }

            public void AddField(parameter_format format)
            {
                postData.Add("format=" + format);
            }

            public void AddField(parameter_assert assert)
            {
                postData.Add("assert=" + assert);
            }

            public void AddField(string postDataString)
            {
                postData.Add(postDataString);
            }

            public void AddField(string postDataString, params object[] formatObjs)
            {
                postData.Add(string.Format(postDataString, formatObjs));
            }

            public override string ToString()
            {
                return string.Join("&", postData.ToArray());
            }

            public string GetAnswerValue(string key)
            {
                if (resultXml == null)
                    return null;
                var index = resultXml.IndexOf(key + "=" + "\"", StringComparison.Ordinal);
                if (index == -1)
                    return null;
                index += key.Length + 2;
                var indexSpace = resultXml.IndexOf("\"", index, StringComparison.Ordinal);
                if (index == -1)
                    return null;
                return resultXml.Substring(index, indexSpace - index);
            }

            public string Go(bool getCookies = true, bool allowRedirect = false)
            {
                resultXml = mediaWiki.PostData(ToString(), getCookies, allowRedirect);
                mediaWiki.log(resultXml);
                return this["result"];
            }
        }

        public enum parameter_action
        {
            login, logout, createaccount, query, expandtemplates, parse, opensearch, feedcontributions,
            feedrecentchanges, feedwatchlist, help, paraminfo, rsd, compare, tokens, purge,
            setnotificationtimestamp, rollback, delete, undelete, protect, block, unblock, move, edit, upload,
            filerevert, emailuser, watch, patrol, import, userrights, options, imagerotate, revisiondelete
        }

        public enum parameter_format
        {
            json, jsonfm, php, phpfm, wddx, wddxfm, xml, xmlfm, yaml, yamlfm, rawfm, txt, txtfm, dbg, dbgfm,
            dump, dumpfm, none
        }

        public enum parameter_assert
        {
            user, bot
        }
        
        public string PostData(string postData, bool getCookies,
            bool allowRedirect)
        {
            HttpWebResponse webResp = null;
            for (int errorCounter = 0;; errorCounter++)
            {
                var webReq = (HttpWebRequest)WebRequest.Create(addressAPI);
                webReq.Proxy.Credentials = CredentialCache.DefaultCredentials;
                webReq.UseDefaultCredentials = true;
                webReq.ContentType = "application/x-www-form-urlencoded";
                webReq.Headers.Add("Cache-Control", "no-cache, must-revalidate");
                webReq.UserAgent = "ExBot";
                webReq.AllowAutoRedirect = allowRedirect;
                webReq.CookieContainer = cookies.Count == 0 ? new CookieContainer() : cookies;
                webReq.Headers.Add(HttpRequestHeader.AcceptEncoding, "deflate");
                if (!string.IsNullOrEmpty(postData))
                {
//                if (Bot.isRunningOnMono)    // Monobug 636219 evasion
//                    webReq.AllowAutoRedirect = false;
                    webReq.Method = "POST";
                    webReq.Timeout = 180000;
                    byte[] postBytes = Encoding.UTF8.GetBytes(postData);
                    webReq.ContentLength = postBytes.Length;
                    Stream reqStrm = webReq.GetRequestStream();
                    reqStrm.Write(postBytes, 0, postBytes.Length);
                    reqStrm.Close();
                }
                try
                {
                    webResp = (HttpWebResponse)webReq.GetResponse();
                    break;
                }
                catch (WebException e)
                {
                    string message = e.Message;
                    if (webResp == null)
                        throw;
                    if (webReq.AllowAutoRedirect == false && webResp.StatusCode == HttpStatusCode.Redirect)    // Monobug 636219 evasion
                        return "";
                    if (Regex.IsMatch(message, ": \\(50[02349]\\) "))
                    {    // Remote problem
                        if (errorCounter > 3)
                            throw;
//                        Debug.LogError(message + " " + "Retrying in 60 seconds.");
                        Thread.Sleep(60000);
                    }
                    else if (message.Contains("Section=ResponseStatusLine"))
                        return PostData(postData, getCookies, allowRedirect);
                    else throw;
                }
            }
            var respStream = webResp.GetResponseStream();

            if (webResp.ContentEncoding.ToLower().Contains("deflate"))
                respStream = new DeflateStream(respStream, CompressionMode.Decompress);
            if (getCookies)
            {
                var siteUri = new Uri(addressWiki);
                foreach (Cookie cookie in webResp.Cookies)
                {
                    if (cookie.Domain[0] == '.' &&
                        cookie.Domain.Substring(1) == siteUri.Host)
                        cookie.Domain = cookie.Domain.TrimStart(new[] { '.' });
                    cookies.Add(cookie);
                }
            }
            if (respStream != null)
            {
                var strmReader = new StreamReader(respStream, Encoding.UTF8);
                string respStr = strmReader.ReadToEnd();
                strmReader.Close();
                webResp.Close();
                return respStr;
            }
            return null;
        }

        //ДЛЯ ОТЛАДКИ
        public Action<string> debug { private get; set; }
        private void log(string str)
        {
            if (debug != null)
                debug(str);
        }
        private void log(string str, params object[] objs)
        {
            if (debug != null)
                debug(string.Format(str, objs));
        }
    }
}
Пример применения:
var mediawiki = new MediaWiki("http://xgm.guru/wiki/");
mediawiki.debug = s => Debug.Log(s); //Если не нужен лог для отладки - можно не писать
mediawiki.Login("admin", "qwerty123");
mediawiki.Edit("TestPage", "TestText");
mediawiki.Logout();
Если разобраться в функциях Login/Edit/Logout в принципе можно дополнить класс другими командами. Вся справка по api отображается когда открываешь api.php в браузере.


Просмотров: 896

alexprey #1 - 2 года назад 0
А можно в кратце рассказать, как это применять? Ну в смысле для чего это пригодиться может, пока что идей нет вообще
Devion #2 - 2 года назад 0
alexprey, ну я лично ее писал чтобы сделать автодокументацию. Но пока не очень разобрался как мне достать xml документацию из скриптов в юньке, там у них свои заморочки ибо компилятор свой используют.
Тобиш мысль такая - пишу код, сохраняю, в инетике автоматически складывается документация по написанной библиотеке. Удобно же.
Пока отложил эту задачку в сторону, но мб еще осознаю как это сделать без гемора.