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:
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
64
src/server/api/stream/channels/social-timeline.ts
Normal file
64
src/server/api/stream/channels/social-timeline.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user