/*
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 { route } from "@spacebar/api";
import {
Channel,
DiscordApiErrors,
Guild,
GuildUpdateEvent,
Member,
Permissions,
SpacebarApiErrors,
emitEvent,
getPermission,
getRights,
handleFile,
Config,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { GuildCreateResponse, GuildUpdateSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
router.get(
"/",
route({
responses: {
"200": {
body: "APIGuildWithJoinedAt",
},
401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params as { [key: string]: string };
const [guild, member] = await Promise.all([Guild.findOneOrFail({ where: { id: guild_id } }), Member.findOne({ where: { guild_id: guild_id, id: req.user_id } })]);
if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
return res.send({
...guild,
joined_at: member?.joined_at,
});
},
);
router.patch(
"/",
route({
requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildCreateResponse",
},
401: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params as { [key: string]: string };
const rights = await getRights(req.user_id);
const permission = await getPermission(req.user_id, guild_id);
if (!rights.has("MANAGE_GUILDS") && !permission.has("MANAGE_GUILD")) throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILDS");
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
relations: { emojis: true, roles: true, stickers: true },
});
// trying to `select` this fails
guild.channel_ordering = (
await Guild.findOneOrFail({
where: { id: guild_id },
select: { channel_ordering: true },
})
).channel_ordering;
// TODO: guild update check image
if (body.icon && body.icon != guild.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
if (body.banner && body.banner !== guild.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
if (body.splash && body.splash !== guild.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash);
if (body.discovery_splash && body.discovery_splash !== guild.discovery_splash)
body.discovery_splash = await handleFile(`/discovery-splashes/${guild_id}`, body.discovery_splash);
if (body.features) {
const diff = guild.features.filter((x) => !body.features?.includes(x)).concat(body.features.filter((x) => !guild.features.includes(x)));
// TODO move these
const MUTABLE_FEATURES = ["COMMUNITY", "INVITES_DISABLED", "DISCOVERABLE"];
for (const feature of diff) {
if (MUTABLE_FEATURES.includes(feature)) continue;
throw SpacebarApiErrors.FEATURE_IS_IMMUTABLE.withParams(feature);
}
// for some reason, they don't update in the assign.
guild.features = body.features;
}
// TODO: check if body ids are valid
guild.assign(body);
if (body.public_updates_channel_id == "1") {
// create an updates channel for them
const channel = await Channel.createChannel(
{
name: "moderator-only",
guild_id: guild.id,
position: 0,
type: 0,
permission_overwrites: [
// remove SEND_MESSAGES from @everyone
{
id: guild.id,
allow: "0",
deny: Permissions.FLAGS.VIEW_CHANNEL.toString(),
type: 0,
},
],
},
undefined,
{ skipPermissionCheck: true },
);
await Guild.insertChannelInOrder(guild.id, channel.id, 0, guild);
guild.public_updates_channel_id = channel.id;
} else if (body.public_updates_channel_id != undefined) {
// ensure channel exists in this guild
await Channel.findOneOrFail({
where: { guild_id, id: body.public_updates_channel_id },
select: { id: true },
});
}
if (body.rules_channel_id == "1") {
// create a rules for them
const channel = await Channel.createChannel(
{
name: "rules",
guild_id: guild.id,
position: 0,
type: 0,
permission_overwrites: [
// remove SEND_MESSAGES from @everyone
{
id: guild.id,
allow: "0",
deny: Permissions.FLAGS.SEND_MESSAGES.toString(),
type: 0,
},
],
},
undefined,
{ skipPermissionCheck: true },
);
await Guild.insertChannelInOrder(guild.id, channel.id, 0, guild);
guild.rules_channel_id = channel.id;
} else if (body.rules_channel_id != undefined) {
// ensure channel exists in this guild
await Channel.findOneOrFail({
where: { guild_id, id: body.rules_channel_id },
select: { id: true },
});
}
const data = guild.toJSON();
// TODO: guild hashes
// TODO: fix vanity_url_code, template_id
// delete data.vanity_url_code;
delete data.template_id;
await Promise.all([
guild.save(),
emitEvent({
event: "GUILD_UPDATE",
data: {
...data,
// TODO: did i do this right?
afk_channel_id: data.afk_channel_id ?? undefined,
public_updates_channel_id: data.public_updates_channel_id ?? undefined,
rules_channel_id: data.rules_channel_id ?? undefined,
system_channel_id: data.system_channel_id ?? undefined,
} satisfies GuildCreateResponse, // apparently we dont have a separate schema for this
guild_id,
} satisfies GuildUpdateEvent),
]);
return res.json(data);
},
);
export default router;