//【登録場所】リンク、URLExec、選択テキスト(ユーザーポップアップのみ対応)
//【ラベル】ツイートのポップアップ
//【内容】ツイート、ツイッターユーザーのポップアップ
// ツイートかユーザーかはURLから自動的に判別
// 選択テキストから起動された場合は、それをユーザーIDとして処理する
//【コマンド1】${SCRIPT:FrwS} popupTwitterInfo.js
//【コマンド2】${SCRIPT:FrwS} popupTwitterInfo.js テンプレートのファイル名 (popupTweetフォルダ内の任意のテンプレートファイルを指定)
//【URLExec】https?://(?:mobile\.|m\.)?twitter\.com/(?:#!/)?\w+[#/]?$ $& ${V2CSCRIPT:FrS} popupTwitterInfo.js (ユーザー)
// https?://(?:mobile\.|m\.)?twitter\.com/(?:#!/)?[^/]+/status(?:es)?/\d+ $& ${V2CSCRIPT:FrS} popupTwitterInfo.js (ツイート)
//設定(onがtrue、offがfalse)-----------------------
//ポップアップの最大幅
var maxPopupWidth = 400;
//マウス移動でポップアップを閉じる
v2c.context.setDefaultCloseOnMouseExit(true);
//終了後に残りアクセス回数をステータスバーに表示
var showRemainingHits = false;
//一度読み込んだテンプレートとtwitterデータはキャッシュし、同じデータを読み込む場合はこれを表示する。
//キャッシュはV2C再起動でクリアされる。使用にはT20110522以降が必要。
var cacheMode = true;
//ポップアップ上の@userクリックをスクリプトによるポップアップするかどうか
var userPopup = false;
//-----------------------------------------------
//デフォルトテンプレートファイル名
var TEMPLATE_TWEET = "TemplateStatus.txt";
var TEMPLATE_USER = "TemplateUser.txt";
//キャッシュ管理
var cacheData = {
data: null,
load: function() {
if (cacheMode) {
this.data = v2c.getScriptObject();
}
if (!this.data) {
this.data = {};
}
},
set: function(key, value) {
this.load();
this.data[key] = value;
if (cacheMode) {
v2c.setScriptObject(this.data);
}
},
get: function(key) {
this.load();
return this.data[key];
}
}
var userRx =/https?:\/\/(?:mobile\.|m\.)?twitter\.com\/(?:#!\/)?(\w+)/i;
var statusRx =/https?:\/\/(?:mobile\.|m\.)?twitter\.com\/(?:#!\/)?[^\/]+\/status(?:es)?\/(\d+)/i;
Twitter = function(accessToken, accessTokenSecret, userId) {
this.requestTokenUrl = 'https://a...content-available-to-author-only...r.com/oauth/request_token';
this.authorizeUrl = 'https://a...content-available-to-author-only...r.com/oauth/authorize';
this.accessTokenUrl = 'https://a...content-available-to-author-only...r.com/oauth/access_token';
this.consumerKey = '3tLPbTiq8xvHXN4ZtJYsA';
this.consumerSecret = 'mozHVgfsRZzFujV1vKYPMPHGmgJsvPYdzaNvwTf7So';
this.requestToken;
this.requestTokenSecret;
this.accessToken = accessToken;
this.accessTokenSecret = accessTokenSecret;
this.userId = userId;
}
Twitter.prototype.getUnixTime = function()
{
var time = java.lang.System.currentTimeMillis() / 1000;
return Math.floor(time);
}
Twitter.prototype.createNonce = function()
{
const min = 123400;
const max = 9999999;
// min以上 max未満の値を生成
var v = Math.floor(Math.random() * (max - min)) + min;
return v;
}
Twitter.prototype.concatParameters = function(dic, func)
{
var array = [];
for(var key in dic)
{
array.push(key);
}
array.sort();
var param = '';
for(var i = 0; i < array.length; i++)
{
var key = array[i];
var value = dic[key];
if(key == undefined || value == undefined)
throw 'undefined parameter'
param += func(key, value);
}
return param;
}
Twitter.prototype.urlEncode = function(str)
{
return java.net.URLEncoder.encode(str, 'UTF-8');
}
Twitter.prototype.createAuthorizationHeader = function(params)
{
var self = this;
// oauth_* という名前のパラメータだけを連結
var header = this.concatParameters(params,
function(k, v)
{
if(k.indexOf('oauth_') == 0)
return stringFormat(', {0}="{1}"', self.urlEncode(k), self.urlEncode(v));
return '';
});
header = 'OAuth ' + header.substr(2);
return header;
}
Twitter.prototype.createSignature = function(message, accessTokenSecret)
{
// byte-array -> base64-string
// ref: http://s...content-available-to-author-only...o.com/research/1308640339
function toBase64String(arr)
{
var seed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
if (arr == null)
return "====";
var result = "";
var c = 0;
for (var i = 0; i < arr.length; i++) {
switch(i % 3) {
case 0:
result += seed.charAt((arr[i]>>2)&0x3f);
c = (arr[i]&0x03)<<4;
break;
case 1:
result += seed.charAt(c | ((arr[i]>>4)&0x0f));
c = (arr[i]&0x0f)<<2;
break;
case 2:
result += seed.charAt(c | ((arr[i]>>6)&0x0f));
result += seed.charAt(arr[i]&0x3f);
c = 0;
break;
}
}
if (arr.length % 3 == 1) {
result += seed.charAt(c);
result += "==";
} else if (arr.length % 3 == 2) {
result += seed.charAt(c);
result += "=";
}
return result;
}
printlnLog('-- createSignature --');
dumpObject(message.parameters);
// oauth_signature を除く全てのパラメータを、sortして順番に連結
var urlParam = this.concatParameters(message.parameters, function(k, v)
{
return ('&' + k + '=' + v);
});
urlParam = urlParam.substr(1); // &を抜く
// method&url¶ms
var text = message.method + '&'
+ this.urlEncode(message.action)
+ '&'
+ this.urlEncode(urlParam);
text = new java.lang.String(text);
var rawKey = this.urlEncode(this.consumerSecret) + '&';
// アクセストークンがなくても、'&'を残しておく
// ある時は、アクセストークン秘密鍵も追加してキーとして使う
if(accessTokenSecret)
rawKey += this.urlEncode(accessTokenSecret);
rawKey = new java.lang.String(rawKey);
printlnLog('text: {0}, key: {1}', text, rawKey);
var signingKey = new javax.crypto.spec.SecretKeySpec(rawKey.getBytes(), "HmacSHA1");
var mac = javax.crypto.Mac.getInstance(signingKey.getAlgorithm());
mac.init(signingKey);
var rawHmac = mac.doFinal(text.getBytes());
var bary = [];
for(var i = 0; i < rawHmac.length; i++)
{
// Java の byte は signed なので、unsigned にしてやる。
bary.push(rawHmac[i] & 0xFF);
}
var signature = toBase64String(bary);
return signature;
}
// function(url : string, addParam : Array -> void)
//
// 認証トークン取得に使う、基本的なパラメータを指定したリクエストを生成。
// addParam引数を使ってパラメータを追加できる。
Twitter.prototype.getOAuthToken = function(url, addParam)
{
if(addParam == undefined)
addParam = function(params) { };
if(typeof addParam != 'function')
throw 'addParamを指定する場合、function型のみが有効です。'
var accessor = {
consumerSecret: this.consumerSecret
};
var message = {
method: "POST",
action: url,
parameters: {
oauth_signature_method: "HMAC-SHA1",
oauth_consumer_key: this.consumerKey,
oauth_version: '1.0',
oauth_timestamp: this.getUnixTime(),
oauth_nonce: this.createNonce()
}
};
addParam(message.parameters);
// signature 以外のパラメータはここまでに message.parameters に入れておく。
message.parameters['oauth_signature'] = this.createSignature(message);
// POST
var req = v2c.createHttpRequest(message.action, '');
var header = this.createAuthorizationHeader(message.parameters);
req.setRequestProperty('Content-Type', 'application/x-www-form-urlencoded');
//req.setRequestProperty('Content-Length', 0);
req.setRequestProperty('Authorization', header);
var res = req.getContentsAsString();
if(req.responseCode != 200)
{
printlnLog('--- dump ---')
dumpObject(req, function(name, value) { return typeof value != typeof function() { }; });
throw '認証エラー';
}
return res;
}
Twitter.prototype.getRequestToken = function()
{
var res = this.getOAuthToken(this.requestTokenUrl);
var results = this.parseToken(res);
// リクエストトークンを保存する
this.requestToken = results['oauth_token'];
this.requestTokenSecret = results['oauth_token_secret'];
printlnLog('requestToken: {0}, requestTokenSecret: {1}', this.requestToken, this.requestTokenSecret);
}
// アクセストークンを取得し、メンバに保存する。
//
// TODO: これやってないんだけど、なんで動くんだろう
// 署名のキーに、リクエストトークン秘密鍵も加わる。
//
// oauth_tokenとしてリクエストトークンを、oauth_verifierとしてPINを送信
// アクセストークンは永続化して使い回し可能。
Twitter.prototype.getAccessToken = function(pin)
{
assert(this.requestToken && this.requestTokenSecret);
assert(pin);
var self = this;
var res = this.getOAuthToken(this.accessTokenUrl, function(params)
{
params['oauth_token'] = self.requestToken;
params['oauth_verifier'] = pin;
})
var results = this.parseToken(res);
// アクセストークンを保存する
this.accessToken = results['oauth_token'];
this.accessTokenSecret = results['oauth_token_secret'];
this.userId = results['user_id'];
printlnLog('accessToken: {0}, accessTokenSecret: {1}, userId: {2}',
this.accessToken, this.accessTokenSecret, this.userId)
assert(this.isAuthorized());
}
Twitter.prototype.parseToken = function(data)
{
var tokens = data.split('&');
var parsedToken = {};
tokens.forEach(function(token) {
var kv = token.split('=');
parsedToken[kv[0]] = kv[1];
});
return parsedToken;
};
// public
Twitter.prototype.authenticate = function()
{
this.getRequestToken();
// ユーザーにこのクライアントを承認してもらい、表示されたPINを入力してもらう
v2c.alert('[popupTwitterInfo.js] ブラウザで開かれるページで認証を行なってください。');
var target = this.authorizeUrl + '?' + 'oauth_token=' + this.requestToken;
v2c.browseURLDefExt(target);
var pin = v2c.prompt('PIN を入力してください', '');
this.getAccessToken(pin);
if(!this.isAuthorized())
{
v2c.alert('error: 認証に失敗しました。');
}
}
Twitter.prototype.isAuthorized = function()
{
return this.accessToken != null
&& this.accessTokenSecret != null
&& this.userId != null;
}
Twitter.prototype.getAuthorizedData = function()
{
return [ this.accessToken, this.accessTokenSecret, this.userId ];
}
// TODO: 今のところ GET しかサポートしてない
Twitter.prototype.createRequest = function(method, api)
{
if(method != "GET"/* && method != "POST"*/)
{
throw 'サポートしていないメソッドです。'
}
var accessor = {
consumerSecret: this.consumerSecret,
tokenSecret: this.accessTokenSecret
};
var message = {
method: method,
action: api,
parameters: {
oauth_signature_method: "HMAC-SHA1",
oauth_consumer_key: this.consumerKey,
oauth_token: this.accessToken,
oauth_version: '1.0',
oauth_timestamp: this.getUnixTime(),
oauth_nonce: this.createNonce()
}
};
// TODO: postするパラメータを parameters にpush
message.parameters['oauth_signature'] = this.createSignature(message, this.accessTokenSecret);
var target = message.action;
var header = this.createAuthorizationHeader(message.parameters);
printlnLog('header: {0}', header);
var req = method == "GET"
? v2c.createHttpRequest(target)
: v2c.createHttpRequest(target, requestBody);
//req.setRequestProperty('Content-Type', 'application/x-www-form-urlencoded');
//req.setRequestProperty('Content-Length', 0);
req.setRequestProperty('Authorization', header);
return req;
}
function initializeTwitter()
{
var settingFileName = 'popupTwitterInfo_oauth.bin';
var settings = v2c.getScriptDataFile(settingFileName);
var dataArray = v2c.readLinesFromFile(settings);
if(dataArray != null && 3 == dataArray.length)
{
var tw_ = new Twitter(dataArray[0], dataArray[1], dataArray[2]);
if(tw_.isAuthorized())
return tw_;
}
// データがない場合、または認証されていない(ファイル書き換えたとか)場合は再度認証する。
var tw = new Twitter();
tw.authenticate();
if(!tw.isAuthorized())
{
throw 'error: 認証に失敗しました。';
}
// save
var settings = v2c.getScriptDataFile(settingFileName);
var dataArray = tw.getAuthorizedData();
v2c.writeLinesToFile(settings, dataArray);
printlnLog('save oauth data: {0}', dataArray);
return tw;
}
var twitter = initializeTwitter();
var u = v2c.context.link;
if (!u) {//選択テキストから起動された場合は、ユーザーIDとして処理する
var selectedText = v2c.getSelectedText();
if (selectedText && selectedText.match(/@?([0-9A-Za-z_]{1,15})/)) {
u = "http://t...content-available-to-author-only...r.com/" + RegExp.$1;
}
}
if (u) {
popupTwitterInfo(u.toString(), false);
}
else {
v2c.alert('URL取得失敗');
}
function popupTwitterInfo(url,isRefresh) {
var html,key,getHtml;
var tm = v2c.context.args[0];
//URLが正しいかどうかの確認
if (url.match(statusRx)) {
templateFilename = tm ? tm : TEMPLATE_TWEET;
getHtml = getTwitterStatusHTML;
}
else if (url.match(userRx)) {
templateFilename = tm ? tm : TEMPLATE_USER;
getHtml = getTwitterUserHTML;
}
else {
v2c.alert("非対応のURLです\n" + url);
return;
}
if (getHtml) {
key = RegExp.$1 + '/' + templateFilename;
if (!isRefresh) {
//同じURLのポップアップを開いていたら終了
if (v2c.context.getPopupOfID(key)) {
return;
}
html = cacheData.get(key);
}
if (!html){
html = getHtml(RegExp.$1, templateFilename);
}
}
if (html) {
//ポップアップの設定
html = html.replace('%url%',url);//更新ボタン用
v2c.context.setPopupHTML(html);
v2c.context.setMaxPopupWidth(maxPopupWidth);
v2c.context.setPopupID(key);
v2c.context.setRedirectURL(true);
v2c.context.setCloseOnLinkClick(!userPopup);
v2c.context.setTrapFormSubmission(true);
cacheData.set(key,html);
}
if (showRemainingHits) {
var limit = getJson("http://a...content-available-to-author-only...r.com/1.1/account/rate_limit_status.json");
if (limit) {
v2c.context.setStatusBarText("残り回数:" + limit.remaining_hits +
" 次のリセット時間:" +
getDateText(limit.reset_time));
}
}
return;
}
function formSubmitted(url, sm, sd) {
v2c.context.closeOriginalPanel();
popupTwitterInfo(url.toString(),true);
}
function redirectURL(url) {
if (userPopup) {
url = url + '';
if (!url.match(statusRx) && url.match(userRx) && url.indexOf('#!') == -1) {
popupTwitterInfo(url, false);
return null;
}
}
return url;
}
function getTwitterStatusHTML(sid,template) {
var url = "http://a...content-available-to-author-only...r.com/1.1/statuses/show/" + sid + ".json";
var json = getJson(url);
if (!json) {
return null;
}
//テンプレートを読み込み
var templateText = readTemplate(template);
if (!templateText) {
v2c.alert("ファイルがない? " + template);
return null;
}
var html = getTwitterUserFromJson(json.user, templateText);
html = getTwitterStatusFromJson(json, html);
return html;
}
function getTwitterUserHTML(user,template) {
var url = "http://a...content-available-to-author-only...r.com/1.1/users/show/" + user + ".json";
var json = getJson(url);
if (!json) {
return null;
}
//テンプレートを読み込み
var templateText = readTemplate(template);
if (!templateText) {
v2c.alert("ファイルがない? " + template);
return null;
}
var html = getTwitterUserFromJson(json, templateText);
//最新ツイート取得
//if (json.statuses_count > 0) {
html = getTwitterStatusFromJson(json.status, html);
//}
return html;
}
function getTwitterStatusFromJson(statusJson, templateText) {
var text = '';
var date = '';
var client = '';
var retweetCount = '0';
var statusID = '0';
if (statusJson) {
//本文の取得
if (statusJson.text) {
text = addLinkTag(statusJson.text + '');
}
//投稿日時の取得
if (statusJson.created_at) {
date = getDateText(statusJson.created_at);
}
//リツイート数の取得
if (statusJson.retweet_count) {
retweetCount = statusJson.retweet_count + '';
}
//クライアント
if (statusJson.source) {
client = statusJson.source;
}
//statusID取得
if (statusJson.id) {
statusID = statusJson.id + '';
}
}
else {
text = "取得できず"
}
templateText = templateText.replace('%date%', date);
templateText = templateText.replace('%sid%', statusID);
templateText = templateText.replace('%via%', client);
templateText = templateText.replace('%retweet%', retweetCount);
templateText = templateText.replace('%text%', text);
return templateText;
}
function getTwitterUserFromJson(userJson, templateText) {
var followersCount = '0';
var friendsCount = '0';
var statusesCount = '0';
var verified = '';
var createdAt = '';
var icon = '';
var icolink = '';
var name = '';
var screenName = '';
var homeurl = '';
var description = '';
if (userJson) {
//フォロワー数取得
if (userJson.followers_count) {
followersCount = userJson.followers_count + '';
}
//フォロー数取得
if (userJson.friends_count) {
friendsCount = userJson.friends_count + '';
}
//ツイート数
if (userJson.statuses_count) {
statusesCount = userJson.statuses_count + '';
}
//urlの取得
if (userJson.url) {
homeurl = userJson.url;
}
//認証済み
if (userJson.verified) {
verified = '認証済み';
}
//アカウント名の取得
if (userJson.screen_name) {
screenName = userJson.screen_name;
}
//アイコンURL取得
if (userJson.profile_image_url) {
icon = userJson.profile_image_url;
icolink = "http://t...content-available-to-author-only...r.com/#!/" + screenName;
}
//表示名取得
if (userJson.name) {
name = userJson.name;
}
//紹介文の取得
if (userJson.description) {
description = addLinkTag(userJson.description + '');
}
//作成日の取得
if (userJson.created_at) {
createdAt = getDateText(userJson.created_at,"yyyy/MM/dd");
}
}
//パラメータの置換
templateText = templateText.replace('%aname%', screenName);
templateText = templateText.replace('%uname%', name);
templateText = templateText.replace('%icon%', icon);
templateText = templateText.replace('%icolink%', icolink);
templateText = templateText.replace('%verified%', verified);
templateText = templateText.replace('%followers_count%', followersCount);
templateText = templateText.replace('%statuses_count%', statusesCount);
templateText = templateText.replace('%friends_count%', friendsCount);
templateText = templateText.replace('%created_at%', createdAt);
templateText = templateText.replace('%homeurl%', homeurl);
templateText = templateText.replace('%description%', description);
return templateText;
}
function addLinkTag(htmlText) {
var http = "(?:(?:ftp|https?)://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+)";
var user = '(?:[@][0-9A-Za-z_]{1,15})';
var hashtag = '(?:(^|[^a-zA-Z0-9&?]+)#(\\w*[a-zA-Z_]\\w*))';
var r = new RegExp([http, user, hashtag].join('|'), 'g');
return htmlText.replace(r, function(m0) {
if (m0.match(/^(?:ftp|http)/)) {
return '<a href="' + m0 + '">' + m0 + '</a>';
}
else if (m0.match(/^@/)) {
return '@<a href="http://t...content-available-to-author-only...r.com/' +
m0.substr(1) + '">' + m0.substr(1) + '</a>';
}
else if (m0.match('^' + hashtag)) {
return RegExp.$1 + '<a href="http://t...content-available-to-author-only...r.com/search?q=' +
encodeURIComponent('#' + RegExp.$2) + '">#' + RegExp.$2 + '</a>';
}
else {
return m0;
}
})
}
function getDateText(d, pattern) {
var dd = new Date(d);
if (!pattern) {
pattern = "yyyy/MM/dd HH:mm:ss (E)";
}
var sdf = new java.text.SimpleDateFormat(pattern);
return sdf.format(dd);
}
function getJson(url) {
v2c.setStatus('popupTwitterInfo通信中...');
var hr = twitter.createRequest('GET', url);
var sr = hr.getContentsAsString();
if (!sr) {
v2c.context.setStatusBarText('PopupTwitterInfo ページの取得に失敗しました。: ' + hr.responseCode + ' ' + hr.responseMessage + ' ' + url);
return null;
}
//データ取得
return eval("(" + sr + ")");
}
function readTemplate(fileName) {
var template = cacheData.get(fileName);
if (!template) {
template = v2c.readFile(combinePath(v2c.saveDir, 'script', 'popupTweet', fileName));
cacheData.set(fileName, template);
}
return template;
}
function combinePath() {
if (arguments.length == 0) {
return "";
}
var combined = new java.io.File(arguments[0]);
for (var i = 1; i < arguments.length; i++) {
combined = new java.io.File(combined, arguments[i]);
}
return combined.getPath();
}
// predicate(name, value)
function dumpObject(obj, predicate)
{
if(predicate == null) predicate = function() { return true; };
v2c.println("> " + (obj == null ? "null" : obj.toString()));
for(var name in obj)
{
var value = null;
try { value = obj[name]; } catch(e) { value = e; };
if(predicate(name, value))
v2c.println(name + ": " + value)
}
};
function stringFormat(format /*, ...*/)
{
var args = arguments;
var result = format.replace(/\{(\d)\}/g, function(m, c) { return args[parseInt(c) + 1] });
return result;
}
function printlnLog(format /*, ...*/)
{
var args = arguments;
var message = args.length <= 1
? format
: format.replace(/\{(\d)\}/g, function(m, c) { return args[parseInt(c) + 1] });
v2c.println("[popupTwitterInfo.js] " + (message ? message : 'null'));
}
function assert(condition)
{
if(!condition)
{
throw 'assertion failed!';
}
}