more forum fixes

This commit is contained in:
MathMan05
2026-02-04 16:07:42 -06:00
parent 2b37cd2a5e
commit f1f253e515
5 changed files with 228 additions and 17 deletions
@@ -0,0 +1,84 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api";
import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent, FieldErrors, getPermission, ThreadMember, Message } from "@spacebar/util";
import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment, PostDataSchema } from "@spacebar/schemas";
import { Request, Response, Router } from "express";
import { messageUpload } from "./messages";
import { HTTPError } from "#util/util/lambert-server";
import { FindManyOptions, FindOptionsOrder, In, Like } from "typeorm";
const router = Router({ mergeParams: true });
// TODO: public read receipts & privacy scoping
// TODO: send read state event to all channel members
// TODO: advance-only notification cursor
router.post(
"/",
messageUpload.any(),
(req, res, next) => {
if (req.body.payload_json) {
req.body = JSON.parse(req.body.payload_json);
}
next();
},
route({
requestBody: "PostDataSchema",
permission: "VIEW_CHANNEL",
responses: {
200: {},
403: {},
},
}),
async (req: Request, res: Response) => {
const body = (req.body as PostDataSchema).thread_ids;
const threads = await Channel.find({
where: {
id: In(body),
},
});
const [messages, members] = await Promise.all([
Message.find({
where: {
id: In(threads.map(({ id }) => id)),
},
}),
Member.find({
where: {
id: In(threads.map(({ owner_id }) => owner_id)),
},
}),
]);
const objRet: { threads: Record<string, { first_message: null | Message; owner: null | Member }> } = { threads: {} };
for (const thread of threads) {
const owner = members.find(({ id }) => id === thread.owner_id)?.toJSON() || null;
const first_message = messages.find(({ channel_id }) => channel_id === thread.id)?.toJSON() || null;
objRet.threads[thread.id] = {
owner,
first_message,
};
}
return res.json(objRet);
},
);
export default router;
+100 -1
View File
@@ -17,11 +17,13 @@
*/
import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api";
import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent } from "@spacebar/util";
import { Channel, emitEvent, User, uploadFile, Attachment, Member, ReadState, MessageCreateEvent, FieldErrors, getPermission, ThreadMember, Message } from "@spacebar/util";
import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment } from "@spacebar/schemas";
import { Request, Response, Router } from "express";
import { messageUpload } from "./messages";
import { HTTPError } from "#util/util/lambert-server";
import { FindManyOptions, FindOptionsOrder, In, Like } from "typeorm";
const router = Router({ mergeParams: true });
@@ -168,4 +170,101 @@ router.post(
},
);
router.get(
"/search",
route({
responses: {
200: {
body: "GuildMessagesSearchResponse",
},
403: {
body: "APIErrorResponse",
},
422: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { name, slop, tag, tag_setting, archived, sort_by, sort_order, limit, offset, max_id, min_id } = req.query as Record<string, string>;
const { channel_id } = req.params as Record<string, string>;
const parsedLimit = Number(limit) || 25;
if (parsedLimit < 1 || parsedLimit > 25) throw new HTTPError("limit must be between 1 and 25", 422);
let order: FindOptionsOrder<Channel>;
switch (sort_by) {
case undefined:
case "creation_time":
order = {
created_at: sort_order === "asc" ? "ASC" : "DESC",
};
break;
case "last_message_time":
order = {
last_message_id: sort_order === "asc" ? "ASC" : "DESC",
};
break;
default:
throw FieldErrors({
sort_by: {
message: "Value must be one of ('last_message_time', 'archive_time', 'relevance', 'creation_time').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
}
const channel = await Channel.findOneOrFail({
where: {
id: channel_id,
},
});
const permissions = await getPermission(req.user_id, channel.guild_id, channel);
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json({ threads: [], total_results: 0, members: [], has_more: false, first_messages: [] });
const member = await Member.findOneOrFail({ where: { guild_id: channel.guild_id, id: req.user_id } });
const query: FindManyOptions<Channel> = {
order,
where: {
parent_id: channel_id,
...(name ? { name: Like(`%${name}%`) } : {}),
...(archived
? {
thread_metadata: {
archived: archived === "true" ? true : false,
},
}
: {}),
},
relations: {},
};
const threads: Channel[] = await Channel.find({ ...query, take: parsedLimit || 0, skip: offset ? Number(offset) : 0 });
const total_results = await Channel.count(query);
const members = ThreadMember.find({
where: {
member_idx: member.index,
id: In(threads.map(({ id }) => id)),
},
});
const messages = Message.find({
where: {
id: In(threads.map(({ id }) => id)),
},
});
const left = total_results - threads.length - +offset;
return res.json({
threads: threads.map((_) => _.toJSON()),
members: (await members).map((_) => _.toJSON()),
messages: (await messages).map((_) => _.toJSON()),
total_results,
has_more: left > 0,
});
},
);
export default router;
+19 -13
View File
@@ -419,8 +419,8 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const threadMembers = await ThreadMember.find({
where: { member_idx: In(member_idx) },
relations: { channel: { thread_members: { member: true } } },
});
const threadMemberMap = new Map(threadMembers.map((member) => [member.id, member] as const));
const threadMemberTime = taskSw.getElapsedAndReset();
// Populated with guilds 'unavailable' currently
@@ -428,6 +428,15 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const pending_guilds: Guild[] = [];
const allThreads = (
await Channel.find({
where: {
type: In([ChannelType.GUILD_NEWS_THREAD, ChannelType.GUILD_PUBLIC_THREAD]),
guild_id: In(members.map(({ guild }) => guild.id)),
},
})
).filter(({ thread_metadata }) => thread_metadata?.archived === false);
// Generate guilds list ( make them unavailable if user is bot )
const guilds: GuildOrUnavailable[] = members.map((member) => {
member.guild.channels = member.guild.channels
@@ -456,23 +465,20 @@ export async function onIdentify(this: WebSocket, data: Payload) {
pending_guilds.push(member.guild);
return { id: member.guild.id, unavailable: true };
}
const threads = threadMembers
.filter(({ channel }) => channel.guild_id === member.guild.id)
.map((_) => {
return {
member: {
..._.toJSON(),
channel: undefined,
},
..._.channel.toJSON(),
};
});
const threads: Channel[] = allThreads.filter((_) => _.guild_id === member.guild_id);
return {
...member.guild.toJSON(),
joined_at: member.joined_at,
threads,
threads: threads.map((thread) => {
const member = threadMemberMap.get(thread.id)?.toJSON();
return {
...thread.toJSON(),
member,
};
}),
};
});
const generateGuildsListTime = taskSw.getElapsedAndReset();
@@ -0,0 +1,21 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface PostDataSchema {
thread_ids: string[];
}
+4 -3
View File
@@ -1,17 +1,17 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2025 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -94,3 +94,4 @@ export * from "./WidgetModifySchema";
export * from "./MessageThreadCreationSchema";
export * from "./ThreadCreationSchema";
export * from "./MessageActivity";
export * from "./PostDataSchema";