Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1,12 @@
import Channel from "../channel.js";
export default class extends Channel {
chName = "admin";
static shouldShare = true;
static requireCredential = true;
async init(params) {
// Subscribe admin stream
this.subscriber.on(`adminStream:${this.user.id}`, (data)=>{
this.send(data);
});
}
}
@@ -0,0 +1,48 @@
import Channel from "../channel.js";
import { Notes } from "../../../../models/index.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { IdentifiableError } from "../../../../misc/identifiable-error.js";
export default class extends Channel {
chName = "antenna";
static shouldShare = false;
static requireCredential = false;
antennaId;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
}
async init(params) {
this.antennaId = params.antennaId;
// Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
}
async onEvent(data) {
if (data.type === "note") {
try {
const note = await Notes.pack(data.body.id, this.user, {
detail: true
});
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
this.connection.cacheNote(note);
this.send("note", note);
} catch (e) {
if (e instanceof IdentifiableError && e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") {
// skip: note not visible to user
return;
} else {
throw e;
}
}
} else {
this.send(data.type, data.body);
}
}
dispose() {
// Unsubscribe events
this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
}
}
@@ -0,0 +1,65 @@
import Channel from "../channel.js";
import { Users } from "../../../../models/index.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
export default class extends Channel {
chName = "channel";
static shouldShare = false;
static requireCredential = false;
channelId;
typers = new Map();
emitTypersIntervalId;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
this.emitTypers = this.emitTypers.bind(this);
}
async init(params) {
this.channelId = params.channelId;
// Subscribe stream
this.subscriber.on("notesStream", this.onNote);
this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent);
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
}
async onNote(note) {
if (note.visibility === "hidden") return;
if (note.channelId !== this.channelId) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
onEvent(data) {
if (data.type === "typing") {
const id = data.body;
const begin = !this.typers.has(id);
this.typers.set(id, new Date());
if (begin) {
this.emitTypers();
}
}
}
async emitTypers() {
const now = new Date();
// Remove not typing users
for (const [userId, date] of Object.entries(this.typers)){
if (now.getTime() - date.getTime() > 5000) this.typers.delete(userId);
}
const userIds = Array.from(this.typers.keys());
const users = await Users.packMany(userIds, null, {
detail: false
});
this.send({
type: "typers",
body: users
});
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent);
clearInterval(this.emitTypersIntervalId);
}
}
@@ -0,0 +1,12 @@
import Channel from "../channel.js";
export default class extends Channel {
chName = "drive";
static shouldShare = true;
static requireCredential = true;
async init(params) {
// Subscribe drive stream
this.subscriber.on(`driveStream:${this.user.id}`, (data)=>{
this.send(data);
});
}
}
@@ -0,0 +1,53 @@
import Channel from "../channel.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { isInstanceMuted } from "../../../../misc/is-instance-muted.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { isFiltered } from "../../../../misc/is-filtered.js";
export default class extends Channel {
chName = "globalTimeline";
static shouldShare = true;
static requireCredential = false;
withReplies;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
const meta = await fetchMeta();
if (meta.disableGlobalTimeline) {
if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return;
}
this.withReplies = params.withReplies;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
async onNote(note) {
if (note.visibility !== "public") return;
if (note.channelId != null) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (this.userProfile && await isFiltered(note, this.user, this.userProfile)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
}
}
@@ -0,0 +1,55 @@
import Channel from "../channel.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { isInstanceMuted } from "../../../../misc/is-instance-muted.js";
import { isFiltered } from "../../../../misc/is-filtered.js";
export default class extends Channel {
chName = "homeTimeline";
static shouldShare = true;
static requireCredential = true;
withReplies;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
this.withReplies = params.withReplies;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
async onNote(note) {
if (note.visibility === "hidden") return;
if (note.channelId) {
if (!this.followingChannels.has(note.channelId)) return;
} else {
// その投稿のユーザーをフォローしていなかったら弾く
if (this.user.id !== note.userId && !this.following.has(note.userId)) return;
}
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
// Members of lists with hideFromHome set
if (note.userId !== this.user.id && isUserRelated(note, this.hidden)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (this.userProfile && await isFiltered(note, this.user, this.userProfile)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
}
}
@@ -0,0 +1,57 @@
import Channel from "../channel.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { isInstanceMuted } from "../../../../misc/is-instance-muted.js";
import { isFiltered } from "../../../../misc/is-filtered.js";
export default class extends Channel {
chName = "hybridTimeline";
static shouldShare = true;
static requireCredential = true;
withReplies;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
const meta = await fetchMeta();
if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return;
this.withReplies = params.withReplies;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
async onNote(note) {
if (note.visibility === "hidden") return;
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
// フォローしているチャンネルの投稿 の場合だけ
if (!(note.channelId == null && this.user.id === note.userId || note.channelId == null && this.following.has(note.userId) || note.channelId == null && note.user.host == null && note.visibility === "public" || note.channelId != null && this.followingChannels.has(note.channelId))) return;
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
// Members of lists with hideFromHome set
if (note.userId !== this.user.id && isUserRelated(note, this.hidden)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (this.userProfile && await isFiltered(note, this.user, this.userProfile)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
}
}
@@ -0,0 +1,40 @@
import main from "./main.js";
import homeTimeline from "./home-timeline.js";
import localTimeline from "./local-timeline.js";
import hybridTimeline from "./hybrid-timeline.js";
import recommendedTimeline from "./recommended-timeline.js";
import globalTimeline from "./global-timeline.js";
import serverStats from "./server-stats.js";
import queueStats from "./queue-stats.js";
import userList from "./user-list.js";
import antenna from "./antenna.js";
import messaging from "./messaging.js";
import messagingIndex from "./messaging-index.js";
import drive from "./drive.js";
import channel from "./channel.js";
import admin from "./admin.js";
import reversi from "./reversi.js";
import reversiGame from "./reversi-game.js";
import shogi from "./shogi.js";
import shogiGame from "./shogi-game.js";
export default {
main,
homeTimeline,
localTimeline,
recommendedTimeline,
hybridTimeline,
globalTimeline,
serverStats,
queueStats,
userList,
antenna,
messaging,
messagingIndex,
drive,
channel,
admin,
reversi,
reversiGame,
shogi,
shogiGame
};
@@ -0,0 +1,51 @@
import Channel from "../channel.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { isFiltered } from "../../../../misc/is-filtered.js";
export default class extends Channel {
chName = "localTimeline";
static shouldShare = true;
static requireCredential = false;
withReplies;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
const meta = await fetchMeta();
if (meta.disableLocalTimeline) {
if (this.user == null || !(this.user.isAdmin || this.user.isModerator)) return;
}
this.withReplies = params.withReplies;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
async onNote(note) {
if (note.user.host !== null) return;
if (note.visibility !== "public") return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (this.userProfile && await isFiltered(note, this.user, this.userProfile)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
}
}
@@ -0,0 +1,28 @@
import Channel from "../channel.js";
import { isInstanceMuted, isUserFromMutedInstance } from "../../../../misc/is-instance-muted.js";
export default class extends Channel {
chName = "main";
static shouldShare = true;
static requireCredential = true;
async init(params) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user.id}`, async (data)=>{
switch(data.type){
case "notification":
{
// Ignore notifications from instances the user has muted
if (isUserFromMutedInstance(data.body, new Set(this.userProfile?.mutedInstances ?? []))) return;
if (data.body.userId && this.muting.has(data.body.userId)) return;
break;
}
case "mention":
{
if (isInstanceMuted(data.body, new Set(this.userProfile?.mutedInstances ?? []))) return;
if (this.muting.has(data.body.userId)) return;
break;
}
}
this.send(data.type, data.body);
});
}
}
@@ -0,0 +1,12 @@
import Channel from "../channel.js";
export default class extends Channel {
chName = "messagingIndex";
static shouldShare = true;
static requireCredential = true;
async init(params) {
// Subscribe messaging index stream
this.subscriber.on(`messagingIndexStream:${this.user.id}`, (data)=>{
this.send(data);
});
}
}
@@ -0,0 +1,95 @@
import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from "../../common/read-messaging-message.js";
import Channel from "../channel.js";
import { UserGroupJoinings, Users, MessagingMessages } from "../../../../models/index.js";
export default class extends Channel {
chName = "messaging";
static shouldShare = false;
static requireCredential = true;
otherpartyId;
otherparty;
groupId;
subCh;
typers = new Map();
emitTypersIntervalId;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
this.onMessage = this.onMessage.bind(this);
this.emitTypers = this.emitTypers.bind(this);
}
async init(params) {
this.otherpartyId = params.otherparty;
this.otherparty = this.otherpartyId ? await Users.findOneByOrFail({
id: this.otherpartyId
}) : null;
this.groupId = params.group;
// Check joining
if (this.groupId) {
const joining = await UserGroupJoinings.findOneBy({
userId: this.user.id,
userGroupId: this.groupId
});
if (joining == null) {
return;
}
}
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
this.subCh = this.otherpartyId ? `messagingStream:${this.user.id}-${this.otherpartyId}` : `messagingStream:${this.groupId}`;
// Subscribe messaging stream
this.subscriber.on(this.subCh, this.onEvent);
}
onEvent(data) {
if (data.type === "typing") {
const id = data.body;
const begin = !this.typers.has(id);
this.typers.set(id, new Date());
if (begin) {
this.emitTypers();
}
} else {
this.send(data);
}
}
onMessage(type, body) {
switch(type){
case "read":
if (this.otherpartyId) {
readUserMessagingMessage(this.user.id, this.otherpartyId, [
body.id
]);
// リモートユーザーからのメッセージだったら既読配信
if (Users.isLocalUser(this.user) && Users.isRemoteUser(this.otherparty)) {
MessagingMessages.findOneBy({
id: body.id
}).then((message)=>{
if (message) deliverReadActivity(this.user, this.otherparty, message);
});
}
} else if (this.groupId) {
readGroupMessagingMessage(this.user.id, this.groupId, [
body.id
]);
}
break;
}
}
async emitTypers() {
const now = new Date();
// Remove not typing users
for (const [userId, date] of this.typers.entries()){
if (now.getTime() - date.getTime() > 5000) this.typers.delete(userId);
}
const userIds = Array.from(this.typers.keys());
const users = await Users.packMany(userIds, null, {
detail: false
});
this.send({
type: "typers",
body: users
});
}
dispose() {
this.subscriber.off(this.subCh, this.onEvent);
clearInterval(this.emitTypersIntervalId);
}
}
@@ -0,0 +1,35 @@
import Xev from "xev";
import Channel from "../channel.js";
const ev = new Xev();
export default class extends Channel {
chName = "queueStats";
static shouldShare = true;
static requireCredential = false;
constructor(id, connection){
super(id, connection);
this.onStats = this.onStats.bind(this);
this.onMessage = this.onMessage.bind(this);
}
async init(params) {
ev.addListener("queueStats", this.onStats);
}
onStats(stats) {
this.send("stats", stats);
}
onMessage(type, body) {
switch(type){
case "requestLog":
ev.once(`queueStatsLog:${body.id}`, (statsLog)=>{
this.send("statsLog", statsLog);
});
ev.emit("requestQueueStatsLog", {
id: body.id,
length: body.length
});
break;
}
}
dispose() {
ev.removeListener("queueStats", this.onStats);
}
}
@@ -0,0 +1,56 @@
import Channel from "../channel.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
import { isInstanceMuted } from "../../../../misc/is-instance-muted.js";
import { isFiltered } from "../../../../misc/is-filtered.js";
export default class extends Channel {
chName = "recommendedTimeline";
static shouldShare = true;
static requireCredential = true;
withReplies;
constructor(id, connection){
super(id, connection);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
const meta = await fetchMeta();
if (meta.disableRecommendedTimeline && !this.user.isAdmin && !this.user.isModerator) return;
this.withReplies = params.withReplies;
// Subscribe events
this.subscriber.on("notesStream", this.onNote);
}
async onNote(note) {
if (note.visibility === "hidden") return;
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
// フォローしているチャンネルの投稿 の場合だけ
const meta = await fetchMeta();
if (!(note.user.host != null && meta.recommendedInstances.includes(note.user.host) && note.visibility === "public")) return;
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set(this.userProfile?.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if (this.userProfile && await isFiltered(note, this.user, this.userProfile)) return;
this.connection.cacheNote(note);
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off("notesStream", this.onNote);
}
}
@@ -0,0 +1,49 @@
import Channel from "../channel.js";
import { cancelGame, checkTimeout, gameReady, isValidUpdateKey, isValidUpdateValue, putStone, updateSettings } from "../../../../services/reversi/index.js";
export default class extends Channel {
chName = "reversiGame";
static shouldShare = false;
static requireCredential = false;
gameId = null;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
}
async init(params) {
if (typeof params.gameId !== "string") return;
this.gameId = params.gameId;
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
}
onEvent(data) {
this.send(data);
}
onMessage(type, body) {
if (!this.gameId) return;
switch(type){
case "ready":
if (this.user && typeof body === "boolean") gameReady(this.gameId, this.user, body);
break;
case "updateSettings":
if (this.user && body && isValidUpdateKey(body.key) && isValidUpdateValue(body.key, body.value)) {
updateSettings(this.gameId, this.user, body.key, body.value);
}
break;
case "cancel":
if (this.user) cancelGame(this.gameId, this.user);
break;
case "putStone":
if (this.user && body && typeof body.pos === "number") {
putStone(this.gameId, this.user, body.pos, typeof body.id === "string" ? body.id : null);
}
break;
case "claimTimeIsUp":
checkTimeout(this.gameId);
break;
}
}
dispose() {
if (this.gameId) {
this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
}
}
}
@@ -0,0 +1,19 @@
import Channel from "../channel.js";
export default class extends Channel {
chName = "reversi";
static shouldShare = true;
static requireCredential = true;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
}
async init(params) {
this.subscriber.on(`reversiStream:${this.user.id}`, this.onEvent);
}
onEvent(data) {
this.send(data);
}
dispose() {
this.subscriber.off(`reversiStream:${this.user.id}`, this.onEvent);
}
}
@@ -0,0 +1,35 @@
import Xev from "xev";
import Channel from "../channel.js";
const ev = new Xev();
export default class extends Channel {
chName = "serverStats";
static shouldShare = true;
static requireCredential = false;
constructor(id, connection){
super(id, connection);
this.onStats = this.onStats.bind(this);
this.onMessage = this.onMessage.bind(this);
}
async init(params) {
ev.addListener("serverStats", this.onStats);
}
onStats(stats) {
this.send("stats", stats);
}
onMessage(type, body) {
switch(type){
case "requestLog":
ev.once(`serverStatsLog:${body.id}`, (statsLog)=>{
this.send("statsLog", statsLog);
});
ev.emit("requestServerStatsLog", {
id: body.id,
length: body.length
});
break;
}
}
dispose() {
ev.removeListener("serverStats", this.onStats);
}
}
@@ -0,0 +1,41 @@
import Channel from "../channel.js";
import { cancelGame, gameReady, putMove } from "../../../../services/shogi/index.js";
export default class extends Channel {
chName = "shogiGame";
static shouldShare = false;
static requireCredential = false;
gameId = null;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
}
async init(params) {
if (typeof params.gameId !== "string") return;
this.gameId = params.gameId;
this.subscriber.on(`shogiGameStream:${this.gameId}`, this.onEvent);
}
onEvent(data) {
this.send(data);
}
onMessage(type, body) {
if (!this.gameId) return;
switch(type){
case "ready":
if (this.user && typeof body === "boolean") gameReady(this.gameId, this.user, body);
break;
case "cancel":
if (this.user) cancelGame(this.gameId, this.user);
break;
case "move":
if (this.user && body && typeof body.usi === "string") {
putMove(this.gameId, this.user, body.usi, typeof body.id === "string" ? body.id : null);
}
break;
}
}
dispose() {
if (this.gameId) {
this.subscriber.off(`shogiGameStream:${this.gameId}`, this.onEvent);
}
}
}
@@ -0,0 +1,19 @@
import Channel from "../channel.js";
export default class extends Channel {
chName = "shogi";
static shouldShare = true;
static requireCredential = true;
constructor(id, connection){
super(id, connection);
this.onEvent = this.onEvent.bind(this);
}
async init(params) {
this.subscriber.on(`shogiStream:${this.user.id}`, this.onEvent);
}
onEvent(data) {
this.send(data);
}
dispose() {
this.subscriber.off(`shogiStream:${this.user.id}`, this.onEvent);
}
}
@@ -0,0 +1,59 @@
import Channel from "../channel.js";
import { UserListJoinings, UserLists } from "../../../../models/index.js";
import { isUserRelated } from "../../../../misc/is-user-related.js";
export default class extends Channel {
chName = "userList";
static shouldShare = false;
static requireCredential = false;
listId;
listUsers = [];
listUsersClock;
constructor(id, connection){
super(id, connection);
this.updateListUsers = this.updateListUsers.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
async init(params) {
this.listId = params.listId;
// Check existence and owner
const exist = await UserLists.exist({
where: {
id: this.listId,
userId: this.user.id
}
});
if (!exist) return;
// Subscribe stream
this.subscriber.on(`userListStream:${this.listId}`, this.send);
this.subscriber.on("notesStream", this.onNote);
this.updateListUsers();
this.listUsersClock = setInterval(this.updateListUsers, 5000);
}
async updateListUsers() {
const users = await UserListJoinings.find({
where: {
userListId: this.listId
},
select: [
"userId"
]
});
this.listUsers = users.map((x)=>x.userId);
}
async onNote(note) {
if (note.visibility === "hidden") return;
if (!this.listUsers.includes(note.userId)) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
if (note.renote && !note.text && this.renoteMuting.has(note.userId)) return;
this.send("note", note);
}
dispose() {
// Unsubscribe events
this.subscriber.off(`userListStream:${this.listId}`, this.send);
this.subscriber.off("notesStream", this.onNote);
clearInterval(this.listUsersClock);
}
}