[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 的專案當中,
你可以看到, .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 選單設計如下
現在我們要再弄一個設定 Plurk 帳號的介面.
新增一個 WinForm, 放兩個簡單的 textBox 給使用者輸入:
由於預設拖拉到 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