Use PostgreSQL instead of MongoDB (#4572)

* wip

* Update note.ts

* Update timeline.ts

* Update core.ts

* wip

* Update generate-visibility-query.ts

* wip

* wip

* wip

* wip

* wip

* Update global-timeline.ts

* wip

* wip

* wip

* Update vote.ts

* wip

* wip

* Update create.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update files.ts

* wip

* wip

* Update CONTRIBUTING.md

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update read-notification.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update cancel.ts

* wip

* wip

* wip

* Update show.ts

* wip

* wip

* Update gen-id.ts

* Update create.ts

* Update id.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Docker: Update files about Docker (#4599)

* Docker: Use cache if files used by `yarn install` was not updated

This patch reduces the number of times to installing node_modules.
For example, `yarn install` step will be skipped when only ".config/default.yml" is updated.

* Docker: Migrate MongoDB to Postgresql

Misskey uses Postgresql as a database instead of Mongodb since version 11.

* Docker: Uncomment about data persistence

This patch will save a lot of databases.

* wip

* wip

* wip

* Update activitypub.ts

* wip

* wip

* wip

* Update logs.ts

* wip

* Update drive-file.ts

* Update register.ts

* wip

* wip

* Update mentions.ts

* wip

* wip

* wip

* Update recommendation.ts

* wip

* Update index.ts

* wip

* Update recommendation.ts

* Doc: Update docker.ja.md and docker.en.md (#1) (#4608)

Update how to set up misskey.

* wip

* ✌️

* wip

* Update note.ts

* Update postgre.ts

* wip

* wip

* wip

* wip

* Update add-file.ts

* wip

* wip

* wip

* Clean up

* Update logs.ts

* wip

* 🍕

* wip

* Ad notes

* wip

* Update api-visibility.ts

* Update note.ts

* Update add-file.ts

* tests

* tests

* Update postgre.ts

* Update utils.ts

* wip

* wip

* Refactor

* wip

* Refactor

* wip

* wip

* Update show-users.ts

* Update update-instance.ts

* wip

* Update feed.ts

* Update outbox.ts

* Update outbox.ts

* Update user.ts

* wip

* Update list.ts

* Update update-hashtag.ts

* wip

* Update update-hashtag.ts

* Refactor

* Update update.ts

* wip

* wip

* ✌️

* clean up

* docs

* Update push.ts

* wip

* Update api.ts

* wip

* ✌️

* Update make-pagination-query.ts

* ✌️

* Delete hashtags.ts

* Update instances.ts

* Update instances.ts

* Update create.ts

* Update search.ts

* Update reversi-game.ts

* Update signup.ts

* Update user.ts

* id

* Update example.yml

* 🎨

* objectid

* fix

* reversi

* reversi

* Fix bug of chart engine

* Add test of chart engine

* Improve test

* Better testing

* Improve chart engine

* Refactor

* Add test of chart engine

* Refactor

* Add chart test

* Fix bug

* コミットし忘れ

* Refactoring

* ✌️

* Add tests

* Add test

* Extarct note tests

* Refactor

* 存在しないユーザーにメンションできなくなっていた問題を修正

* Fix bug

* Update update-meta.ts

* Fix bug

* Update mention.vue

* Fix bug

* Update meta.ts

* Update CONTRIBUTING.md

* Fix bug

* Fix bug

* Fix bug

* Clean up

* Clean up

* Update notification.ts

* Clean up

* Add mute tests

* Add test

* Refactor

* Add test

* Fix test

* Refactor

* Refactor

* Add tests

* Update utils.ts

* Update utils.ts

* Fix test

* Update package.json

* Update update.ts

* Update manifest.ts

* Fix bug

* Fix bug

* Add test

* 🎨

* Update endpoint permissions

* Updaye permisison

* Update person.ts

#4299

* データベースと同期しないように

* Fix bug

* Fix bug

* Update reversi-game.ts

* Use a feature of Node v11.7.0 to extract a public key (#4644)

* wip

* wip

* ✌️

* Refactoring

#1540

* test

* test

* test

* test

* test

* test

* test

* Fix bug

* Fix test

* 🍣

* wip

* #4471

* Add test for #4335

* Refactor

* Fix test

* Add tests

* 🕓

* Fix bug

* Add test

* Add test

* rename

* Fix bug
This commit is contained in:
syuilo
2019-04-07 21:50:36 +09:00
committed by GitHub
parent 13caf37991
commit f0a29721c9
592 changed files with 13463 additions and 14147 deletions

View File

@ -15,6 +15,14 @@ export default abstract class Channel {
return this.connection.user;
}
protected get following() {
return this.connection.following;
}
protected get muting() {
return this.connection.muting;
}
protected get subscriber() {
return this.connection.subscriber;
}

View File

@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe admin stream
this.subscriber.on(`adminStream:${this.user._id}`, data => {
this.subscriber.on(`adminStream:${this.user.id}`, data => {
this.send(data);
});
}

View File

@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe drive stream
this.subscriber.on(`driveStream:${this.user._id}`, data => {
this.subscriber.on(`driveStream:${this.user.id}`, data => {
this.send(data);
});
}

View File

@ -1,22 +1,22 @@
import autobind from 'autobind-decorator';
import * as CRC32 from 'crc-32';
import * as mongo from 'mongodb';
import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
import { publishReversiGameStream } from '../../../../../services/stream';
import Reversi from '../../../../../games/reversi/core';
import * as maps from '../../../../../games/reversi/maps';
import Channel from '../../channel';
import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
import { ReversiGames } from '../../../../../models';
export default class extends Channel {
public readonly chName = 'gamesReversiGame';
public static shouldShare = false;
public static requireCredential = false;
private gameId: mongo.ObjectID;
private gameId: ReversiGame['id'];
@autobind
public async init(params: any) {
this.gameId = new mongo.ObjectID(params.gameId as string);
this.gameId = params.gameId;
// Subscribe game stream
this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
@ -29,7 +29,7 @@ export default class extends Channel {
switch (type) {
case 'accept': this.accept(true); break;
case 'cancelAccept': this.accept(false); break;
case 'updateSettings': this.updateSettings(body.settings); break;
case 'updateSettings': this.updateSettings(body.key, body.value); break;
case 'initForm': this.initForm(body); break;
case 'updateForm': this.updateForm(body.id, body.value); break;
case 'message': this.message(body); break;
@ -39,54 +39,55 @@ export default class extends Channel {
}
@autobind
private async updateSettings(settings: any) {
const game = await ReversiGame.findOne({ _id: this.gameId });
private async updateSettings(key: string, value: any) {
const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
if (game.user1Id.equals(this.user._id) && game.user1Accepted) return;
if (game.user2Id.equals(this.user._id) && game.user2Accepted) return;
if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
if ((game.user1Id === this.user.id) && game.user1Accepted) return;
if ((game.user2Id === this.user.id) && game.user2Accepted) return;
await ReversiGame.update({ _id: this.gameId }, {
$set: {
settings
}
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
await ReversiGames.update({ id: this.gameId }, {
[key]: value
});
publishReversiGameStream(this.gameId, 'updateSettings', settings);
publishReversiGameStream(this.gameId, 'updateSettings', {
key: key,
value: value
});
}
@autobind
private async initForm(form: any) {
const game = await ReversiGame.findOne({ _id: this.gameId });
const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
const set = game.user1Id.equals(this.user._id) ? {
const set = game.user1Id === this.user.id ? {
form1: form
} : {
form2: form
};
form2: form
};
await ReversiGame.update({ _id: this.gameId }, {
$set: set
});
await ReversiGames.update({ id: this.gameId }, set);
publishReversiGameStream(this.gameId, 'initForm', {
userId: this.user._id,
userId: this.user.id,
form
});
}
@autobind
private async updateForm(id: string, value: any) {
const game = await ReversiGame.findOne({ _id: this.gameId });
const game = await ReversiGames.findOne({ id: this.gameId });
if (game.isStarted) return;
if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1;
const form = game.user1Id === this.user.id ? game.form2 : game.form1;
const item = form.find((i: any) => i.id == id);
@ -94,18 +95,16 @@ export default class extends Channel {
item.value = value;
const set = game.user1Id.equals(this.user._id) ? {
const set = game.user1Id === this.user.id ? {
form2: form
} : {
form1: form
};
await ReversiGame.update({ _id: this.gameId }, {
$set: set
});
await ReversiGames.update({ id: this.gameId }, set);
publishReversiGameStream(this.gameId, 'updateForm', {
userId: this.user._id,
userId: this.user.id,
id,
value
});
@ -115,24 +114,22 @@ export default class extends Channel {
private async message(message: any) {
message.id = Math.random();
publishReversiGameStream(this.gameId, 'message', {
userId: this.user._id,
userId: this.user.id,
message
});
}
@autobind
private async accept(accept: boolean) {
const game = await ReversiGame.findOne({ _id: this.gameId });
const game = await ReversiGames.findOne(this.gameId);
if (game.isStarted) return;
let bothAccepted = false;
if (game.user1Id.equals(this.user._id)) {
await ReversiGame.update({ _id: this.gameId }, {
$set: {
user1Accepted: accept
}
if (game.user1Id === this.user.id) {
await ReversiGames.update({ id: this.gameId }, {
user1Accepted: accept
});
publishReversiGameStream(this.gameId, 'changeAccepts', {
@ -141,11 +138,9 @@ export default class extends Channel {
});
if (accept && game.user2Accepted) bothAccepted = true;
} else if (game.user2Id.equals(this.user._id)) {
await ReversiGame.update({ _id: this.gameId }, {
$set: {
user2Accepted: accept
}
} else if (game.user2Id === this.user.id) {
await ReversiGames.update({ id: this.gameId }, {
user2Accepted: accept
});
publishReversiGameStream(this.gameId, 'changeAccepts', {
@ -161,15 +156,15 @@ export default class extends Channel {
if (bothAccepted) {
// 3秒後、まだacceptされていたらゲーム開始
setTimeout(async () => {
const freshGame = await ReversiGame.findOne({ _id: this.gameId });
const freshGame = await ReversiGames.findOne(this.gameId);
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
let bw: number;
if (freshGame.settings.bw == 'random') {
if (freshGame.bw == 'random') {
bw = Math.random() > 0.5 ? 1 : 2;
} else {
bw = freshGame.settings.bw as number;
bw = parseInt(freshGame.bw, 10);
}
function getRandomMap() {
@ -178,22 +173,20 @@ export default class extends Channel {
return Object.values(maps)[rnd].data;
}
const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
const map = freshGame.map != null ? freshGame.map : getRandomMap();
await ReversiGame.update({ _id: this.gameId }, {
$set: {
startedAt: new Date(),
isStarted: true,
black: bw,
'settings.map': map
}
await ReversiGames.update({ id: this.gameId }, {
startedAt: new Date(),
isStarted: true,
black: bw,
map: map
});
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
const o = new Reversi(map, {
isLlotheo: freshGame.settings.isLlotheo,
canPutEverywhere: freshGame.settings.canPutEverywhere,
loopedBoard: freshGame.settings.loopedBoard
isLlotheo: freshGame.isLlotheo,
canPutEverywhere: freshGame.canPutEverywhere,
loopedBoard: freshGame.loopedBoard
});
if (o.isEnded) {
@ -206,23 +199,22 @@ export default class extends Channel {
winner = null;
}
await ReversiGame.update({
_id: this.gameId
await ReversiGames.update({
id: this.gameId
}, {
$set: {
isEnded: true,
winnerId: winner
}
});
isEnded: true,
winnerId: winner
});
publishReversiGameStream(this.gameId, 'ended', {
winnerId: winner,
game: await pack(this.gameId, this.user)
game: await ReversiGames.pack(this.gameId, this.user)
});
}
//#endregion
publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user));
publishReversiGameStream(this.gameId, 'started',
await ReversiGames.pack(this.gameId, this.user));
}, 3000);
}
}
@ -230,16 +222,16 @@ export default class extends Channel {
// 石を打つ
@autobind
private async set(pos: number) {
const game = await ReversiGame.findOne({ _id: this.gameId });
const game = await ReversiGames.findOne(this.gameId);
if (!game.isStarted) return;
if (game.isEnded) return;
if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
const o = new Reversi(game.settings.map, {
isLlotheo: game.settings.isLlotheo,
canPutEverywhere: game.settings.canPutEverywhere,
loopedBoard: game.settings.loopedBoard
const o = new Reversi(game.map, {
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard
});
for (const log of game.logs) {
@ -247,7 +239,7 @@ export default class extends Channel {
}
const myColor =
(game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2)
((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
? true
: false;
@ -271,20 +263,18 @@ export default class extends Channel {
pos
};
const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
await ReversiGame.update({
_id: this.gameId
game.logs.push(log);
await ReversiGames.update({
id: this.gameId
}, {
$set: {
crc32,
isEnded: o.isEnded,
winnerId: winner
},
$push: {
logs: log
}
});
crc32,
isEnded: o.isEnded,
winnerId: winner,
logs: game.logs
});
publishReversiGameStream(this.gameId, 'set', Object.assign(log, {
next: o.turn
@ -293,14 +283,14 @@ export default class extends Channel {
if (o.isEnded) {
publishReversiGameStream(this.gameId, 'ended', {
winnerId: winner,
game: await pack(this.gameId, this.user)
game: await ReversiGames.pack(this.gameId, this.user)
});
}
}
@autobind
private async check(crc32: string) {
const game = await ReversiGame.findOne({ _id: this.gameId });
const game = await ReversiGames.findOne(this.gameId);
if (!game.isStarted) return;
@ -308,7 +298,7 @@ export default class extends Channel {
if (game.crc32 == null) return;
if (crc32 !== game.crc32) {
this.send('rescue', await pack(game, this.user));
this.send('rescue', await ReversiGames.pack(game, this.user));
}
}
}

View File

@ -1,8 +1,7 @@
import autobind from 'autobind-decorator';
import * as mongo from 'mongodb';
import Matching, { pack } from '../../../../../models/games/reversi/matching';
import { publishMainStream } from '../../../../../services/stream';
import Channel from '../../channel';
import { ReversiMatchings } from '../../../../../models';
export default class extends Channel {
public readonly chName = 'gamesReversi';
@ -12,7 +11,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe reversi stream
this.subscriber.on(`reversiStream:${this.user._id}`, data => {
this.subscriber.on(`reversiStream:${this.user.id}`, data => {
this.send(data);
});
}
@ -22,12 +21,12 @@ export default class extends Channel {
switch (type) {
case 'ping':
if (body.id == null) return;
const matching = await Matching.findOne({
parentId: this.user._id,
childId: new mongo.ObjectID(body.id)
const matching = await ReversiMatchings.findOne({
parentId: this.user.id,
childId: body.id
});
if (matching == null) return;
publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId));
publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, matching.childId));
break;
}
}

View File

@ -1,17 +1,14 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
private mutedUserIds: string[] = [];
@autobind
public async init(params: any) {
const meta = await fetchMeta();
@ -20,29 +17,26 @@ export default class extends Channel {
}
// Subscribe events
this.subscriber.on('globalTimeline', this.onNote);
const mute = await Mute.find({ muterId: this.user._id });
this.mutedUserIds = mute.map(m => m.muteeId.toString());
this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await pack(note.replyId, this.user, {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, this.user, {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.mutedUserIds)) return;
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@ -50,6 +44,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('globalTimeline', this.onNote);
this.subscriber.off('notesStream', this.onNote);
}
}

View File

@ -1,40 +1,46 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'hashtag';
public static shouldShare = false;
public static requireCredential = false;
private q: string[][];
@autobind
public async init(params: any) {
const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
this.q = params.q;
const q: string[][] = params.q;
if (q == null) return;
if (this.q == null) return;
// Subscribe stream
this.subscriber.on('hashtag', async note => {
const noteTags = note.tags.map((t: string) => t.toLowerCase());
const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
if (!matched) return;
this.subscriber.on('notesStream', this.onNote);
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, this.user, {
detail: true
});
}
@autobind
private async onNote(note: any) {
const noteTags = note.tags.map((t: string) => t.toLowerCase());
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase())));
if (!matched) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, mutedUserIds)) return;
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
this.send('note', note);
});
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}

View File

@ -1,42 +1,49 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
private mutedUserIds: string[] = [];
@autobind
public async init(params: any) {
// Subscribe events
this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote);
const mute = await Mute.find({ muterId: this.user._id });
this.mutedUserIds = mute.map(m => m.muteeId.toString());
this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, this.user, {
// その投稿のユーザーをフォローしていなかったら弾く
if (this.user.id !== note.userId && !this.following.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.mutedUserIds)) return;
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@ -44,6 +51,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote);
this.subscriber.off('notesStream', this.onNote);
}
}

View File

@ -1,55 +0,0 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
export default class extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
private mutedUserIds: string[] = [];
@autobind
public async init(params: any) {
const meta = await fetchMeta();
if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
// Subscribe events
this.subscriber.on('hybridTimeline', this.onNewNote);
this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote);
const mute = await Mute.find({ muterId: this.user._id });
this.mutedUserIds = mute.map(m => m.muteeId.toString());
}
@autobind
private async onNewNote(note: any) {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, this.user, {
detail: true
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.mutedUserIds)) return;
this.send('note', note);
}
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('hybridTimeline', this.onNewNote);
this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote);
}
}

View File

@ -1,7 +1,7 @@
import main from './main';
import homeTimeline from './home-timeline';
import localTimeline from './local-timeline';
import hybridTimeline from './hybrid-timeline';
import socialTimeline from './social-timeline';
import globalTimeline from './global-timeline';
import notesStats from './notes-stats';
import serverStats from './server-stats';
@ -20,7 +20,7 @@ export default {
main,
homeTimeline,
localTimeline,
hybridTimeline,
socialTimeline,
globalTimeline,
notesStats,
serverStats,

View File

@ -1,17 +1,14 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
private mutedUserIds: string[] = [];
@autobind
public async init(params: any) {
const meta = await fetchMeta();
@ -20,29 +17,39 @@ export default class extends Channel {
}
// Subscribe events
this.subscriber.on('localTimeline', this.onNote);
const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, this.user, {
if (note.user.host !== null) return;
if (note.visibility === 'home') return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.mutedUserIds)) return;
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@ -50,6 +57,6 @@ export default class extends Channel {
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('localTimeline', this.onNote);
this.subscriber.off('notesStream', this.onNote);
}
}

View File

@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Mute from '../../../../models/mute';
import Channel from '../channel';
import { Mutings } from '../../../../models';
export default class extends Channel {
public readonly chName = 'main';
@ -9,16 +9,15 @@ export default class extends Channel {
@autobind
public async init(params: any) {
const mute = await Mute.find({ muterId: this.user._id });
const mutedUserIds = mute.map(m => m.muteeId.toString());
const mute = await Mutings.find({ muterId: this.user.id });
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user._id}`, async data => {
this.subscriber.on(`mainStream:${this.user.id}`, async data => {
const { type, body } = data;
switch (type) {
case 'notification': {
if (mutedUserIds.includes(body.userId)) return;
if (mute.map(m => m.muteeId).includes(body.userId)) return;
if (body.note && body.note.isHidden) return;
break;
}

View File

@ -9,7 +9,7 @@ export default class extends Channel {
@autobind
public async init(params: any) {
// Subscribe messaging index stream
this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => {
this.subscriber.on(`messagingIndexStream:${this.user.id}`, data => {
this.send(data);
});
}

View File

@ -14,7 +14,7 @@ export default class extends Channel {
this.otherpartyId = params.otherparty as string;
// Subscribe messaging stream
this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => {
this.subscriber.on(`messagingStream:${this.user.id}-${this.otherpartyId}`, data => {
this.send(data);
});
}
@ -23,7 +23,7 @@ export default class extends Channel {
public onMessage(type: string, body: any) {
switch (type) {
case 'read':
read(this.user._id, this.otherpartyId, body.id);
read(this.user.id, this.otherpartyId, body.id);
break;
}
}

View File

@ -0,0 +1,64 @@
import autobind from 'autobind-decorator';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import Channel from '../channel';
import fetchMeta from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'socialTimeline';
public static shouldShare = true;
public static requireCredential = true;
@autobind
public async init(params: any) {
const meta = await fetchMeta();
if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@autobind
private async onNote(note: any) {
// 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ
if (!(
this.user.id === note.userId ||
this.following.includes(note.userId) ||
note.user.host === null
)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}

View File

@ -1,23 +1,81 @@
import autobind from 'autobind-decorator';
import Channel from '../channel';
import { pack } from '../../../../models/note';
import { Notes, UserListJoinings } from '../../../../models';
import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
import { User } from '../../../../models/entities/user';
export default class extends Channel {
public readonly chName = 'userList';
public static shouldShare = false;
public static requireCredential = false;
private listId: string;
public listUsers: User['id'][] = [];
private listUsersClock: NodeJS.Timer;
@autobind
public async init(params: any) {
const listId = params.listId as string;
this.listId = params.listId as string;
// Subscribe stream
this.subscriber.on(`userListStream:${listId}`, async data => {
// 再パック
if (data.type == 'note') data.body = await pack(data.body.id, this.user, {
this.subscriber.on(`userListStream:${this.listId}`, this.send);
this.subscriber.on('notesStream', this.onNote);
this.updateListUsers();
this.listUsersClock = setInterval(this.updateListUsers, 5000);
}
@autobind
private async updateListUsers() {
const users = await UserListJoinings.find({
where: {
userListId: this.listId,
},
select: ['userId']
});
this.listUsers = users.map(x => x.userId);
}
@autobind
private async onNote(note: any) {
if (!this.listUsers.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true
});
this.send(data);
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true
});
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
this.send('note', note);
}
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off(`userListStream:${this.listId}`, this.send);
this.subscriber.off('notesStream', this.onNote);
clearInterval(this.listUsersClock);
}
}

View File

@ -1,33 +1,35 @@
import autobind from 'autobind-decorator';
import * as websocket from 'websocket';
import User, { IUser } from '../../../models/user';
import readNotification from '../common/read-notification';
import { readNotification } from '../common/read-notification';
import call from '../call';
import { IApp } from '../../../models/app';
import readNote from '../../../services/note/read';
import Channel from './channel';
import channels from './channels';
import { EventEmitter } from 'events';
import { User } from '../../../models/entities/user';
import { App } from '../../../models/entities/app';
import { Users, Followings, Mutings } from '../../../models';
/**
* Main stream connection
*/
export default class Connection {
public user?: IUser;
public app: IApp;
public user?: User;
public following: User['id'][] = [];
public muting: User['id'][] = [];
public app: App;
private wsConnection: websocket.connection;
public subscriber: EventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
public sendMessageToWsOverride: any = null; // 後方互換性のため
private followingClock: NodeJS.Timer;
private mutingClock: NodeJS.Timer;
constructor(
wsConnection: websocket.connection,
subscriber: EventEmitter,
user: IUser,
app: IApp
user: User,
app: App
) {
this.wsConnection = wsConnection;
this.user = user;
@ -35,6 +37,14 @@ export default class Connection {
this.subscriber = subscriber;
this.wsConnection.on('message', this.onWsConnectionMessage);
if (this.user) {
this.updateFollowing();
this.followingClock = setInterval(this.updateFollowing, 5000);
this.updateMuting();
this.mutingClock = setInterval(this.updateMuting, 5000);
}
}
/**
@ -64,7 +74,7 @@ export default class Connection {
@autobind
private async onApiRequest(payload: any) {
// 新鮮なデータを利用するためにユーザーをフェッチ
const user = this.user ? await User.findOne({ _id: this.user._id }) : null;
const user = this.user ? await Users.findOne(this.user.id) : null;
const endpoint = payload.endpoint || payload.ep; // alias
@ -79,7 +89,7 @@ export default class Connection {
@autobind
private onReadNotification(payload: any) {
if (!payload.id) return;
readNotification(this.user._id, payload.id);
readNotification(this.user.id, [payload.id]);
}
/**
@ -100,7 +110,7 @@ export default class Connection {
}
if (payload.read) {
readNote(this.user._id, payload.id);
readNote(this.user.id, payload.id);
}
}
@ -150,7 +160,6 @@ export default class Connection {
*/
@autobind
public sendMessageToWs(type: string, payload: any) {
if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互換性のため
this.wsConnection.send(JSON.stringify({
type: type,
body: payload
@ -208,6 +217,30 @@ export default class Connection {
}
}
@autobind
private async updateFollowing() {
const followings = await Followings.find({
where: {
followerId: this.user.id
},
select: ['followeeId']
});
this.following = followings.map(x => x.followeeId);
}
@autobind
private async updateMuting() {
const mutings = await Mutings.find({
where: {
muterId: this.user.id
},
select: ['muteeId']
});
this.muting = mutings.map(x => x.muteeId);
}
/**
* ストリームが切れたとき
*/
@ -216,5 +249,8 @@ export default class Connection {
for (const c of this.channels.filter(c => c.dispose)) {
c.dispose();
}
if (this.followingClock) clearInterval(this.followingClock);
if (this.mutingClock) clearInterval(this.mutingClock);
}
}