#Thunderbirdに「Todoistのタスクを登録する」ボタンを追加する
Firefox、Thunderbirdのアドオンを作ったことが無い私が挑戦してみた。
環境はWindows10、Thunderbird 52.1.1で作成した。
完成したのはこんなアドオンです
##そもそも、なぜこんなアドオンを開発しようとしたか
###■公式アドオンがない
公式Todoistが、Thunderbird対応を止めてしまったので、Thunderbird 52.1.1で受け取ったメールを簡単にタスク化するには、メール本文などをコピペするしかなかった。(代わりになるアドオンなどは無さそう?)
###■タスク管理方法
下記のような方法でタスクを管理しようとしたが、性に合わなくて続かなかった。
- Lightningアドオンを使う方法
- 受信箱をタスクリストにする方法
- 既読にしない方法
- Thunderbirdのタグをつける方法など
唯一Todoistは続いている。
あまり有料ソフトは使わないのだが、Todoistは年会費を払っている。
Todoistは、いつまでに、優先度付けなどを活用し、タスクを管理するサービス
私は、進捗管理をしない前提(コメントの内容やサブタスクの消化具合で判断している)
##アドオンの仕様を検討した
- メールを選択して、ヘッド表示部(Fromなどのヘッダ情報が表示される部分)にある返信ボタンの横に、ボタンを追加する
- タスクのタイトルは、メールの件名or本文の選択したテキスト
- タスクに自動的にコメントを追加する
- コメントの内容は、メールのヘッダ情報の一部(日付、件名、Fromなど)とメール本文
- 追加したボタンを押したら登録確認画面が表示されて、登録ボタンを押したら、APIで登録される
これが実現できれば、とりあえず使えそうだなということで、仕様はなるべく簡単にした。
###■あきらめたこと
・各自のTodoistの「APIトークン」を設定する画面を作ること
→ 設定エディタに直接登録することにした
・タスクを追加するプロジェクトを選択すること
・期限を自由に選択すること
・タスクに書かれたリンクをクリックすると、メールが開くこと(できないよね?)
・関連メールのやり取り(スレッド)のタスクは、サブタスクにすること
ヘッダ情報をコメントに埋め込めば、できるかなぁ
などなど。
インボックスで、今日の期限のタスクに表示されるので、ブラウザ上で修正することにした。
##やったこと
###(1) 開発環境準備
・thunderebirdの開発用プロファイルを準備する
私はポータブル版だったので、普通とやり方が少し違うのかも?
C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -p
だったか(覚えていない)
C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -ProfileManager
として起動後、プロファイルを新規作成した。
・開発用希望batの作成
batファイルを作って、中身を
C:\ThunderbirdPortable\App\Thunderbird\thunderbird.exe -p "開発用"
とした。
・いくつかの設定を変更
このサイトを参考にして、
ツール>オプション>詳細>設定エディタ
の画面で変更した。(ここで変更することすら知らなかったので苦労した)
・作業ディレクトリの準備
C:\develop\t2t
を作成した。ここで開発する。
C:\Users\xxxx\AppData\Roaming\Thunderbird\Profiles\xxxxx.開発用\extensions
に新規で
thunderbirdToDoist@sample.com
というファイルを作成した。
ファイルの中身は、
C:\develop\t2t
とした。
###(2) 開発開始
詳しくは、先人たちのWEBと、添付したソースを見てもらいたい。
正確な内容は理解していないので。
・ソースにコメントを少しだけ入れた。(途中で飽きた)
・面倒なので、インデントもテキトウ(--;)
・デバッグログなども残したまま
※javascriptは全然わからない、firefox/thunderebird開発経験ゼロといった
プログラム初心者なので、色々ご容赦いただきたい。
質問などされても全く分からない。
###▼タスク登録する部分だけ少し解説(ソースのコメントで)
あと、登録数に制限があるのかわからないが、日を跨いだら同じソースでも
登録に成功したということがあった。
なにかTodoist側で問題でもあったのだろうか・・・?
apiトークンは、Todiostの設定画面から拾ってくる、
設定エディタで「extensions.t2t.setting.apikey」として登録済みとする。
//APIトークンを、設定から持ってくる
let prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
let apitoken = prefs.getCharPref("extensions.t2t.setting.apikey");
//タスクを登録する
var url = "https://todoist.com/API/v7/sync",
formData, response;
formData = {
"token": apitoken,
"commands": JSON.stringify(
[
{ "type" : "item_add",
"uuid": uuid, //なんの文字列でも良さそう?
"temp_id" : uuid+"a", //<<<<ここのtemp_idと、コメント(note_add)のitem_idを同じにしないと、コメントが登録されない(中身はなんの文字列でも良さそう?)
"args": {
"content": taskname, //タスク名
"date_string": "today" //タスク期限を今日にする
}
},
{
"type": "note_add", //タスクコメントを追加する
"uuid": uuid+"c", //なんの文字列でも良さそう?
"temp_id": uuid+"b", //なんの文字列でも良さそう?
"args": {
"content": comment, //コメント本文
"item_id": uuid+"a" //item_addと同じ文字列
}
}
])
};
...
あとは、XMLHttpRequestでテキトウにPOST送信した。
※先人達のサンプルをコピペしていじっただけです。感謝。
※ソースは自由に改変してもらって良いので、誰かもっと良いものに仕上げてください。お願いします。
##ソース
###taskAdd.xul
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<dialog id="asdf" title="Add task to todoist"
buttons="accept,cancel"
buttonlabelaccept="Add Task"
buttonaccesskeyaccept="a"
ondialogaccept="return doOK();"
buttonlabelcancel="Cancel"
buttonaccesskeycancel="c"
ondialogcancel="return doCancel();"
onload="onLoad();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox>
<hbox>
<description value="タスク名:" /><textbox id="txttaskname" width="700px" />
</hbox>
<hbox>
<description value="コメント:" /><textbox id="txtcomment" multiline="true" height="400px" width="700px" />
</hbox>
</vbox>
<script type="application/x-javascript" src="taskAdd.js" />
</dialog>
...
###taskAdd.js
function onLoad() {
//カーソルを先頭に
document.getElementById("txttaskname").value = window.arguments[0].taskname;
document.getElementById("txtcomment").value = window.arguments[0].comment;
document.getElementById("txtcomment").selectionStart = 0;
document.getElementById("txtcomment").selectionEnd = 0;
document.getElementById("txtcomment").focus();
}
function doCancel()
{
//alert("You pressed Cancel!");
//return true;
return true;
}
function doOK()
{
//alert("You pressed OK!");
//return true;
//1:タスク名
//2:uuid = fromaddress+date
//3:コメント内容:本文
createTask(window.arguments[0].taskname,
window.arguments[0].uuid,
window.arguments[0].comment);
return true;
}
// タスク登録
function createTask(taskname, uuid, comment) {
let prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
let apitoken = prefs.getCharPref("extensions.t2t.setting.apikey");
//nsPreferences.setUnicharPref("extensions.t2t.setting.apitoken", 123);
console.log(uuid);
var url = "https://todoist.com/API/v7/sync",
formData, response;
formData = {
"token": apitoken,
"commands": JSON.stringify(
[
{ "type" : "item_add",
"uuid": uuid,
"temp_id" : uuid+"a",
"args": {
"content": taskname,
"date_string": "today"
}
},
{
"type": "note_add",
"uuid": uuid+"c",
"temp_id": uuid+"b",
"args": {
"content": comment,
"item_id": uuid+"a"
}
}
])
};
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function()
{
var READYSTATE_COMPLETED = 4;
var HTTP_STATUS_OK = 200;
if( xmlHttpRequest.readyState == READYSTATE_COMPLETED
&& xmlHttpRequest.status == HTTP_STATUS_OK)
{
// レスポンスの表示
console.log( xmlHttpRequest.responseText );
alert("タスク登録 成功");// + xmlHttpRequest.responseText );
}
//ステータスNGの時もレスポンスが見たい
console.log( "-------------------------" );
console.log( xmlHttpRequest.readyState );
console.log( xmlHttpRequest.status );
console.log( xmlHttpRequest.responseText );
console.log( "^^^^^^^^^^^^^^^^^^^^^^^^^" );
}
xmlHttpRequest.open( 'POST', url, false );//非同期true
// サーバに対して解析方法を指定する
xmlHttpRequest.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
// データをリクエスト ボディに含めて送信する
xmlHttpRequest.send( EncodeHTMLForm( formData ) );
/*
if (xmlHttpRequest.status===200){
console.log("OK");
}else{
console.log("NG");
}
*/
}
function EncodeHTMLForm( data )
{
var params = [];
for( var name in data )
{
var value = data[ name ];
var param = encodeURIComponent( name ).replace( /%20/g, '+' )
+ '=' + encodeURIComponent( value ).replace( /%20/g, '+' );
params.push( param );
}
return params.join( '&' );
}
...
###taskAddConfirm.xul
<?xml version="1.0"?>
<overlay id="t2t"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript" src="taskAddConfirm.js" />
<toolbox id="header-view-toolbox" position="1" >
<toolbar position="1">
<toolbarbutton label="タスク追加" oncommand="taskAddConfirm(this)" />
</toolbar>
</toolbox>
</overlay>
...
###taskAddConfirm.js
function taskAddConfirm( me )
{
/* ログを出力する方法
var consoleService = Components.
classes["@mozilla.org/consoleservice;1"].
getService( Components.interfaces.nsIConsoleService );
consoleService.logStringMessage( 'Some messages' );
*/
var tname; //タスク名(メール件名or選択した文字列)
var comm; //コメント(メール本文)
//本文を選択していなければ、タスク名はメール件名。
//本文を選択していたら、タスク名は、選択文字列
//メッセージペインで選択した文字列の取得方法
//me.label = document.getElementById("messagepane").contentDocument.getSelection().toString().length;
//メッセージペインで選択した文字列の取得
//文字列を選択していたら、その文字列がタスク名となる。
//文字列を選択していなければ、メール件名が、タスク名となる。
var deltag;
deltag = document.getElementById("messagepane").contentDocument.getSelection().toString();
if (deltag.length == 0) {
tname = document.getAnonymousElementByAttribute(document.getElementById('expandedsubjectBox'), 'anonid', 'headerValue').textContent;
} else {
tname = deltag;
}
//タスクのコメント内容は、次の順序
//From、受信日時、件名
//空行を2つ
//メール本文(HTMLタグとCSSタグは除去するが、&などは元に戻す)
let strEmail = document.getAnonymousElementByAttribute(document.getElementById('expandedfromBox'), 'headerName', 'from').getAttribute('emailAddress');
let strFrom = document.getAnonymousElementByAttribute(document.getElementById('expandedfromBox'), 'headerName', 'from').getAttribute('fullAddress');
let strSubject = document.getAnonymousElementByAttribute(document.getElementById('expandedsubjectBox'), 'anonid', 'headerValue').textContent;
let strDate = document.getElementById('dateLabel').textContent;
var buf;
buf = strFrom +"\r\n" + strDate + "\r\n" + strSubject + "\r\n\r\n";
deltag = document.getElementById("messagepane").contentDocument.body.innerHTML;
// deltag = document.getElementById("messagepane").contentDocument.body.innerHTML;
deltag = deltag.replace(/<style.*?>[.\s\S]*?<\/style.*?>/g,'');
deltag = deltag.replace(/<script.*?>[.\s\S]*?<\/script.*?>/g,'');
deltag = deltag.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
deltag = deltag.replace(/ */gm,'');
deltag = deltag.replace(/^\r*\n+/gm,'');
//deltag = deltag.replace(/([ \s\S]){2,}/g,'\r\n');
//comm = unescape(deltag);
comm = buf + unescapeHTML(deltag);
//comm = buf + deltag;
var uid = new Date().getTime().toString();
var params = {taskname: tname, //タスク名
comment: comm, //コメント
uuid: uid}; //uuid
window.openDialog("chrome://t2t/content/taskAdd.xul","addtask",
"chrome,scrollbars,resizable,centerscreen,height=500,width=800", params).focus();
}
function unescapeHTML(html) {
var htmlNode = document.createElement("div");
htmlNode.innerHTML = html;
if(htmlNode.innerText !== undefined)
return htmlNode.innerText; // IE
return htmlNode.textContent; // FF
}
...