mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-26 05:25:13 +00:00
more forum fixes
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user