風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

近傍ツイート検索で最近リツイートしたユーザーを表示する機能を追加しました(version 0.2.6.100)


前書き

近傍ツイート検索
furyu.hatenablog.com
にて、個別ツイートをリツイートした最近のユーザーを表示する機能を追加しました。
また、該当するユーザーが RT 前後の 10 分間にツイートしていた内容(概要)を表示することも出来るようになりました。

早い話が、『RtRT』("(Find the) Reference to ReTweet")や、
リツイート直後のツイートを表示するやつみたいなことを、ユーザースクリプトや拡張機能でもやりたかったのです……サイトに行って調べるのが面倒なので(苦笑)。
とは言っても色々と制限が厳しく、Webサービスみたいにはいきませんが……。
調子に乗ってあちこちのツイートで試していると API 制限(時間当たりの回数制限)に引っかかるかも知れません。悪しからずご了承ください。

新機能について

当然ながら、ブラウザに近傍ツイート検索の最新版がインストールされていることが前提です。
まだの方は、お使いのブラウザ環境に合わせて
github.com
chrome.google.com

近傍ツイート検索 – Firefox 向けアドオン

からインストールしてください。

使い方

インストール/更新後に Web 版公式ツイッターを開くと、リツイートされているツイートの下の方に [Re:RT] というボタンがついています。
f:id:furyu-tei:20171125034556p:plain

これをクリックすると、当該ツイートを RT したユーザーのうち、最近行った方々が表示されます。
f:id:furyu-tei:20171125034607p:plain

ユーザーのプロフィールの右側に [↓↑] というボタンが表示されますが、
f:id:furyu-tei:20171125034626p:plain

これをクリックすると、
f:id:furyu-tei:20171125034640p:plain

RT 前後 10 分間のツイート概要が表示されます。
RTした人は、その前後に当該ツイートについて言及していることもよくある、という経験則に基づきます。

オプションとしては、表示するユーザー数を変更できます。
f:id:furyu-tei:20171125034653p:plain
これは、最大 100 ユーザーまでです。
Twitter API の仕様による制限です。

注意書き

更新の際に無効化されてしまう

上記の機能に関して、拡張機能に新たな権限が追加されています。
f:id:furyu-tei:20171127045119p:plain

0.2.5.200以前バージョンから更新された場合、いったん拡張機能が無効化され、有効化しようとすると、

「近傍ツイート検索」の最新バージョンは、さらに許可が必要なため無効になっています。

のようにメッセージが表示されて、再度有効にするかどうかの確認があります。
f:id:furyu-tei:20171127034910p:plain

[再度有効にする]ボタンを押してください。
上記は Chrome の場合ですが、Firefox の場合にも同様のダイアログが表示されることがありますので、その場合は[更新(U)]ボタンを押してください。

※キャンセルを押してしまった場合などは……
 Google Chrome → 拡張機能の画面 chrome://extensions/ (「≡」(右上のハンバーガーメニュー)→「その他のツール(L)」→「拡張機能(E)」)を開き、「近傍ツイート検索」を探して、「□ 有効にする」のチェックボックスにチェックを入れてください。
 Firefox Quantum → アドオンマネージャー about:addons ([Ctrl]+[Shift]+[A]、「≡」(右上のハンバーガーメニュー)→アドオン)を開き、「近傍ツイート検索」を探して、[有効化]ボタンを押してください。

ここで、Google Chrome の場合には

・twitter.com の全サイト、twitter.com 上にある自分のデータの読み取りと変更

とあるので(特に『変更』と出てしまっているので)不安に思われる方もいらっしゃるようですが、「ユーザーデータには変更を加えてはいません」のでご安心ください。
Firefox Quantum の場合、同様のダイアログでは、「・twitter.com の保存されたデータへのアクセス」という表現になっていると思います。
作者本人がそう言っても不安だ・信用できない、という方は、公開しているソースコードをご覧になるか、使用をお控えください。

これは、今回の機能の中で、「個別ツイートをリツイートした最近のユーザー」の情報を取得するために、新たにアクセス権を追加したためです。
Twitter 側に設定してある本拡張機能用のアクセス権限は、「読み取り専用」(Read-only)としているのですが、
f:id:furyu-tei:20171127035554p:plain
拡張機能には「読み取り専用」という権限はなく、アクセスを許可しようとすると自動的に「読み取りと変更」ということになってしまいます。

なお、上記の権限に関して、実際に使用している Twitter の API は以下の通りです。

POST oauth2/token — Twitter Developers
GET application/rate_limit_status — Twitter Developers
GET statuses/retweets/:id — Twitter Developers

余談

ブラウザ拡張機能用に background で ZIP 化するためのライブラリを試作(Chrome拡張機能/Firefox Quantum WebExtensions 用)


前書き

WebExtensions について調べていると、Promise を使用して云々……という記述が出てきて、今さらながらに Promise というものの存在を知りました(ヲイ。
慣れれば使い勝手が良さそうなので、練習を兼ねて、ブラウザ拡張機能の background で ZIP 化することが出来るようなライブラリを試作してみました。
github.com
習作なので、いつも以上に動作保証できません。ご利用は計画的に(汗)。

概要

content_scripts に対し、ZipRequest クラスを提供します。
ZipRequest#open()/file()/generate()/close() という一連の関数にて、background に対してメッセージを送ることで ZIP 化に関する指示を出し、background からの応答メッセージで結果を受け取り、content_scrips に返します。
background での ZIP 化には、JSZip を使用しています。

比較する意味もあって、content_scripts 用には Promise を使ったもの(Promise版・zip_request.js) と、使わないもの(コールバック版・zip_request_legacy.js)とがあります。
background 用のもの(zip_worker.js)は共通です。

サンプル

はてなブログ("*://*.hatenablog.com/*")を開くと、画像を適当な数選んで ZIP 化・ダウンロードする、という迷惑な(汗)サンプルコード(抜粋)です。
サンプルソースコード全文は、こちらをご覧ください

並列処理

複数のファイルを同時並行で取得しながらアーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open();

files.forEach( function ( file ) {
    var url = file.src || file.href,
        filename = get_filename( url );
    
    console.log( '[start]', url, filename );
    
    zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    }, function ( result ) {
        console.log( '[result]', url, filename, result );
    } );
} );

zip_request.generate( 'blob', function ( response ) {
    zip_request.close();
    
    // 以下、Aタグのdownload 属性を使ったダウンロード処理
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

await Promise.all(
    files.map( async ( file ) => {
        let result,
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        result = await zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        } )
        .catch( result => { return result } ); // Promise.all() を停止させないための対策
        
        console.log( '[result]', url, filename, result );
        
        return result;
    } )
);

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

コールバック版のライブラリ内で多少工夫をしていることもあり、並列処理に関しては、一見したところそれ程違いは無いかもしれません。
コールバック版では、例えば file() に対応する応答が background からまだ来ない状態で generate() が呼ばれたとしても、全てのファイルについて結果が返るのを待って background に要求を出すようにしているため、上記の書き方が可能。ただし、close() については、generate() のコールバック後に呼び出す必要あり。

直列処理

ファイルを一つずつ順番に取得しながら(逐次)アーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open( function ( result ) {
    var file_index = 0;
    
    function zip_files() {
        if ( files.length <= file_index ) {
            zip_request.generate( 'blob', function ( response ) {
                zip_request.close();
                
                // 以下、Aタグのdownload 属性を使ったダウンロード処理                                
            } );
            return;
        }
        
        var file = files[ file_index ++ ],
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        }, function ( result ) {
            console.log( '[result]', url, filename, result );
            
            zip_files();
        } );
    }
    
    zip_files();
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

for ( let file of files ) {
    // files.map( async ( file ) => { ... } ) は使えないことに注意
    // ※ map() では、コールバック関数の戻り値が Promise object になり、直列処理されない
    let url = file.src || file.href,
        filename = get_filename( url ),
        result;
    
    console.log( '[start]', url, filename );
    
    result = await zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    } )
    .catch( result => { return result } ); // エラーで停止させないための対策
    
    console.log( '[result]', url, filename, result );
}

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

こちらは、Promise 版のメリットが出ていると思います。
コールバック版は処理の流れが一見解りにくいのに対し、Promise 版では上から下への自然な流れで解りやすくなっています。

はまった点など

  • Promise.all() は、並列実行中の Promise オブジェクトが一つでもエラーになると異常終了してしまう(catchされてしまう)ため、中断したくない場合、それぞれの Promise で reject() ではなく resolve() を呼び、戻り値によって判別するようにする
  • Array#map() 等のコールバック処理を持つものは、直列処理では使用できない(コールバックの結果が Promise オブジェクトで返されるため)
  • Promise の resolve() や reject() は、呼んだ後も続きが実行される(実行されないようにするには、直後に return が必要)
  • background における、browser/chrome.runtime.onMessage.addListener() のコールバック関数内で、非同期の処理を呼んでから sendResponse()を返す場合、コールバック関数の戻り値に true を設定する必要がある(chrome.runtime - Google Chrome
  • content_scripts と background 間のやり取り(sendMessage()/sendResponse())では、JSON で基本的にはシリアライズ可能なオブジェクトしか渡せない……ところが、渡せるオブジェクトの種類に、ブラウザ間で差異がある(関数オブジェクトは Firefox で NG、Blob が渡せるのは Firefox のみ、等)
  • background で URL.createObjectURL() により得られた Blob URL を content_scripts に送ると、Chrome では download 属性付き A タグでダウンロード可能なのに対し、Firefox や MS-Edge では不可(Firefoxについては、なぜか Blob がそのまま content_scripts に送れるため、そちらで Blob URL に変換することで対応している)
  • MS-Edge では、作成した ZIP をダウンロードさせる術が見つからない

Promise/async/awaitやclassの書き方でもっとはまると思っていたが、これらはそれ程でもなかった代わりに、拡張機能の仕様やブラウザ間の細かい差異の方が難解。

Chrome 拡張機能を Microsoft Edge の拡張機能にも対応させようとして挫折した件


前書き

承前。
furyu.hatenablog.com
furyu.hatenablog.com
せっかくだから、Firefox Quantum に対応できた拡張機能を、MS-Edge でも動かしたいと欲張ったのだが……見事に挫折した(哀)。

修正方法等

manifest.json

「マニフェスト解析エラー: 'author' フィールドが見つかりません。」と出たので、author フィールドを追加。

browser.* ネームスペース対応

WebExtensions 用に、予め対応してあった。

PATH の問題

オプション画面にて、chrome.browserAction.setIcon( { path : icon_path } ) を使ってアイコンを変化させる処理を書いてあったところ、MS-Edge ではアイコンが正常に表示されなくなった。
調べてみたところ、どうも、アイコンの PATH の指定が、

  • Chrome 拡張機能 / Firefox WebExtensions → 呼び出し元の HTML(オプション画面)からの相対パス
  • MS-Edge 拡張機能 → manifest.json のあるフォルダからの相対パス

のように違いがあるようで、場合分けが必要だった。

未解決問題

以下は、Microsoft Edge 41.16299.15.0 / Microsoft EdgeHTML 16.16299 で発生

fetch() や XMLHttpRequest で ArrayBuffer を使用すると未定義のエラーが発生

ユーザーコンテキスト(content_scripts)で fetch() を使用すると、

  SCRIPT65535: 未定義のエラーです。

どうやら、同様の現象が他でも見られるみたい。
Fetch API in Extension SCRIPT65535 error - Microsoft Edge Development
https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/attachment/14192157/8236868/

同様に、XMLHttpRequest で、xhr.responseType を 'arraybuffer' に設定した場合に、xhr.response を参照しようとするとエラーとなってしまう。

  SCRIPT65535: 未定義のエラーです。

いったん、responseType を 'blob' にして受けてやると response は参照できるので、これを ArrayBuffer 化することは可能だった。
ただ、その場合でも、JSZip の アーカイブ処理(JSZip#generateAsync)で失敗してしまう。

よって、MS-Edge の拡張機能では、Twitter 原寸びゅーや Twitter メディアダウンローダで使用している JSZip による ZIP 化が出来ず、ZIP ダウンロード機能が無効化してしまう。
特に、メディアダウンローダの方はほぼすべての機能が使えなくなってしまう……(哀)。

バックグラウンドでのダウンロードができない

バックグラウンドコンテキスト(background)内では、現状、ファイルのダウンロードができない模様。

Overarching issues
The following known issues span across the extension platform and will be fixed in the near future:
: (中略)
・Triggering a download via a hidden anchor tag will fail from background scripts. This should be done from an extension page instead.

Extensions - Supported APIs - Microsoft Edge Development | Microsoft Docs
  • browser.downloads API は存在しない(2017/04/11現在のロードマップで、"Under consideration" になっている
  • a タグの download 属性を使ったダウンロードも不可(click()してもダウンロードされない)
  • navigator.msSaveOrOpenBlob() 等も使えない
    SCRIPT16386: インターフェイスがサポートされていません
  • tabs.create() で新たにタブを開いてそちらでやろうとしても、background から開いた場合には上記の不具合が継承されてしまう

ということで、八方ふさがり。
Twitter 原寸びゅーのコンテキストメニューからの「原寸画像を保存」が実現出来ない。
background から content_scripts 宛に sendMessage() して、そちらで処理ならできるかも?でも面倒くさそう……。

余談

というわけで、自作拡張機能を MS-Edge に対応させることは、少なくとも現状では諦めた。
まぁ、出来た拡張機能を公式に公開するすべは今のところ無さそうだし良いけど。

それにしても MS-Edge は、拡張機能を開発しやすいとはお世辞にも言えないな……。

  • 拡張機能関連のメニューが自動的に隠れてしまうため、アクセスしずらい
    "about:flags"みたいに、タブに独立して出す方法はある?
  • 開発者ツールでデバッグしていると固まりやすく、またかなりの確率でブラウザごと落ちてしまう
  • 上記の fetch() のように、拡張機能のコンテキストでのみ動作しない不具合が多い

それでもめげず(?)、便利な拡張機能を公開されている限られた方々には頭が下がる訳だが。

少なくとも自分は、当面 MS-Edge では開発したくない。
幸い、MS-Edge にも Tampermonkey があるので、当方の拡張機能を使いたい方は、Tampermonkey を入れて、ユーザースクリプト版をお使いいただきたい。
とか言っていたら、MS-Edge 版の Tampermonkey が TweetDeck では異常が発生し、ユーザースクリプトを動かせない不具合を見つけてしまったり……。

【覚書】Firefox アドオン (WebExtensions) を AMO で公開した際の手順


前書き

Chrome 拡張機能を Firefox Quantum (WebExtentions) にも対応させたので、せっかくだしと AMO にも登録してみることに。
その手順を覚え書きとして記しておく。

手順

アドオン開発者センターにユーザー登録

Firefoxアカウントを未所持の場合、アドオン開発者センターにて、ユーザー登録を行う。

f:id:furyu-tei:20171117232228p:plain
f:id:furyu-tei:20171117232239p:plain

自作アドオンのアップロード準備

当該アドオンの全ファイルを一つの ZIP アーカイブにまとめる(アドオンのパッケージ化)。
この際、manifest.json 及び同一フォルダ内のファイルが一番上の階層になるようにアーカイブすること。

あなたのWebExtensionは次のものを格納したディレクトリです。manifest.jsonとその他の必要なファイル-スクリプト、アイコン、HTMLドキュメント等。あなたはこれらを1つにまとめたzipファイルをAMOにアップロードする必要があります。

ひとつトリッキーなこととして、ZIPファイルはWebExtensionを構成するファイルを含み、ディレクトリに入ってはいません。

WebExtensionを公開する - Mozilla | MDN

At this point your extension will consist of a directory containing a manifest.json and any other files it needs - scripts, icons, HTML documents, and so on. You'll need to zip these into a single file for uploading to AMO.

One trick is that the ZIP file must be a ZIP of the extension's files themselves, not of the containing directory.

Publishing your extension - Mozilla | MDN

OKな例

manifest.json
html/options.html
js/background.js
 :

NGな例

src/manifest.json
src/html/options.html
src/js/background.js
 :

自分の場合、src/manifest.json のように src 以下に全て置いていたため、src フォルダを含む形で ZIP を作っていたところ(実際、Chrome 拡張機能の場合はこれでも問題なくアップロードできる)、AMOにアップロードしようとしたらエラーが出てしまった。

新しいアドオンの登録

アドオン開発者センター

f:id:furyu-tei:20171117232245p:plain
[新しいアドオンの登録]ボタンを押して、登録を開始する。

f:id:furyu-tei:20171117232252p:plain
配布手段を選択し、[続ける]を押す。
特に理由がなければ、「◉当サイト上で……」で良いと思う。

f:id:furyu-tei:20171117232258p:plain

f:id:furyu-tei:20171117232304p:plain
前の手順で準備した パッケージ(ZIP ファイル)をアップロードすると、アドオンの検証が行われ、結果が表示される。

f:id:furyu-tei:20171117232309p:plain
「検証レポートの詳細を見る」をクリックすると、詳細な検証レポートが開く。

f:id:furyu-tei:20171117232319p:plain
これをチェックし、問題があれば修正する。
eval等を使用しているといった警告はあったが、自作以外のライブラリなこともあり、対応はせずにそのまま。エラーでなければ継続できるらしい。

f:id:furyu-tei:20171117232325p:plain

f:id:furyu-tei:20171117232332p:plain
「アドオンの説明」画面に必要事項を記入・入力し(名前と概要は manifest.json に記載のものが転記されている)、[バージョンを登録]を押して登録する。

f:id:furyu-tei:20171117232338p:plain
登録完了後、[掲載ページを管理]を押すと、

f:id:furyu-tei:20171117232347p:plain
アドオンに関する各種情報を確認・変更できるので、必要に応じて追記や修正を行う。

余談

「『AMO』ってなんだろう、サイト名としては『Firefox Add-ons』か『Add-ons for Firefox』かだよね…」とか思っていたのだが、これ、ドメイン名である"addons.mozilla.org" の頭文字を取ったものだったのか……。

Chrome 拡張機能を Firefox Quantum の WebExtensions にも対応させた例


前書き

とりあえず、自作の Chrome 拡張機能のうち、二つを Firefox Quantum (WebExtentions) にも対応させてみた。
Twitter 原寸びゅー – Firefox 向けアドオン
Twitter メディアダウンローダ – Firefox 向けアドオン
この際に修正した箇所を中心に、覚え書きを記しておく。

修正方法等

manifest.json

最初、次のような警告が出ていた。

Reading manifest: Error processing background.persistent: Event pages are not currently supported. This will run as a persistent background page.
Reading manifest: Error processing options_page: An unexpected property was found in the WebExtension manifest.

上の方は、

    "background": {
        "scripts" : [ "js/background.js" ],
        "persistent" : true
    },

の "persistent" オプションに関するもので、現状は Firefox ではサポートされておらず、無視される。動作的には特に問題ないので、このまま。

下の方は、

   "options_page" : "html/options.html",

に関するもので、エラーとなっているが、このままでもオプションページは開くことが出来る。
現在は、

    "options_ui" : {
        "page" : "html/options.html"
    },

という書き方になっているため、こちらに合わせておく。

jQuery

jQuery 読み込み時に、

  Content Security Policy: ページの設定により次のリソースの読み込みをブロックしました: self (“script-src moz-extension://7959b7cd-9bbb-443f-8362-df4673e654f6 'unsafe-eval'”) Source: onfocusin attribute on DIV element.

のようなエラーが出ていた。
どうも、v1.x 系を使用していると発生する模様(使用していたのは、v1.11.3)。
参考:Web Extension: Cannot load jQuery into background page, due to CSP - Development - Mozilla Discourse
これは、v2.x 系にて修正されているらしい。v2.2.4 にしたところ、発生しなくなった。

chrome.app.* は使わない

オプション画面の、拡張機能バージョンを取得する処理でエラーが発生していた。
原因は、chrome.app.getDetails().version を使用していたこと。
代わりに chrome.runtime.getManifest().version を使うことで解決した。
参考: Where is chrome.app officially documented? - Stack Overflow

browser.* ネームスペース対応

これまで chrome.* ネームスペースを使用していたが、今後の流れとしては browser.* ネームスペースを使う方が良さそう。
参考: Chrome incompatibilities - Mozilla | MDN

といいつつ、既存のものを全部書き換えるのは煩わしいので、各スクリプトの先頭に

window.chrome = ( ( typeof browser != 'undefined' ) && browser.runtime ) ? browser : chrome;

のような一行を入れておくことでお茶を濁してある。

なお、Firefox Quantum の WebExtensions では、browser と chrome の両方が定義されているが、コンテキストによって微妙に見え方が違う。

バックグラウンドコンテキスト(background)

chrome window.chrome browser window.browser
Chrome 拡張機能 × ×
Firefox WebExtensions
MS-Edge 拡張機能 × ×

ユーザーコンテキスト(content_scripts)

chrome window.chrome browser window.browser
Chrome 拡張機能 × ×
Firefox WebExtensions × ×
MS-Edge 拡張機能 × ×

独り言

とりあえず、Twitter 原寸びゅーTwitter メディアダウンローダに関しては、以上のような修正を行うことで、無事、Firefox Quantum でも動作するようになり、かつ、Google Chrome 上でもこれまで通り動作しているので、共用化できたと言える。
もともと、ユーザースクリプトとしては両方の環境で動作するように作成してあったため、最低限の修正で済んだといえる。
まぁ、細かい不具合は出ているのだが……。