Newer
Older
percord / src / api / routes / users / @me / mentions.ts
/*
	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/>.
*/

import { route } from "@spacebar/api";
import { Snowflake, User, Message, Member, Channel, Permissions, timePromise, NewUrlUserSignatureData, Stopwatch, Attachment } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { In, LessThan, FindOptionsWhere } from "typeorm";

const router: Router = Router({ mergeParams: true });

router.get(
    "",
    route({
        responses: {
            200: {
                body: "MessageListResponse",
            },
            404: {
                body: "APIErrorResponse",
            },
        },
    }),
    // AFAICT this endpoint doesn't list DMs
    async (req: Request, res: Response) => {
        const limit = req.query.limit && !isNaN(Number(req.query.limit)) ? Number(req.query.limit) : 25;
        const everyone = req.query.everyone !== undefined ? Boolean(req.query.everyone) : true;
        const roles = req.query.roles !== undefined ? Boolean(req.query.roles) : true;
        const before = req.query.before !== undefined ? String(req.query.before as string) : undefined;
        const guild_id = req.query.guild_id !== undefined ? req.query.guild_id : undefined;

        const user = await User.findOneOrFail({
            where: { id: req.user_id },
        });

        const memberships = await Member.find({
            where: { id: req.user_id, ...(guild_id === undefined ? {} : { guild_id: String(guild_id) }) },
            select: {
                guild_id: true,
                id: true,
                communication_disabled_until: true,
                roles: {
                    // We don't want to include all guild roles, as this could cause a lot more explosive behavior
                    id: true,
                    position: true,
                    permissions: true,
                    mentionable: true, // cause we can skip querying for unmentionable roles
                },
                guild: {
                    id: true,
                    owner_id: true,
                },
            },
            relations: ["guild", "roles"],
        });

        const channels = await Channel.find({
            where: {
                guild_id: In(memberships.map((m) => m.guild_id)),
            },
            select: { id: true, guild_id: true, permission_overwrites: true },
        });

        const visibleChannels = channels.filter((c) => {
            const member = memberships.find((m) => m.guild_id === c.guild_id)!;
            return Permissions.finalPermission({
                user: { id: member.id, roles: member.roles.map((r) => r.id), communication_disabled_until: member.communication_disabled_until, flags: 0 },
                guild: { id: member.guild.id, owner_id: member.guild.owner_id!, roles: member.roles },
                channel: c,
            }).has("VIEW_CHANNEL");
        });

        const visibleChannelIds = visibleChannels.map((c) => c.id);
        const ownedMentionableRoleIds = memberships.reduce((acc, m) => {
            acc.push(...m.roles.filter((r) => r.mentionable).map((r) => r.id));
            return acc;
        }, [] as Snowflake[]);

        const whereQuery: FindOptionsWhere<Message>[] = [
            {
                channel_id: In(visibleChannelIds),
                mentions: { id: user.id },
                id: before ? LessThan(before) : undefined,
            },
        ];
        if (everyone) {
            whereQuery.push({
                channel_id: In(visibleChannelIds),
                mention_everyone: true,
                id: before ? LessThan(before) : undefined,
            });
        }
        if (roles) {
            whereQuery.push({
                channel_id: In(visibleChannelIds),
                mention_roles: { id: In(ownedMentionableRoleIds) },
                id: before ? LessThan(before) : undefined,
            });
        }

        const sw = Stopwatch.startNew();
        const finalMessages = (
            await Message.find({
                where: whereQuery,
                order: { timestamp: "DESC" },
                relations: [
                    "author",
                    "webhook",
                    "application",
                    "mentions",
                    "mention_roles",
                    "mention_channels",
                    "sticker_items",
                    "attachments",
                    "referenced_message",
                    "referenced_message.author",
                    "referenced_message.webhook",
                    "referenced_message.application",
                    "referenced_message.mentions",
                    "referenced_message.mention_roles",
                    "referenced_message.mention_channels",
                    "referenced_message.sticker_items",
                    "referenced_message.attachments",
                ],
                take: limit,
            })
        ).map((m) => {
            return {
                ...m.toJSON(),
                attachments: m.attachments?.map((attachment: Attachment) =>
                    Attachment.prototype.signUrls.call(
                        attachment,
                        new NewUrlUserSignatureData({
                            ip: req.ip,
                            userAgent: req.headers["user-agent"] as string,
                        }),
                    ),
                ),
            };
        });

        console.log(`[Inbox/mentions] User ${user.id} fetched full message data for ${finalMessages.length} messages in ${sw.elapsed().totalMilliseconds}ms`);

        return res.json(finalMessages);
    },
);

export default router;