// ==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')