Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc5d2b2875 | ||
|
|
94ef03db9e | ||
|
|
038bd100b2 | ||
|
|
3b5c3f086a | ||
|
|
a136715111 | ||
|
|
daa22d68fa | ||
|
|
f24d202024 | ||
|
|
d3e0b8574b | ||
|
|
f4482cc34a | ||
|
|
3ff226cd6b |
@@ -164,3 +164,6 @@ drive:
|
||||
# external: true
|
||||
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
||||
# timeout: 300000
|
||||
|
||||
# Max allowed note text length in charactors
|
||||
maxNoteTextLength: 1000
|
||||
|
||||
@@ -606,6 +606,9 @@ desktop/views/components/charts.vue:
|
||||
drive: "ドライブ"
|
||||
network: "ネットワーク"
|
||||
charts:
|
||||
federation: "フェデレーション"
|
||||
federation-instances: "インスタンスの増減"
|
||||
federation-instances-total: "インスタンスの積算"
|
||||
notes: "投稿の増減 (統合)"
|
||||
local-notes: "投稿の増減 (ローカル)"
|
||||
remote-notes: "投稿の増減 (リモート)"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.30.2",
|
||||
"clientVersion": "1.0.11041",
|
||||
"version": "10.31.0",
|
||||
"clientVersion": "1.0.11051",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
|
||||
66
src/chart/federation.ts
Normal file
66
src/chart/federation.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import Chart, { Obj } from '.';
|
||||
import Instance from '../models/instance';
|
||||
|
||||
/**
|
||||
* フェデレーションに関するチャート
|
||||
*/
|
||||
type FederationLog = {
|
||||
instance: {
|
||||
/**
|
||||
* インスタンス数の合計
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 増加インスタンス数
|
||||
*/
|
||||
inc: number;
|
||||
|
||||
/**
|
||||
* 減少インスタンス数
|
||||
*/
|
||||
dec: number;
|
||||
};
|
||||
};
|
||||
|
||||
class FederationChart extends Chart<FederationLog> {
|
||||
constructor() {
|
||||
super('federation');
|
||||
}
|
||||
|
||||
@autobind
|
||||
protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> {
|
||||
const [total] = init ? await Promise.all([
|
||||
Instance.count({})
|
||||
]) : [
|
||||
latest ? latest.instance.total : 0
|
||||
];
|
||||
|
||||
return {
|
||||
instance: {
|
||||
total: total,
|
||||
inc: 0,
|
||||
dec: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async update(isAdditional: boolean) {
|
||||
const update: Obj = {};
|
||||
|
||||
update.total = isAdditional ? 1 : -1;
|
||||
if (isAdditional) {
|
||||
update.inc = 1;
|
||||
} else {
|
||||
update.dec = 1;
|
||||
}
|
||||
|
||||
await this.inc({
|
||||
instance: update
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new FederationChart();
|
||||
@@ -3,6 +3,10 @@
|
||||
<header>
|
||||
<b>%i18n:@title%:</b>
|
||||
<select v-model="chartType">
|
||||
<optgroup label="%i18n:@federation%">
|
||||
<option value="federation-instances">%i18n:@charts.federation-instances%</option>
|
||||
<option value="federation-instances-total">%i18n:@charts.federation-instances-total%</option>
|
||||
</optgroup>
|
||||
<optgroup label="%i18n:@users%">
|
||||
<option value="users">%i18n:@charts.users%</option>
|
||||
<option value="users-total">%i18n:@charts.users-total%</option>
|
||||
@@ -79,6 +83,8 @@ export default Vue.extend({
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartType) {
|
||||
case 'federation-instances': return this.federationInstancesChart(false);
|
||||
case 'federation-instances-total': return this.federationInstancesChart(true);
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'notes': return this.notesChart('combined');
|
||||
@@ -109,11 +115,13 @@ export default Vue.extend({
|
||||
this.now = new Date();
|
||||
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
(this as any).api('charts/federation', { limit: limit, span: 'hour' }),
|
||||
(this as any).api('charts/users', { limit: limit, span: 'hour' }),
|
||||
(this as any).api('charts/notes', { limit: limit, span: 'hour' }),
|
||||
(this as any).api('charts/drive', { limit: limit, span: 'hour' }),
|
||||
(this as any).api('charts/network', { limit: limit, span: 'hour' })
|
||||
]), Promise.all([
|
||||
(this as any).api('charts/federation', { limit: limit, span: 'day' }),
|
||||
(this as any).api('charts/users', { limit: limit, span: 'day' }),
|
||||
(this as any).api('charts/notes', { limit: limit, span: 'day' }),
|
||||
(this as any).api('charts/drive', { limit: limit, span: 'day' }),
|
||||
@@ -122,16 +130,18 @@ export default Vue.extend({
|
||||
|
||||
const chart = {
|
||||
perHour: {
|
||||
users: perHour[0],
|
||||
notes: perHour[1],
|
||||
drive: perHour[2],
|
||||
network: perHour[3]
|
||||
federation: perHour[0],
|
||||
users: perHour[1],
|
||||
notes: perHour[2],
|
||||
drive: perHour[3],
|
||||
network: perHour[4]
|
||||
},
|
||||
perDay: {
|
||||
users: perDay[0],
|
||||
notes: perDay[1],
|
||||
drive: perDay[2],
|
||||
network: perDay[3]
|
||||
federation: perDay[0],
|
||||
users: perDay[1],
|
||||
notes: perDay[2],
|
||||
drive: perDay[3],
|
||||
network: perDay[4]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,6 +166,23 @@ export default Vue.extend({
|
||||
return arr.map((v, i) => ({ t: this.getDate(i).getTime(), y: v }));
|
||||
},
|
||||
|
||||
federationInstancesChart(total: boolean): any {
|
||||
return [{
|
||||
datasets: [{
|
||||
label: 'Instances',
|
||||
fill: true,
|
||||
backgroundColor: rgba(colors.localPlus),
|
||||
borderColor: colors.localPlus,
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: '#fff',
|
||||
lineTension: 0,
|
||||
data: this.format(total
|
||||
? this.stats.federation.instance.total
|
||||
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)))
|
||||
}]
|
||||
}];
|
||||
},
|
||||
|
||||
notesChart(type: string): any {
|
||||
return [{
|
||||
datasets: [{
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<span v-if="visibility === 'specified'">%fa:envelope%</span>
|
||||
<span v-if="visibility === 'private'">%fa:lock%</span>
|
||||
</button>
|
||||
<p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p>
|
||||
<p class="text-count" :class="{ over: this.trimmedLength(text) > this.maxNoteTextLength }">{{ this.maxNoteTextLength - this.trimmedLength(text) }}</p>
|
||||
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
|
||||
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
|
||||
</button>
|
||||
@@ -107,10 +107,17 @@ export default Vue.extend({
|
||||
visibleUsers: [],
|
||||
autocomplete: null,
|
||||
draghover: false,
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]')
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
|
||||
maxNoteTextLength: 1000
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
draftId(): string {
|
||||
return this.renote
|
||||
@@ -149,7 +156,7 @@ export default Vue.extend({
|
||||
canPost(): boolean {
|
||||
return !this.posting &&
|
||||
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
|
||||
(length(this.text.trim()) <= 1000);
|
||||
(length(this.text.trim()) <= this.maxNoteTextLength);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -178,9 +178,9 @@ export default Vue.extend({
|
||||
> .date
|
||||
display block
|
||||
margin 0
|
||||
line-height 32px
|
||||
line-height 28px
|
||||
text-align center
|
||||
font-size 0.8em
|
||||
font-size 12px
|
||||
color var(--dateDividerFg)
|
||||
background var(--dateDividerBg)
|
||||
border-bottom solid 1px var(--faceDivider)
|
||||
|
||||
@@ -393,7 +393,6 @@ export default Vue.extend({
|
||||
color var(--text)
|
||||
text-align center
|
||||
background var(--face)
|
||||
border-bottom solid 1px var(--faceDivider)
|
||||
|
||||
&:before
|
||||
content ""
|
||||
@@ -421,11 +420,11 @@ export default Vue.extend({
|
||||
|
||||
> b
|
||||
display block
|
||||
font-size 120%
|
||||
font-size 110%
|
||||
|
||||
> span
|
||||
display block
|
||||
font-size 90%
|
||||
font-size 80%
|
||||
opacity 0.7
|
||||
|
||||
> *
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<header>
|
||||
<button class="cancel" @click="cancel">%fa:times%</button>
|
||||
<div>
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span>
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > this.maxNoteTextLength }">{{ this.maxNoteTextLength - trimmedLength(text) }}</span>
|
||||
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
|
||||
<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
|
||||
</div>
|
||||
@@ -102,10 +102,17 @@ export default Vue.extend({
|
||||
visibleUsers: [],
|
||||
useCw: false,
|
||||
cw: null,
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]')
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
|
||||
maxNoteTextLength: 1000
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
(this as any).os.getMeta().then(meta => {
|
||||
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
draftId(): string {
|
||||
return this.renote
|
||||
@@ -144,7 +151,7 @@ export default Vue.extend({
|
||||
canPost(): boolean {
|
||||
return !this.posting &&
|
||||
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
|
||||
(this.text.trim().length <= 1000);
|
||||
(this.text.trim().length <= this.maxNoteTextLength);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ export default function load() {
|
||||
if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
|
||||
if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
|
||||
|
||||
if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
|
||||
|
||||
if (config.name == null) config.name = 'Misskey';
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
|
||||
@@ -103,6 +103,8 @@ export type Source = {
|
||||
engine: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
maxNoteTextLength?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
35
src/models/instance.ts
Normal file
35
src/models/instance.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import db from '../db/mongodb';
|
||||
|
||||
const Instance = db.get<IInstance>('instances');
|
||||
Instance.createIndex('host', { unique: true });
|
||||
export default Instance;
|
||||
|
||||
export interface IInstance {
|
||||
_id: mongo.ObjectID;
|
||||
|
||||
/**
|
||||
* ホスト
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* このインスタンスを捕捉した日時
|
||||
*/
|
||||
caughtAt: Date;
|
||||
|
||||
/**
|
||||
* このインスタンスのシステム (MastodonとかMisskeyとかPleromaとか)
|
||||
*/
|
||||
system: string;
|
||||
|
||||
/**
|
||||
* このインスタンスのユーザー数
|
||||
*/
|
||||
usersCount: number;
|
||||
|
||||
/**
|
||||
* このインスタンスから受け取った投稿数
|
||||
*/
|
||||
notesCount: number;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import NoteReaction from './note-reaction';
|
||||
import Favorite, { deleteFavorite } from './favorite';
|
||||
import Notification, { deleteNotification } from './notification';
|
||||
import Following from './following';
|
||||
import config from '../config';
|
||||
|
||||
const Note = db.get<INote>('notes');
|
||||
Note.createIndex('uri', { sparse: true, unique: true });
|
||||
@@ -29,7 +30,7 @@ Note.createIndex({
|
||||
export default Note;
|
||||
|
||||
export function isValidText(text: string): boolean {
|
||||
return length(text.trim()) <= 1000 && text.trim() != '';
|
||||
return length(text.trim()) <= config.maxNoteTextLength && text.trim() != '';
|
||||
}
|
||||
|
||||
export function isValidCw(text: string): boolean {
|
||||
|
||||
@@ -13,6 +13,8 @@ import htmlToMFM from '../../../mfm/html-to-mfm';
|
||||
import usersChart from '../../../chart/users';
|
||||
import { URL } from 'url';
|
||||
import { resolveNote } from './note';
|
||||
import registerInstance from '../../../services/register-instance';
|
||||
import Instance from '../../../models/instance';
|
||||
|
||||
const log = debug('misskey:activitypub');
|
||||
|
||||
@@ -173,6 +175,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Register host
|
||||
registerInstance(host).then(i => {
|
||||
Instance.update({ _id: i._id }, {
|
||||
$inc: {
|
||||
usersCount: 1
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newUser();
|
||||
});
|
||||
|
||||
//#region Increment users count
|
||||
Meta.update({}, {
|
||||
$inc: {
|
||||
@@ -214,6 +228,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
||||
//#endregion
|
||||
|
||||
await updateFeatured(user._id).catch(err => console.log(err));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
33
src/server/api/endpoints/charts/federation.ts
Normal file
33
src/server/api/endpoints/charts/federation.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import $ from 'cafy';
|
||||
import getParams from '../../get-params';
|
||||
import federationChart from '../../../../chart/federation';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': 'フェデレーションのチャートを取得します。'
|
||||
},
|
||||
|
||||
params: {
|
||||
span: $.str.or(['day', 'hour']).note({
|
||||
desc: {
|
||||
'ja-JP': '集計のスパン (day または hour)'
|
||||
}
|
||||
}),
|
||||
|
||||
limit: $.num.optional.range(1, 100).note({
|
||||
default: 30,
|
||||
desc: {
|
||||
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
const [ps, psErr] = getParams(meta, params);
|
||||
if (psErr) throw psErr;
|
||||
|
||||
const stats = await federationChart.getChart(ps.span as any, ps.limit);
|
||||
|
||||
res(stats);
|
||||
});
|
||||
@@ -49,6 +49,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
|
||||
swPublickey: config.sw ? config.sw.public_key : null,
|
||||
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
|
||||
bannerUrl: meta.bannerUrl,
|
||||
maxNoteTextLength: config.maxNoteTextLength,
|
||||
|
||||
features: {
|
||||
registration: !meta.disableRegistration,
|
||||
|
||||
@@ -28,6 +28,8 @@ import perUserNotesChart from '../../chart/per-user-notes';
|
||||
|
||||
import { erase, unique } from '../../prelude/array';
|
||||
import insertNoteUnread from './unread';
|
||||
import registerInstance from '../register-instance';
|
||||
import Instance from '../../models/instance';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@@ -170,6 +172,20 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
||||
notesChart.update(note, true);
|
||||
perUserNotesChart.update(user, note, true);
|
||||
|
||||
// Register host
|
||||
if (isRemoteUser(user)) {
|
||||
registerInstance(user.host).then(i => {
|
||||
Instance.update({ _id: i._id }, {
|
||||
$inc: {
|
||||
notesCount: 1
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newNote();
|
||||
});
|
||||
}
|
||||
|
||||
// ハッシュタグ登録
|
||||
tags.map(tag => registerHashtag(user, tag));
|
||||
|
||||
|
||||
22
src/services/register-instance.ts
Normal file
22
src/services/register-instance.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Instance, { IInstance } from '../models/instance';
|
||||
import federationChart from '../chart/federation';
|
||||
|
||||
export default async function(host: string): Promise<IInstance> {
|
||||
if (host == null) return null;
|
||||
|
||||
const index = await Instance.findOne({ host });
|
||||
|
||||
if (index == null) {
|
||||
const i = await Instance.insert({
|
||||
host,
|
||||
caughtAt: new Date(),
|
||||
system: null // TODO
|
||||
});
|
||||
|
||||
federationChart.update(true);
|
||||
|
||||
return i;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user