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,245 @@
import { Blockings, Followings, Mutings, RenoteMutings, UserListJoinings, UserProfiles } from "../../../../models/index.js";
import { apiLogger } from "../../logger.js";
import { MastodonStreamUser } from "./channels/user.js";
import { MastodonStreamDirect } from "./channels/direct.js";
import { MastodonStreamPublic } from "./channels/public.js";
import { MastodonStreamList } from "./channels/list.js";
import { toSingleLast } from "../../../../prelude/array.js";
import { MastodonStreamTag } from "./channels/tag.js";
const logger = apiLogger.createSubLogger("streaming").createSubLogger("mastodon");
const channels = {
"user": MastodonStreamUser,
"user:notification": MastodonStreamUser,
"direct": MastodonStreamDirect,
"list": MastodonStreamList,
"public": MastodonStreamPublic,
"public:media": MastodonStreamPublic,
"public:local": MastodonStreamPublic,
"public:local:media": MastodonStreamPublic,
"public:remote": MastodonStreamPublic,
"public:remote:media": MastodonStreamPublic,
"public:allow_local_only": MastodonStreamPublic,
"public:allow_local_only:media": MastodonStreamPublic,
"hashtag": MastodonStreamTag,
"hashtag:local": MastodonStreamTag
};
export class MastodonStreamingConnection {
user;
userProfile;
following = new Set();
muting = new Set();
renoteMuting = new Set();
blocking = new Set();
hidden = new Set();
token;
wsConnection;
channels = [];
subscriber;
constructor(wsConnection, subscriber, user, token, query){
const channel = toSingleLast(query.stream);
logger.debug(`New connection on channel: ${channel}`);
this.wsConnection = wsConnection;
this.subscriber = subscriber;
if (user) this.user = user;
if (token) this.token = token;
this.onMessage = this.onMessage.bind(this);
this.onUserEvent = this.onUserEvent.bind(this);
this.wsConnection.on("message", this.onMessage);
if (this.user) {
this.updateFollowing();
this.updateMuting();
this.updateRenoteMuting();
this.updateBlocking();
this.updateHidden();
this.updateUserProfile();
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
}
if (channel) {
const list = toSingleLast(query.list);
const tag = toSingleLast(query.tag);
this.onMessage({
type: "utf8",
utf8Data: JSON.stringify({
stream: channel,
type: "subscribe",
list,
tag
})
});
}
}
onUserEvent(data) {
switch(data.type){
case "follow":
this.following.add(data.body.id);
break;
case "unfollow":
this.following.delete(data.body.id);
break;
case "mute":
this.muting.add(data.body.id);
break;
case "unmute":
this.muting.delete(data.body.id);
break;
case "userHidden":
this.hidden.add(data.body);
break;
case "userUnhidden":
this.hidden.delete(data.body);
break;
// TODO: renote mute events
// TODO: block events
case "updateUserProfile":
this.userProfile = data.body;
break;
case "terminate":
this.closeConnection();
break;
default:
break;
}
}
async onMessage(data) {
if (data.type !== "utf8") return;
if (data.utf8Data == null) return;
let message;
try {
message = JSON.parse(data.utf8Data);
} catch (e) {
logger.error("Failed to parse json data, ignoring");
return;
}
const { stream, type, list, tag } = message;
if (!message.stream || !message.type) {
logger.error("Invalid message received, ignoring");
return;
}
if (list ?? tag) logger.info(`${type}: ${stream} ${list ?? tag}`);
else logger.info(`${type}: ${stream}`);
switch(type){
case "subscribe":
this.connectChannel(stream, list, tag);
break;
case "unsubscribe":
this.disconnectChannel(stream);
break;
}
}
send(stream, event, payload) {
const json = JSON.stringify({
stream: [
stream
],
event: event,
payload: typeof payload === "string" ? payload : JSON.stringify(payload)
});
this.wsConnection.send(json);
}
connectChannel(channel, list, tag) {
if (!channels[channel]) {
logger.info(`Ignoring connection to unknown channel ${channel}`);
return;
}
if (channels[channel].requireCredential) {
if (this.user == null) {
logger.info(`Refusing connection to channel ${channel} without authentication, terminating connection`);
this.closeConnection();
return;
} else if (!channels[channel].requiredScopes.every((p)=>this.token?.scopes?.includes(p))) {
logger.info(`Refusing connection to channel ${channel} without required OAuth scopes, terminating connection`);
this.closeConnection();
return;
}
}
if (channels[channel].shouldShare && this.channels.some((c)=>c.chName === channel)) {
return;
}
let ch;
if (channel === "list") {
ch = new channels[channel](this, channel, list);
} else if (channel.startsWith("hashtag")) ch = new channels[channel](this, channel, tag);
else ch = new channels[channel](this, channel);
this.channels.push(ch);
ch.init(null);
}
disconnectChannel(channelName) {
const channel = this.channels.find((c)=>c.chName === channelName);
if (channel) {
if (channel.dispose) channel.dispose();
this.channels = this.channels.filter((c)=>c.chName !== channelName);
}
}
async updateFollowing() {
const followings = await Followings.find({
where: {
followerId: this.user.id
},
select: [
"followeeId"
]
});
this.following = new Set(followings.map((x)=>x.followeeId));
}
async updateMuting() {
const mutings = await Mutings.find({
where: {
muterId: this.user.id
},
select: [
"muteeId"
]
});
this.muting = new Set(mutings.map((x)=>x.muteeId));
}
async updateRenoteMuting() {
const renoteMutings = await RenoteMutings.find({
where: {
muterId: this.user.id
},
select: [
"muteeId"
]
});
this.renoteMuting = new Set(renoteMutings.map((x)=>x.muteeId));
}
async updateBlocking() {
const blockings = await Blockings.find({
where: {
blockeeId: this.user.id
},
select: [
"blockerId"
]
});
this.blocking = new Set(blockings.map((x)=>x.blockerId));
}
async updateHidden() {
const hidden = await UserListJoinings.find({
where: {
userList: {
userId: this.user.id,
hideFromHomeTl: true
}
},
select: [
"userId"
]
});
this.hidden = new Set(hidden.map((x)=>x.userId));
}
async updateUserProfile() {
this.userProfile = await UserProfiles.findOneBy({
userId: this.user.id
});
}
closeConnection() {
this.wsConnection.close();
this.dispose();
}
dispose() {
for (const c of this.channels.filter((c)=>c.dispose)){
if (c.dispose) c.dispose();
}
}
}