(ただし、速度に不満あり)
前書き
フォローしてますか。
信頼できるフォロイーはフィードを豊かにしてくれます。
彼らのいいねは記事を価値あるものと信ずるに足ると勝手に思っています。
しかし、彼らのいいねが流れるのはフィードの一瞬。
最近は人気の記事一覧にも反映され目にする機会は増えましたが、それでもリアルタイムなキャッチアップには利用できても、本来の調べ物をしているときの判断材料には使いづらかったです。(適度に枯れているものを使いますし)
Qiitaの記事の指標にいいね数は使いますが、それでもさらにフォロイーもいいねしていることが可視化されると、より判断しやすくなるんじゃないかと思いました。
…特にポエム系はいいね数だけで信じるべきか悩ましいので。
前々から作りたかったものが、技術の巡りあわせがよかったので低レベルながら出来た。
(構想おそらく2年ぐらい)
- (ストック時代にストックで)作りたい
- わからない(通信・表示・キャッシュ)
- 出来たとして、APIリクエスト数制限→トークンとか認証わからない→1時間60回は現実的で無い(モチベ低下)
- いいねの導入(ストックの非可視化)
- いいね取得APIの(一般に)非導入→ストックの意味霧散→表示してもな…(モチベ低下)
- 別のユーザースクリプトでJSスキルすこし上がる(Storage,Fetch,async)
- いいね取得API(一般に)追加発見→モチベ回復
- ページングと認証頑張れば今までの組み合わせで出来そう
- LINKでページング回避できそう→パースは嫌だ…→助けてGitHub!
- できた
スクリーンショット
デザインはこだわってないので分かりづらかったりレイアウト崩れの可能性があります。
…Qiitaのhovercardかっこいいので本当に使いたい。
検証環境
- Firefox 54
- greasemonkey 3.11
コード
Qiita_Show_Followees_iine.user.js
// ==UserScript==
// @name Qiita show followees iine
// @namespace khsk
// @description 記事に自分(ユーザー)がフォローandいいねしているユーザーを表示
// @include http://qiita.com/*/items/*
// @include https://qiita.com/*/items/*
// @include http://qiita.com/*/private/*
// @include https://qiita.com/*/private/*
// @version 1
// @grant none
// ==/UserScript==
console.time('Qiita show followees iine')
const userId = ''; // 主に自分
const token = ''; // 各自で取得してください
const displayFolloweesNum = 5; // 表示数 -1 で無制限
(async () => {
// 表示場所作成
const iine = document.getElementsByClassName('list-inline ArticleMainHeader__users')[0]
const ciine = iine.cloneNode()
iine.parentElement.insertBefore(ciine, null)
// TODO いいね取得が長いから進捗を適当に書いているんだけれど、事前にheightを確保していないので、フォロイいねが無かったときなどにガタッとレイアウトが変わるかも。オプションめ。
ciine.innerText = 'フォロイー取得中'
let followees = sessionStorage.getItem('followees')
if (!followees) {
followees = await fetchAll('https://qiita.com/api/v2/users/' + userId + '/followees?per_page=100',user => {
// 無いとは思うが、sessionStorage容量上限対策として削れるものは削る
delete user.description
delete user.facebook_id
delete user.followees_count
delete user.followers_count
delete user.github_login_name
delete user.items_count
delete user.linkedin_id
delete user.location
delete user.prganization
delete user.permanent_id
delete user.twitter_screen_name
delete user.website_url
delete user.organization
return user
})
// 保存
followees = JSON.stringify(followees)
sessionStorage.setItem('followees', followees)
}
followees = JSON.parse(followees)
ciine.innerText = 'いいね取得中'
// 記事のいいね取得
const itemId = location.pathname.split('/').pop()
// 1ページ取得→即アイコン表示の方が止まっている疑惑が出にくいが…
let likes = await fetchAll('https://qiita.com/api/v2/items/' + itemId + '/likes?per_page=100', o => { return o.user.id })
ciine.innerText = ''
// 要素作成ループ
// ここ100*1000程度はありそうでヤバめ
// いいねしたフォロイーだけ抽出するとまたループが必要なのでDOMまで作るか…
// breakしたいしsomeは嫌だからforにするか…
// フォロイーといいねどちらを削除していけば性能良いかわからん…
for (let i = 0, likesLength = likes.length, displayedNum = 0; i < likesLength; i++ ) {
if (!followees.length || (displayFolloweesNum > 0 && displayFolloweesNum == displayedNum) ) {
break
}
for (let j = 0, followeesLength = followees.length; j < followeesLength; j++) {
if (likes[i] == followees[j].id) {
let followee = followees[j]
let id = followee.id
let userTemplate = iine.firstChild.cloneNode(true)
userTemplate.dataHovercardTargetName = id
userTemplate.querySelector('a').href = '/' + id
let img = userTemplate.querySelector('img')
img.alt = followee.name || id
img.src = followee.profile_image_url
// ツールチップ追加 フォロイーはアイコンでわかるだろうからオマケ程度
setPopover(userTemplate, followee)
ciine.insertBefore(userTemplate, null)
displayedNum++
// 表示し終わったフォロイーは配列から削除しようと思っていたが、影響が出るような数百単位のいいねの場合はいいね取得がボトルネックに見えるので保留にする
}
}
}
})()
/////////// functions
// 今回は無名関数でなくfuntionを使ってみる。ライブラリコピペを下部に持ってくるので、自作関数も下にまとめたい→変数なら上に書かなければ行けないので関数で
// レスポンスがないとnextがわからないのでオーバーヘッドがだいぶある。
// いいねやフォロイー総数から事前にページ数を計算すれば非同期並列で取得して最後にPromise.allで合算、で高速化できそうなんだけれども、手間がかかる。
// 計算せずとも最初のlinkにラストページが表示されるのでその数までループ、なら総数取得は無しに出来るかも。初回1本+以降並列。
// TODO 進捗表示のためにいちループごとに何か出来るコールバック作るか
async function fetchAll(url, callback = (v) =>{return v}) {
let next = url
let result = []
while (next) {
let response = await fetch(next, {
headers: new Headers({ Authorization: 'Bearer ' + token,}),
})
if (response.ok == false || response.status != 200) {
return result
}
next = li().parse(response.headers.get('Link')).next
let responseData = await response.json()
// ここデフォルト引数付けずに無いならmapしない方が性能いいけど
Array.prototype.push.apply(result, responseData.map(callback))
}
return result
}
// フィードのポップアップスクリプトの関数を持ってきて調整
function setPopover(elm, data) {
// Qiitaのhovercardはhovercard.jsではないのかもで諦め
// 最後に$(elm).tooltip() or popover()を実行するとタイトルがタグになる。謎。なので最初に設定
// datasetでは効かないものがある
$(elm).popover({
trigger : 'hover',
placement : 'bottom', // いいねは下付き
container : 'body',
html : true,
// 画像サイズ固定化や左配置のために、本家で使っている.hovervardクラスをデフォルトテンプレートに追加
template : '<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-title"></h3><div class="popover-content hovercard"></div></div>'
})
elm.dataset.toggle = 'popover'
elm.dataset.originalTitle = 'Followee Info'
// hovercardっぽさのためにアイコン画像は今回は載せる
elm.dataset.content = '<img src="' + data.profile_image_url + '">' + '<br>\n' + (data.name || data.id)
// 改行対策にデフォルト値 + nowrap
elm.dataset.viewport = "{ selector: 'body', padding: 0 , white-space: nowrap,}"
}
/////////// 外部ライブラリ(コピペ)
// MITライセンス表示はこれで大丈夫かしら
/**
* Author : José F. Romaniello <jfromaniello@gmail.com> (http://joseoncode.com)
* License : MIT 2014 - JOSE F. ROMANIELLO https://opensource.org/licenses/mit-license.php
* URL : https://github.com/jfromaniello/li
**/
function li () {
// compile regular expressions ahead of time for efficiency
var relsRegExp = /^;\s*([^"=]+)=(?:"([^"]+)"|([^";,]+)(?:[;,]|$))/;
var keysRegExp = /([^\s]+)/g;
var sourceRegExp = /^<([^>]*)>/;
var delimiterRegExp = /^\s*,\s*/;
return {
parse: function (linksHeader, options) {
var match;
var source;
var rels;
var extended = options && options.extended || false;
var links = [];
while (linksHeader) {
linksHeader = linksHeader.trim();
// Parse `<link>`
source = sourceRegExp.exec(linksHeader);
if (!source) break;
var current = {
link: source[1]
};
// Move cursor
linksHeader = linksHeader.slice(source[0].length);
// Parse `; attr=relation` and `; attr="relation"`
var nextDelimiter = linksHeader.match(delimiterRegExp);
while(linksHeader && (!nextDelimiter || nextDelimiter.index > 0)) {
match = relsRegExp.exec(linksHeader);
if (!match) break;
// Move cursor
linksHeader = linksHeader.slice(match[0].length);
nextDelimiter = linksHeader.match(delimiterRegExp);
if (match[1] === 'rel' || match[1] === 'rev') {
// Add either quoted rel or unquoted rel
rels = (match[2] || match[3]).split(/\s+/);
current[match[1]] = rels;
} else {
current[match[1]] = match[2] || match[3];
}
}
links.push(current);
// Move cursor
linksHeader = linksHeader.replace(delimiterRegExp, '');
}
if (!extended) {
return links.reduce(function(result, currentLink) {
if (currentLink.rel) {
currentLink.rel.forEach(function(rel) {
result[rel] = currentLink.link;
});
}
return result;
}, {});
}
return links;
},
stringify: function (headerObject, callback) {
var result = "";
for (var x in headerObject) {
result += '<' + headerObject[x] + '>; rel="' + x + '", ';
}
result = result.substring(0, result.length - 2);
return result;
}
};
}
///////////////
console.timeEnd('Qiita show followees iine')
トークン
https://qiita.com/settings/applications
からread_qiitaにチェックを入れたトークンを発行してください。
速度
私の環境ではいいね100件取得するのに2~3秒かかっている。
直列なので、1000いいね記事などの取得はすこぶる長い。
学び
- RFC 5988
- Array.prototype.push.apply
- async function
参考
-
API v2 で Qiita の「いいね!」がサポートされました - 他 - Qiita Blog
はやいいね導入から約10ヶ月。近日とはいったい。 - Qiita API v2ドキュメント - Qiita:Developer#いいね
- Qiita API v2ドキュメント - Qiita:Developer#認証認可
- Fetch API - Web API インターフェイス | MDN
- async function - JavaScript | MDN
-
async await の使い方 - Qiita
async functionの使い方はサンプルコードでも難しい。まだ簡潔な方なコードで理解の助けに。 -
JavaScript ループ途中で抜ける処理は for(in break) ではなく some を使おう♪ - Qiita
タイトルとは逆にコメントからforで頑張ろうと。でも度々ぶつかる問題。せっかくのArray.forEachが… -
配列の連結(concat vs Array.prototype.push.apply) - Qiita
いいねの一覧作成時、pushでは毎回ループかと悩んだときに。コメントから[]とArrayの違いも。ショートハンドじゃない。 -
JavaScriptの配列の使い方まとめ。要素の追加,結合,取得,削除。 - Qiita
当初は配列の要素の削除もするつもりだった。 - Qiita API v2ドキュメント - Qiita:Developer#ページネーション
-
jfromaniello/li: JavaScript utility to parse and generate the Link header according to RFC 5988.
jsのパーサーはいくつか見つかったが、ライセンスがゆるくコピペしやすい(1ファイル/オブジェクト)ものを拝借。
-
Qiitaの記事をストックした人を表示するBookmarklet - Qiita
やりたかったことを思い出せて、その一ヶ月後にAPI来たからよ
かった
-
はてブではてなお気に入りユーザーのスターを強調するユーザースクリプト - Qiita
原動力同じ。 -
Qiitaのフィードのタグアイコンをホバー時ユーザーアイコンに切り替えついでに名前とかをポップアップするユーザースクリプト - Qiita
asyncとポップアップ表示
他の人のQiitaAPI使った実装を見るに、ページネーションはnextを辿らずエラーが出るまで機械的にインクリメントが王道そうでしたけど、link気に入ったので。