MSNメッセンジャーアドイン

ほぼ裏技の世界で、かつ非サポート。挙句の果てにショボイ仕様。


この二日位何やってたのか、貼り付けておくます。
メッセンジャーで会話中にはてなキーワードを引っ張ってくるアドイン作ってますた。


そのうち知り合いがインストーラ付きで公開してくれると思うので、今はとりあえずコードとメモだけ。

開発環境

コードを書く時に参考にしたサイト

デバッガからアタッチするには。

以下の様なレジストリエントリを投入すると、

[HKEY_CURRENT_USER\Software\Microsoft\MSNMessenger\AddInDebugHooks]
"PromptForDebugger"=dword:00000001

Live Messengerのオプションダイアログで、「アドイン」を選択した時に、
何のコメントも無く、OK/Cancel しか無い様なモーダルダイアログが起動する。
ここで、直ぐにOKを押さないで、VisualStudioからデバッガでアタッチ出来るのだが、
結局、アタッチした後に、ソースコードとプロセスをマッチングさせて、
設定したブレークポイントで処理を止める事が出来なかったデス。

その他のメモ

メッセージ送受信ハンドラ内では、以下の様な事象が観察される。
特に仕様が書いてある訳では無いので、正しくないかもしれないが、メモしておく。

  • 利用可能な処理時間に限度がある模様。それを過ぎると強制的に処理スレッドが殺され結果的に無反応であるかの様に見える。
  • MessengerClient#SendTextMessageは、一度のイベントに付き一回しか呼び出す事が出来ない。
    一度呼び出すとそこでスレッドが終了される様に見える。
    二度呼び出す様にコードを書いても送信されるのは最初の一回のみで、二回目以降は無視されている。
  • 一度に送信可能なメッセージの長さには限度がある。400byte前後が限界の様だ。
    それ以上の長さの文字列をMessengerClient#SendTextMessageに渡すと、無視される。
  • イベントハンドラより外側に例外を送出すると、アドイン自体が強制的に終了してしまう。
  • アドインが送信したメッセージをイベントハンドラでハンドリングする事は出来ない。
  • 兎に角何か都合の悪い事がおこりそうになると、処理が途中でも色々強制終了するので諦めない心が重要。

ガチガチのセキュリティを乗り越える為にはGACへの登録が必要デス。

ちなみに、HTTPリクエストを送信したり、ローカルファイルにアクセスしたりする為には、
厳密名で署名した上で、アセンブリがGACに登録されていなければなりませぬ。
開発中には、「ビルド後に実行するコマンドライン」として、
こんな感じにしておけば、忘れないデス。

"$(DevEnvDir)..\..\SDK\v2.0\Bin\gacutil.exe" /i "$(TargetPath)"

ソースコード

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Windows.Forms;
using System.Xml;

using Microsoft.Messenger;

/// <auther>taichi</auther>
namespace MessengerAddIn
{
    public class HatenaSearchAddIn : IMessengerAddIn
    {
        private const String defaultTrigger = "調べて";
        private const String hatenaKeyword = "http://d.hatena.ne.jp/keyword?mode=rss2&ie=utf8&page=1&word=";
        //private String wikipedia = "http://ja.wikipedia.org/wiki/Special:Export/";


        private MessengerClient messenger;

        private String trigger;

        public void Initialize(MessengerClient messenger)
        {
            this.messenger = messenger;
            this.messenger.AddInProperties.Creator = "Taichi Sato";
            this.messenger.AddInProperties.Description = "Hatena Keyword Search AddIn";
            this.messenger.AddInProperties.FriendlyName = "Hatena keyword"; // アドインとして表示される名前
            this.messenger.AddInProperties.Url = new Uri("http://d.hatena.ne.jp/taichitaichi/");
            this.messenger.ShowOptionsDialog += new EventHandler(messenger_ShowOptionsDialog);
            this.messenger.IncomingTextMessage += new EventHandler<IncomingTextMessageEventArgs>(messenger_IncomingTextMessage);
            this.messenger.OutgoingTextMessage += new EventHandler<OutgoingTextMessageEventArgs>(messenger_OutgoingTextMessage);

            this.trigger = this.messenger.SavedState;
            if (this.trigger == null || this.trigger.Length < 1)
            {
                this.trigger = defaultTrigger;
            }

        }

        /// <summary>
        /// アドインの設定ボタンが押された時のハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void messenger_ShowOptionsDialog(object sender, EventArgs e)
        {
            // 反応する語尾を設定出来るだけのダイアログ。
            using (SettingsForm form = new SettingsForm(this.trigger))
            {
                // モーダルダイアログにしたい所だけど、senderはIWin32Windowでは無いみたい。
                // 上手くデバッガをアタッチ出来れば、調べられるんだけど…
                if (form.ShowDialog() == DialogResult.OK)
                {
                    this.trigger = form.End;
                    // string一つしか保存出来ないけど、
                    // XMLを作って、それをToStringすれば、構造化された情報も保存出来るかもね。
                    this.messenger.SavedState = form.End;
                }
            }
            
        }

        /// <summary>
        /// メッセージ送信時のハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void messenger_OutgoingTextMessage(object sender, OutgoingTextMessageEventArgs e)
        {
            search(e.TextMessage, e.UserTo);
        }

        /// <summary>
        /// メッセージ受信時のハンドラ
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void messenger_IncomingTextMessage(object sender, IncomingTextMessageEventArgs e)
        {
            search(e.TextMessage, e.UserFrom);
        }
        
        /// <summary>
        /// メインの検索処理。
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="u"></param>
        void search(String msg, User u)
        {
            try
            {

                if (msg != null && u != null && this.trigger.Length < msg.Length && msg.EndsWith(this.trigger))
                {
                    String word = msg.Substring(0, msg.Length - this.trigger.Length);

                    String encoded = HttpUtility.UrlEncode(word);
                    StringBuilder stb = new StringBuilder();
                    stb.Append(hatenaKeyword);
                    stb.Append(encoded);

                    using (XmlTextReader xmlReader = new XmlTextReader(stb.ToString()))
                    {
                        int count = 0;
                        // RSS2.0 をかなりテキトーにパース
                        while (xmlReader.Read())
                        {
                            if (xmlReader.Name == "channel")
                            {
                                xmlReader.Skip();
                            } else if (xmlReader.Name == "description")
                            {
                                String desc = xmlReader.ReadString();                                
                                if (desc == null || desc.Length < 1)
                                {
                                    count++;
                                }
                                else
                                {
                                    // HTMLのタグを全部消す。
                                    desc = Regex.Replace(desc, @"[ \t\n]*<[^<]*>", "");

                                    // 長かったら切る。
                                    if (150 < desc.Length)
                                    {
                                        desc = desc.Substring(0, 150) + " (ry";
                                    }
                                    this.messenger.SendTextMessage(desc, u);
                                    return;
                                }
                                
                            }
                        }
                        if (0 < count)
                        {
                            this.messenger.SendTextMessage("中身ナカターヨ - " + stb.ToString(), u);
                            return;
                        }
                    }
                    
                    this.messenger.SendTextMessage("(´д`)ワカンネ", u);
                }
            }
            catch (Exception e)
            {
                this.messenger.SendTextMessage(e.ToString(), u);
            }
        }
    }
}

Wikipediaから取ってこようとしたけど、面倒になったので、やめてしまいました。
正直に申し上げます。wiki記法によるタグをサックリと除去する様な正規表現を考えるのがタルくなったからです、ええヘタレです。


return文の位置が色々怪しいケドまぁ、気にしないでクダサイ。

今のAPIがイマイチな点。

  • 同時に一つしかアドインを起動出来ない事。複数起動出来て欲しいナリ。
  • エラーログを全く出さない事。イベントログにスタックトレースを出して欲しいデス。
  • 黙って処理が中断される事。例外位投げれ。セキュリティ的にダメならセキュリティ例外みたいなの投げれ。無反応が一番対処に困る。イキナリスレッド殺してるでしょ?
  • 何度もSendMessageできない事。細かく何度もSendMessageしたいなり。その為に、自前でキューイングとか無理。いや、Timer使えば出来るけどさ…
  • IMessengerAddInの実装クラスのライフサイクルモデルがイマイチ判然としない事。もしかしてドキュメント読み足りない?…orz
  • APIから画像を送受信出来ない事。

XmlTextReaderガチでスゲェ。

黙っててもHTTPリクエスト投げてくれる。超便利。
プルパーサっぽいけど、そうでも無い感じがメチャ使い易い。
ReadとSkipのコンボが、テキトーにコーディングするには凄くイイ。
これ、同じ挙動のクラス、Javaにもあるのかな…