Automating KiGG Publishing

by KodefuGuru 14. July 2009 19:17

This is part 1 of a series on KiGG Automation.

Part 1: Automating KiGG Publishing
Part 2: KiGG’s Story Summary 
Part 3: Automating KiGG Submission

When I set up my first KiGG site, I was surprised to discover that I had to manually publish articles. I assumed it would be an automated process that would run once a day. Since there are times I may not be able to log into my website, I set about figuring out how to automate the process.

I should note that during this process, I didn’t use best practices. I had one requirement: make a program that I can schedule to publish stories on KiGG. I wasn’t really sure what I would need to go about doing that. So, I did what I assume most developers do: create a console application prototype. I don’t claim this code is perfect, but if you want classes to automate publishing in KiGG (or control MVC applications), this will do the trick.

KiGG is an ASP.NET MVC application. This made it easy to figure out where to begin. I wanted to Publish, so I needed to find the admin link and see what it called. I guessed it would be something like, /Publish, and I was correct.

scriptManager.RegisterOnReady("Administration.set_publishUrl('{0}');"
    .FormatWith(Url.RouteUrl("Publish")));

set_publishUrl is simply a setter in the JavaScript to prevent a url from being hardcoded in script. Looking over the JavaScript I could tell there wasn’t much more needed than calling the proper url.

$.ajax(
    {
        url: Administration._publishUrl,
        type: 'POST',
        dataType: 'json',
        data : '__MVCASYNCPOST=true', // a fake param to fool iis for content-lenth,
        beforeSend: function()
        {
            $U.showProgress('Publishing stories...');
        },
        success: function(result)
        {
            $U.hideProgress();

            if (result.isSuccessful)
            {
                $U.messageBox('Success', 'Story publishing process completed.', false);
            }
            else
            {
                $U.messageBox('Error', result.errorMessage, true);
            }
        }
    }
);

The script is doing a POST on the /Publish url with fake data (to fool IIS?).

Next, it was time to check the routing for /Publish. Routing in KiGG is defined in Kigg.Web\BootstrapperTasks\RegisterRoutes.cs. It was done this way to be consistent with the Open Closed Principle.

_routes.MapRoute("Publish", "Publish", 
    new { controller = "Story", action = "Publish" });

Now we know the entry to this is in the story controller and that the method is Publish. The story controller being used is defined within the web.config file using Unity.

<typeAlias alias="StoryController" type="Kigg.Web.StoryController, Kigg.Web"/>

I set up a break point and wrote a quick routine to post to the publish url.

var request = WebRequest.Create(http://localhost:1736/Publish);
request.ContentType = "application/x-www-form-urlencoded"; request.Method = "POST"; var form = Encoding.ASCII.GetBytes("__MVCASYNCPOST=true"); using (Stream requestStream = request.GetRequestStream()) { requestStream.Write(form, 0, form.Length); requestStream.Close(); } var response = request.GetResponse(); using (StreamReader reader = new StreamReader(response.GetResponseStream())) { var responseText = reader.ReadToEnd(); Debug.WriteLine(responseText); reader.Close(); }

Everything executed fine, but the return message indicated something was wrong: “{"isSuccessful":false,"errorMessage":"You are currently not authenticated."}.” It’s pretty obvious that authentication would be necessary, otherwise I could publish DotNetShoutOut whenever I wanted. Unfortunately, the obvious way to authenticate does not work.

request.Credentials = new NetworkCredential("admin", "password");

Since it is a NetworkCredential, I assume this code would work with Windows authentication turned on. However, we’re using forms authentication. I looked at the routes and found one for Login and Logout. At this point it was clear that I would be calling post multiple times, so I moved the code to a method that would accept a url and formdata as a string.

Post("http://localhost:1736/Login", "userName=admin&password=password");
Post("http://localhost:1736/Publish", "__MVCASYNCPOST=true");
Post("http://localhost:1736/Logout", "__MVCASYNCPOST=true");

This returned the following results:

{"isSuccessful":true,"errorMessage":null}
{"isSuccessful":false,"errorMessage":"You are currently not authenticated."}
{"isSuccessful":false,"errorMessage":"You are currently not logged in."}

I put a breakpoint in the Login method of the MembershipController, and from there was able to see what was happening. Although it was a few levels deep, it was clear that FormsAuthentication.SetAuthCookie was the important piece of code; I needed a way to retain cookies between requests.

Cookies are only available by using HttpWebRequest and HttpWebReponse rather than their base classes. So, I cast my variables to the proper class. One odd thing I found was the cookies have to be retrieved from the request rather than the response after you have requested the response. I wrapped this functionality up in a WebSession class.

public class WebSession
{
    protected CookieCollection Cookies { get; set; }

    public WebSession()
    {
        Cookies = new CookieCollection();
    }

    public string Post(string url, HttpDictionary formData)
    {
        return Post(url, formData.ToString());
    }

    public string Post(string url, string formData)
    {            
        var request = WebRequest.Create(url) as HttpWebRequest;
        if (request == null)
        {
            throw new HttpException();
        }
        request.ContentType = "application/x-www-form-urlencoded";
        request.Method = "POST";
        request.CookieContainer = new CookieContainer();
        request.CookieContainer.Add(request.RequestUri, Cookies);

        var form = Encoding.ASCII.GetBytes(formData);
        WriteToRequest(request, form);

        var response = request.GetResponse() as HttpWebResponse;
        if (response == null)
        {
            throw new HttpException();
        }

        var responseText = ReadResponse(response);
        Cookies = request.CookieContainer.GetCookies(request.RequestUri);

        Debug.WriteLine(responseText);
        
        return responseText;
    }

    private static void WriteToRequest(WebRequest request, byte[] form)
    {
        using (Stream requestStream = request.GetRequestStream())
        {
            requestStream.Write(form, 0, form.Length);
            requestStream.Close();
        }
    }

    private string ReadResponse(WebResponse response)
    {
        using (StreamReader reader = 
            new StreamReader(response.GetResponseStream()))
        {
            var responseText = reader.ReadToEnd();
            reader.Close();
            return responseText;
        }
    }
}

If you look closely you’ll notice an HttpDictionary class. This is because I wanted to treat the form data as a dictionary rather than a string. I suppose this could be written as an extension method on Dictionary<string, string> instead.

public class HttpDictionary : Dictionary<string, string>
{
    public override string ToString()
    {
        return this.Select(q => EncodePair(q))
            .Aggregate((a, b) => a + "&" + b);
    }

    private string EncodePair(KeyValuePair<string, string> pair)
    {
        return HttpUtility.HtmlEncode(pair.Key) +
            "=" + HttpUtility.HtmlEncode(pair.Value);
    }
}

I also felt it necessary to wrap up the result received from KiGG in a class that can be used. Otherwise, it’s difficult to know whether or not the call was successful. To convert from Json, I used Json.NET.

public class KiggResult
{        
    [JsonProperty(PropertyName="isSuccessful")]
    public bool IsSuccessful{ get; set; }

    [JsonProperty(PropertyName = "errorMessage")]
    public string ErrorMessage { get; set; }

    public static KiggResult Create(string json)
    {
        return JsonConvert.DeserializeObject<KiggResult>(json);
    }
}

Finally, I made a KiggBot class to provide us an interface that makes sense for accessing a KiGG site. This can be changed to throw an exception if the result indicates the call was not successful.

public class KiggBot 
{
    private static readonly HttpDictionary mvcAsyncPost = new HttpDictionary
    {
        {"__MVCASYNCPOST", "true"}
    };

    private WebSession session;
    private string url;

    public KiggBot(string url)
    {
        if (!url.EndsWith("/"))
            url += "/";

        this.url = url;
        session = new WebSession();
    }

    public bool Login(string userName, string password)
    {
        HttpDictionary dictionary = new HttpDictionary { 
            {"userName", userName}, {"password", password}};

        var result = KiggResult.Create(
            session.Post(url + "Login", dictionary));
        
        return result.IsSuccessful;
    }

    public bool Logout()
    {
        var result = KiggResult.Create(
            session.Post(url + "Logout", mvcAsyncPost));

        return result.IsSuccessful;
    }

    public bool Publish()
    {
        var result = KiggResult.Create(
            session.Post(url + "Publish", mvcAsyncPost));

        return result.IsSuccessful;
    }
}

With these classes, it’s fairly easy to create an automated publishing program. At the very least, write a console app and use scheduler to fire it daily.

KiggBot bot = new KiggBot("http://localhost:1736");
bot.Login("admin", "password");
bot.Publish();
bot.Logout();
Hope that helps. If anyone makes a cool GUI for this and releases it, might I recommend PiGG?

Tags: , ,

Kodefu

Comments

7/14/2009 7:18:36 PM #

trackback

Trackback from DotNetKicks.com

Automating KiGG Publishing

DotNetKicks.com

7/14/2009 7:20:45 PM #

trackback

Trackback from DotNetShoutout

Automating KiGG Publishing

DotNetShoutout

7/14/2009 7:38:09 PM #

trackback

Trackback from WebDevVote.com

Automating KiGG Publishing

WebDevVote.com

7/14/2009 9:59:04 PM #

pingback

Pingback from adobelearn.com

Automating KiGG Publishing | Adobe Tutorials

adobelearn.com

7/15/2009 1:35:23 AM #

pingback

Pingback from web2designer.org

News   Automating KiGG Publishing | Web 2.0 Designer

web2designer.org

7/15/2009 8:47:30 AM #

trackback

Trackback from NewsPeeps

Automating KiGG Publishing

NewsPeeps

7/15/2009 4:30:57 PM #

Muhammad Mosa

This is one wonderful post about automated publishing in KiGG.
Really a killer post.

Muhammad Mosa Egypt

7/30/2009 6:36:29 PM #

trackback

Trackback from KodefuGuru

Automating KiGG Submission

KodefuGuru

9/7/2009 2:11:24 PM #

Antonio Chagoury

Great article.

The issue with automating the publishing is that you are really throwing away moderation. For example, some stories end up in the "Unapproved" pile. Some of those, even though did not meet the SPAM filters requirements, are good stories - some of course not.

I think it would be good to build a method to retrieve a list of unpublished stories, and provide a method to approve only selected stories.

I will be using your code in the submission article to build a tool that reads from an RSS feed and submit all in one shot - maybe we'll call that one HoGG! Smile

Antonio Chagoury United States

9/7/2009 9:13:54 PM #

Chris

If you have your notifications turned on, you should receive an email if a story is submitted and unapproved.

I have the code to read in RSS feeds and submit the stories on a timed basis, but I haven't published it yet because it requires extensive mods to Categories. Be careful reading Feedburner because you'll end up submitting Feedproxy urls =). I'll write a quick article tomorrow how to solve that problem (it's easy).

One thing to note: I screwed up the Dictionary code. Use UrlEncode not HtmlEncode.

Chris United States

9/9/2009 1:22:28 AM #

Neb

Great article. It would be great to extend Kigg to have a feed reading capability and publishing method like the one you posted.

Neb

9/9/2009 2:55:37 AM #

Neb

I got some questions
1. Where do you set up the admin user name and password.
2. Is there a separate page on kigg for web administrator.
3. How come there is not index or default page on the project??

Neb

9/9/2009 9:51:58 AM #

chris

1. The web.config. When you first run KiGG it will install the default users from the config file.
2. When you login as administrator, there is an admin panel. There really isn't an admin screen per se, but there is extra functionality on some of the views.
3. KiGG is an ASP.NET MVC application. Its default route goes to Publish action on the Story controler, which returns the List view in Views\Story.

chris United States

9/11/2009 12:58:57 AM #

Neb

I tried the above code and to test it I tried to manually add new 5 stories. Then I ran the above code Which run successfully. But when I login onto my site as admin all the 4 stories are listed in the new category field of the control panel. So my question is does it take time for the stories to get published? I ask this because even if I login as admin and publish stories, stories gets published on the second day.

Neb

9/14/2009 12:09:30 PM #

chris

There's a rule that stories must be posted for at least 4 hours before they can be published.

chris United States

10/4/2009 4:32:47 PM #

Neb

I got two questions.
How do you modify the icon of this site. I tried editing fav.ico in assets/images folder. It didnot work.
Second, Is there a certain rule which shows stories in a particular order. As I could see so far the last story published is on top.

Neb

10/5/2009 10:03:12 PM #

chris

The fav.ico will be cached by your browser, so you won't see the changes locally without clearing the cache and restarting the browser.

For the second question, without looking I believe that is controlled by the storyrepository.

chris United States

11/23/2009 5:06:51 AM #

Geneboss

Hi Chris,
Thank you for the wonderful solution to KiGG Automation.
Could you please provide with source codes of all 3 parts?

Geneboss United States

11/29/2009 10:25:01 PM #

Geneboss

Hi Chris,
I guess I need to be more specific to deserve an answer.
I know you have the codes explained in your blog. What I meant was like a C# source code that I can download. I tried your instructions but unfortunatly ended up with errors and could not deploy it properly. Being a novice programmer, I tought

Geneboss United States

11/29/2009 10:59:49 PM #

Chris

The source code I have isn't prepared for distribution. I will contact you directly. Really, I should go ahead and prep it for release on Codeplex.

Chris United States

12/2/2009 4:54:06 AM #

Geneboss

Hi Chris,
Thanks again for your email. I'm still waiting for your response.

Geneboss United States

Add comment




  Country flag

biuquote
  • Comment
  • Preview
Loading



Powered by BlogEngine.NET 1.6.0.0
Theme by Mads Kristensen

Whois KodefuGuru

Chris Eargle

Chris Eargle
.NET Community Champion

LinkedIn Twitter Technorati Facebook

MVP - Visual C#

 

INETA Community Champions
Friend of RedGate
Telerik .NET Ninja
Community blogs & blog posts

I am a #52er


World Map

RecentComments

Comment RSS

Tag cloud

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2010