[Windows Mobile .NET CF] 拍照上傳到 Flickr 再加 Plurk! – Day4

[Windows Mobile .NET CF] 拍照上傳到 Flickr 再加 Plurk! – Day4

上傳到 Flickr …
嗯, 再加上打串文字訊息就很適合玩噗浪囉~~~

Flickr 有 API, Plurk 也有非官方的 API for C# http://code.google.com/p/plurkapi/
無奈微軟的 .NET CF 常常缺東缺西, 所以我們就要開始負責補上許多缺失囉.

首先, 一樣把 API 的 source code 下載回來, 我們放到 CameraNow 的專案當中,
image

你可以看到, .NET CF 連 HttpUtility 都沒提供…
所以只好依賴網路的善心人士提供了 HttpUtility 的 code

然後呢, .NET CF 的 HttpWebRequest 也不支援 cookie,
這對 Plurk API 來說是致命傷啊,
所以我也只好寫一個簡單輔助讀取 cookie 的 class
(好在 .NET CF 至少提供了 header , 而 cookie 可以從 header 中讀取或設定)
我並不需要設定 cookie value, 所以僅實做了 cookie 的讀取與丟回去的動作.
 

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Net;

namespace PlurkApi
{
    public class CookieContainer
    {
        private List<Cookie> cookies;

        public CookieContainer()
        {
            cookies = new List<Cookie>();
        }

        public static void SetToWebRequest(HttpWebRequest request, CookieContainer container)
        {
            List<string> cookievalues = new List<string>();
            foreach (Cookie cookie in container.cookies) {
                cookievalues.Add(cookie.Key+"="+cookie.Value);
            }
            request.Headers.Add("Cookie",string.Join(";", cookievalues.ToArray()));
        }

        public static CookieContainer GetFromWebResponse(HttpWebResponse response)
        {
            CookieContainer c = new CookieContainer();
            string setcookie = response.Headers["Set-Cookie"];
            if (string.IsNullOrEmpty(setcookie) == false)
            {
                string[] parts = setcookie.Split(new char[] {';'});
                if (parts.Length > 0)
                {
                    string cpt = parts[0].Trim();
                    int idx = cpt.IndexOf('=');
                    if (idx > 0)
                    {
                        string key = cpt.Substring(0, idx);
                        string value = cpt.Substring(idx + 1, cpt.Length - idx-1);
                        c.Add(new Cookie()
                        {
                            Key = key,
                            Value = value,
                        });
                    }
                }
            }
            return c;
        }

        public void Add(Cookie cookie)
        {
            cookies.Add(cookie);
        }

        public Cookie GetCookie(string key)
        {
            return cookies.Find(c => string.Compare(c.Key, key) == 0);
        }
    }

    public class Cookie
    {
        public string Key { get; set; }
        public string Value { get; set; }
    }
}

最後, 也是令我最頭痛的.
就是因為 HttpWebRequest 不支援 cookie, 所以那個 C# 的 plurk API 幾乎要全改了…
所以我只好花時間研究一下 plurk API 囉, 以下是幾個心得:


1. .NET CF 的 HttpWebRequest 有一個屬性為 AllowWriteStreamBuffering , 基於效能因素, 預設是 false.
    但是要設為 true 才行, MSDN 的說明是

Windows Mobile for Pocket PC, Windows Mobile for Smartphone, Windows CE 平台注意事項: 基於效能考量,這個屬性的預設值為 false;然而,重要的是要注意,如果動作必須有實體 (Entity) 資料 (例如 POST),則重新導向和驗證可能不會發生。若要實作 HTTP 要求的完整 .NET Framework 行為,請將 AllowWriteStreamBuffering 設為 true

    但是老實說我也看不大懂這中文…, 只知道要設為 true , 不然使用的時候偶而就丟 exception 給你看!

2. .NET CF 的 HttpWebRequest 雖然支援了 AllowAutoRedirect (重新導向) , 可偏偏因為它沒有實做 Cookie…
    一整個失敗! 你可以想像, 在重新導向的過程中, 存放在 header 的 cookie 就這樣遺落在沒人知道的記憶體空間中了…
    所以要設為 false, 還要自己一邊處理重新導向, 一邊搞定 cookie 的存取問題.
    (這個問題還需要透過 fiddler 之類的工具才發現問題, 簡直是在考驗工程師對 TCP/IP 的熟悉程度)
 

3. 手機上的網路比較慢 (3G or 3.5G) , 而 plurk 有針對手機做了對應的網頁 : http://www.plurk.com/m
所以 plurk API 應該要參考手機網頁的 protocol 來設計, 這樣速度快了不少, 傳輸的資料也少了很多.
原本的 plurk API 針對網頁版本的 protocol 設計, 不僅速度慢, 流量也多很多.

最後, 以下這個函式就是負責跟 plurk web 溝通的主要函式:

 

/// <summary>
/// Use HttpWebRequest to get web page
/// </summary>
/// <param name="url"></param>
/// <param name="parameters"> null means no parameters.</param>
/// <param name="cookie"></param>
/// <param name="waitContent"></param>
/// <returns></returns>
public string GetPage(String url, Dictionary<string,string> parameters, ref Cookie cookie, Boolean waitContent)
{
    HttpWebResponse response = null;
    try
    {
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.UserAgent = "CameraNow PlurkAPI";
        request.AllowWriteStreamBuffering = true;
        request.AllowAutoRedirect = false;

        if (cookie != null)
        {
            CookieContainer container = new CookieContainer();
            container.Add(cookie);
            CookieContainer.SetToWebRequest(request, container);
        }

        if (parameters != null)
        {
            request.Method = "POST";
            List<string> postparameters = new List<string>();
            foreach (var p in parameters)
            {
                if (p.Value != null)
                    postparameters.Add(p.Key + "=" + HttpUtility.UrlEncode(p.Value));
            }
            string postcontent = string.Join("&", postparameters.ToArray());
            byte[] byteBuffer = Encoding.UTF8.GetBytes(postcontent);
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = byteBuffer.Length;
            using (Stream requestStream = request.GetRequestStream())
            {
                requestStream.Write(byteBuffer, 0, byteBuffer.Length);
            }
        }
        else
        {
            request.Method = "GET";
        }

        //received data
        response = (HttpWebResponse)request.GetResponse();

        //reset cookie if we got cookie.
        Cookie getcookie = CookieContainer.GetFromWebResponse(response).GetCookie("plurkcookiea");
        if (getcookie != null)
            cookie = getcookie;

        if (waitContent)
        {
            using (Stream responseStream = response.GetResponseStream())
            {
                StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.UTF8);
                return reader.ReadToEnd();
            }
        }
        else
        {
            return string.Empty;
        }
    }
    finally
    {
        // close and clean 
        if (response != null)
            response.Close();
    }
}

 

要登入 Plurk, 就是要帳號跟密碼, 而手機上的程式當然要以方便為原則,
登入後就不要再輸入帳號密碼了, 因為安全性沒辦法做到像 Flickr 這麼好 (這需要官方支援)
所以我就先把帳號密碼存在 settings.xml 當中.
當然, 講到帳號密碼, 就是要加密!
所以以下示範加密解密的程式碼 (在 Settings 這個 class 當中的片段)

/// <summary>
/// 相機解析度
/// </summary>
public System.Drawing.Size defaultCaptureSize { get; set; }

/// <summary>
/// plurk 帳號
/// </summary>
public string PlurkAccount { get; set; }

/// <summary>
/// 加密過的 plurk 密碼
/// </summary>
public string EncryptedPlurkPassword { get; set; }

private static byte[] DesKey = new byte[] { 32, 21, 3, 63, 12, 33, 67, 93 };
private static byte[] DesIV = new byte[] { 9,42,109,239,29,52,34,21 };

[XmlIgnore]
public string PlurkPassword
{
    get
    {
        if (string.IsNullOrEmpty(EncryptedPlurkPassword) == true)
            return string.Empty;

        DESCryptoServiceProvider des = new DESCryptoServiceProvider();
        using (var enc = des.CreateDecryptor(DesKey, DesIV))
        {
            byte[] encrypted = Convert.FromBase64String(EncryptedPlurkPassword);
            byte[] decrypted = enc.TransformFinalBlock(encrypted, 0, encrypted.Length);
            return Encoding.UTF8.GetString(decrypted, 0, decrypted.Length);
        }
    }
    set
    {
        if (string.IsNullOrEmpty(value) == true)
        {
            EncryptedPlurkPassword = string.Empty;
        }
        else
        {
            DESCryptoServiceProvider des = new DESCryptoServiceProvider();
            using (var dec = des.CreateEncryptor(DesKey, DesIV))
            {
                byte[] decrypted = Encoding.UTF8.GetBytes(value);
                byte[] encrypted = dec.TransformFinalBlock(decrypted, 0, decrypted.Length);
                EncryptedPlurkPassword = Convert.ToBase64String(encrypted);
            }
        }
    }
}

 

只需要短短四到五行的 code, 就可以做 DES 解密或是加密,
這點是 .NET Framework 很強大的地方!
另外因為 PlurkPassword 不應該存在 Settings.xml 當中,
所以加上了 [XmlIgnore]  這樣的屬性.
於是當 Settings 物件做 Xml Serialization 的時候, 就會忽略 PlurkPassword.
我也想把相機的解析度存下來, 這樣一來, 就可以儲存上次使用者在照相時所設定的解析度.
加了這麼多東西到 Settings 當中, 卻不用改 Load, Save , 儲存的 settings.xml 範例就像下面這樣

<?xml version="1.0"?>
<Settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <FlickrAuthToken>xxx Flickr Token - 這是馬賽克 xxx</FlickrAuthToken>
  <defaultCaptureSize>
    <Width>1600</Width>
    <Height>1200</Height>
  </defaultCaptureSize>
  <PlurkAccount>PlurkAccountExample</PlurkAccount>
  <EncryptedPlurkPassword>lNXXXXXXXXXXXXXXWQ==</EncryptedPlurkPassword>
</Settings>

如果真正的 settings.xml 內容如果洩漏了, 大家就可以用那個認證碼上傳照片到 flickr,
也可以解密 (解密的 key , iv 就在上方的程式碼當中) 出 password, 所以我都用 XXX 替代掉了.
(大家自己在用這個軟體的時候也要注意 settings.xml 的內容不要隨便給人家知道)
不過由此可以看出 XmlSerialization 對於儲存讀取設定資料是很好用的.

因為現在只打算登入 Plurk, 丟一個 Message …
所以我就先搞定這兩個 plurk API 吧.

登入 plurk 的 API 內容如下

private static Regex loginregex = new Regex("\\<input type=\"hidden\" name=\"user_id\" value=\"([\\d]+)\" \\/\\>");

/// <summary>
/// Login to plurk
/// </summary>
/// <param name="username">Username</param>
/// <param name="password">Password</param>
/// <returns>True if login was successful, false otherwise</returns>
public bool Login(string username, string password)
{
    string data;
    this.username = username;
    this.password = password;
    data = web.GetPage("http://www.plurk.com/m/login",
        new Dictionary<string, string>() {
            {"username", this.username},
            {"password", this.password},
        }, ref cookie, true);

    if (cookie != null)
    {
        // 取得 cookie, 再導頁取一次就可以取得 uid.
        data = web.GetPage("http://www.plurk.com/m", null, ref cookie, true);
        Match m = loginregex.Match(data);
        if (m.Success)
        {
            this.uid = Convert.ToInt32(m.Groups[1].Value);
            this.myFriends = this.getFriends(this.uid);
            this.isLogged = true;
            return true;
        }
    }
    this.isLogged = false;
    return false;
}


 

新增一個 噗浪訊息的 API 如下:

/// <summary>
/// Add plurk message
/// </summary>
/// <param name="lang">The plurk language, for traditional chinese : tr_ch</param>
/// <param name="qualifier">The plurk qualifier</param>
/// <param name="content">The content of plurk message to be posted</param>
/// <param name="alowComments">true if this plurk message allows comments, false otherwise</param>
/// <param name="limited_to">Limite this plurk message to some friends. Format: [uid,uid,uid]. Otherwise set with ""</param>
/// <returns>true if it was posted, otherwise false</returns>
public bool addMessage(string lang, string qualifier, string content, bool alowComments, string limited_to)
{
    string error_match = string.Empty;

    if (string.IsNullOrEmpty(limited_to) == true)
        limited_to = "\"everybody\"";
    if (string.IsNullOrEmpty(qualifier) == true)
        qualifier = ":";

    string data = web.GetPage("http://www.plurk.com/m", new Dictionary<string, string>()
    {
        {"user_id",uid.ToString()},
        {"qualifier", qualifier},
        {"content", content},
        {"no_comments", !alowComments ? "1" : null},
        {"limited_to_only", string.Empty},
        {"limited_to", limited_to},
        {"language", lang},
    }, ref cookie, true);

    return (data.IndexOf("<p>You should be redirected automatically to target URL:") != -1);
}

 

終於, 要修改主程式的部分了.
首先還是先拖拉一些 UI 設定,
我把 menu 選單設計如下

image

現在我們要再弄一個設定 Plurk 帳號的介面.
新增一個 WinForm, 放兩個簡單的 textBox 給使用者輸入:

image

由於預設拖拉到 Form 裡面的元件, 只有該 Form 能處理 (protected 權限)
所以為了讓 MainForm (也就是 Form1) 能夠存取, 寫了下面這段簡單的 code

using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace CameraNow
{
    public partial class PlurkSetting : Form
    {
        public PlurkSetting()
        {
            InitializeComponent();
        }

        public void LoadFromSetting(Settings setting)
        {
            textBox1.Text = setting.PlurkAccount;
            textBox2.Text = setting.PlurkPassword;
        }

        public void SaveToSetting(Settings setting)
        {
            setting.PlurkAccount = textBox1.Text;
            setting.PlurkPassword = textBox2.Text;
        }
    }
}

 

於是, 要設定 plurk 帳號密碼的程式碼如下

private void menuItem7_Click(object sender, EventArgs e)
{
    using (PlurkSetting ps = new PlurkSetting())
    {
        ps.LoadFromSetting(settings);
        if (ps.ShowDialog() == DialogResult.OK)
        {
            ps.SaveToSetting(settings);
            settings.Save(settingfile);
        }
    }
}

 

上面這段程式碼就是產生一個 PlurkSetting 的 Form,
然後填入原先的設定, 再把該 Form 顯示出來,
直到使用者按下該 Form 右上角的 OK.
然後我們就把使用者在該 Form 內輸入的資訊存好.

最後, 以上種種的準備就是為了
” 隨時打開手機, 拍照, 寫字, 上傳 Plurk 一氣呵成啊 “

寫出如下的程式碼!

private void menuItem6_Click(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(flickr.AuthToken) == true)
    {
        MessageBox.Show("尚未取得 Flickr 授權");
        return;
    }

    if (String.IsNullOrEmpty(settings.PlurkAccount) == true)
    {
        MessageBox.Show("請設定 Plurk 帳號");
        return;
    }

    string photoid = flickr.UploadPicture(imagefilename, "CameraNow", "Photo by CameraNow at " + DateTime.Now.ToString());
    var photoinfo = flickr.PhotosGetInfo(photoid);
                
    PlurkApi.PlurkApi plurk = new PlurkApi.PlurkApi();
    if (plurk.Login(settings.PlurkAccount, settings.PlurkPassword))
    {
        if (plurk.addMessage("tr_ch", string.Empty, photoinfo.WebUrl + " " + textBox1.Text, true, string.Empty))
        {
            MessageBox.Show("posted!");
            return;
        }
    }
    MessageBox.Show("post failed");
}

 

就算其中很多 code 都不是我們自己寫的,
但是能夠整合出這樣功能的程式, 還是會很興奮的…

不含 Flickr API key 的原始碼 : CameraNow3_src.zip
包含 Flickr API key 的可執行碼 : CameraNow3_bin.zip