This commit is contained in:
syuilo 2017-09-08 22:10:25 +09:00
parent 99db09b702
commit 5c37b9cef3
5 changed files with 172 additions and 0 deletions

View file

@ -2,6 +2,10 @@ ChangeLog (Release Notes)
========================= =========================
主に notable な changes を書いていきます 主に notable な changes を書いていきます
unreleased
----------
* New: ユーザーページによく使うドメインを表示 (#771)
2566 (2017/09/07) 2566 (2017/09/07)
----------------- -----------------
* New: 投稿することの多いキーワードをユーザーページに表示する (#768) * New: 投稿することの多いキーワードをユーザーページに表示する (#768)

View file

@ -498,6 +498,7 @@ mobile:
images: "Images" images: "Images"
activity: "Activity" activity: "Activity"
keywords: "Keywords" keywords: "Keywords"
domains: "Domains"
followers-you-know: "Followers you know" followers-you-know: "Followers you know"
last-used-at: "Latest used at" last-used-at: "Latest used at"
@ -512,6 +513,9 @@ mobile:
mk-user-overview-keywords: mk-user-overview-keywords:
no-keywords: "No keywords" no-keywords: "No keywords"
mk-user-overview-domains:
no-domains: "No domains"
mk-user-overview-followers-you-know: mk-user-overview-followers-you-know:
loading: "Loading" loading: "Loading"
no-users: "No users" no-users: "No users"

View file

@ -498,6 +498,7 @@ mobile:
images: "画像" images: "画像"
activity: "アクティビティ" activity: "アクティビティ"
keywords: "キーワード" keywords: "キーワード"
domains: "頻出ドメイン"
followers-you-know: "知り合いのフォロワー" followers-you-know: "知り合いのフォロワー"
last-used-at: "最終ログイン" last-used-at: "最終ログイン"
@ -512,6 +513,9 @@ mobile:
mk-user-overview-keywords: mk-user-overview-keywords:
no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)" no-keywords: "キーワードはありません(十分な数の投稿をしていない可能性があります)"
mk-user-overview-domains:
no-domains: "よく表れるドメインは検出されませんでした"
mk-user-overview-followers-you-know: mk-user-overview-followers-you-know:
loading: "読み込み中" loading: "読み込み中"
no-users: "知り合いのユーザーはいません" no-users: "知り合いのユーザーはいません"

View file

@ -0,0 +1,120 @@
import * as URL from 'url';
import Post from '../../api/models/post';
import User from '../../api/models/user';
import parse from '../../api/common/text';
process.on('unhandledRejection', console.dir);
function tokenize(text: string) {
if (text == null) return [];
// パース
const ast = parse(text);
const domains = ast
// URLを抽出
.filter(t => t.type == 'url' || t.type == 'link')
.map(t => URL.parse(t.url).hostname);
return domains;
}
// Fetch all users
User.find({}, {
fields: {
_id: true
}
}).then(users => {
let i = -1;
const x = cb => {
if (++i == users.length) return cb();
extractDomainsOne(users[i]._id).then(() => x(cb), err => {
console.error(err);
setTimeout(() => {
i--;
x(cb);
}, 1000);
});
};
x(() => {
console.log('complete');
});
});
function extractDomainsOne(id) {
return new Promise(async (resolve, reject) => {
process.stdout.write(`extracting domains of ${id} ...`);
// Fetch recent posts
const recentPosts = await Post.find({
user_id: id,
text: {
$exists: true
}
}, {
sort: {
_id: -1
},
limit: 10000,
fields: {
_id: false,
text: true
}
});
// 投稿が少なかったら中断
if (recentPosts.length < 100) {
process.stdout.write(' >>> -\n');
return resolve();
}
const domains = {};
// Extract domains from recent posts
recentPosts.forEach(post => {
const domainsOfPost = tokenize(post.text);
domainsOfPost.forEach(domain => {
if (domains[domain]) {
domains[domain]++;
} else {
domains[domain] = 1;
}
});
});
// Calc peak
let peak = 0;
Object.keys(domains).forEach(domain => {
if (domains[domain] > peak) peak = domains[domain];
});
// Sort domains by frequency
const domainsSorted = Object.keys(domains).sort((a, b) => domains[b] - domains[a]);
// Lookup top 10 domains
const topDomains = domainsSorted.slice(0, 10);
process.stdout.write(' >>> ' + topDomains.join(', ') + '\n');
// Make domains object (includes weights)
const domainsObj = topDomains.map(domain => ({
domain: domain,
weight: domains[domain] / peak
}));
// Save
User.update({ _id: id }, {
$set: {
domains: domainsObj
}
}).then(() => {
resolve();
}, err => {
reject(err);
});
});
}

View file

@ -240,6 +240,12 @@
<mk-user-overview-keywords user={ user }/> <mk-user-overview-keywords user={ user }/>
</div> </div>
</section> </section>
<section class="domains">
<h2><i class="fa fa-globe"></i>%i18n:mobile.tags.mk-user-overview.domains%</h2>
<div>
<mk-user-overview-domains user={ user }/>
</div>
</section>
<section class="followers-you-know" if={ SIGNIN && I.id !== user.id }> <section class="followers-you-know" if={ SIGNIN && I.id !== user.id }>
<h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2> <h2><i class="fa fa-users"></i>%i18n:mobile.tags.mk-user-overview.followers-you-know%</h2>
<div> <div>
@ -579,6 +585,40 @@
</script> </script>
</mk-user-overview-keywords> </mk-user-overview-keywords>
<mk-user-overview-domains>
<div if={ user.domains != null && user.domains.length > 1 }>
<virtual each={ domain in user.domains }>
<a style="opacity: { 0.5 + (domain.weight / 2) }">{ domain.domain }</a>
</virtual>
</div>
<p class="empty" if={ user.domains == null || user.domains.length == 0 }>%i18n:mobile.tags.mk-user-overview-domains.no-domains%</p>
<style>
:scope
display block
> div
padding 4px
> a
display inline-block
margin 4px
color #555
> .empty
margin 0
padding 16px
text-align center
color #aaa
> i
margin-right 4px
</style>
<script>
this.user = this.opts.user;
</script>
</mk-user-overview-domains>
<mk-user-overview-followers-you-know> <mk-user-overview-followers-you-know>
<p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> <p class="initializing" if={ initializing }><i class="fa fa-spinner fa-pulse fa-fw"></i>%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
<div if={ !initializing && users.length > 0 }> <div if={ !initializing && users.length > 0 }>