This commit is contained in:
Puyodead1 2023-03-24 21:21:21 -04:00
parent 10e1eb95ae
commit c2ce88dee7
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
52 changed files with 1564 additions and 629 deletions

Binary file not shown.

Binary file not shown.

View File

@ -37,7 +37,17 @@ const router: Router = Router();
router.get(
"/",
route({ permission: "BAN_MEMBERS" }),
route({
permission: "BAN_MEMBERS",
responses: {
200: {
body: "GuildBansResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
@ -73,7 +83,20 @@ router.get(
router.get(
"/:user",
route({ permission: "BAN_MEMBERS" }),
route({
permission: "BAN_MEMBERS",
responses: {
200: {
body: "BanModeratorSchema",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const user_id = req.params.ban;
@ -97,7 +120,21 @@ router.get(
router.put(
"/:user_id",
route({ requestBody: "BanCreateSchema", permission: "BAN_MEMBERS" }),
route({
requestBody: "BanCreateSchema",
permission: "BAN_MEMBERS",
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const banned_user_id = req.params.user_id;
@ -143,7 +180,20 @@ router.put(
router.put(
"/@me",
route({ requestBody: "BanCreateSchema" }),
route({
requestBody: "BanCreateSchema",
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
@ -182,7 +232,18 @@ router.put(
router.delete(
"/:user_id",
route({ permission: "BAN_MEMBERS" }),
route({
permission: "BAN_MEMBERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params;

View File

@ -28,18 +28,39 @@ import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const channels = await Channel.find({ where: { guild_id } });
router.get(
"/",
route({
responses: {
201: {
body: "GuildChannelsResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const channels = await Channel.find({ where: { guild_id } });
res.json(channels);
});
res.json(channels);
},
);
router.post(
"/",
route({
requestBody: "ChannelModifySchema",
permission: "MANAGE_CHANNELS",
responses: {
201: {
body: "Channel",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
@ -60,6 +81,15 @@ router.patch(
route({
requestBody: "ChannelReorderSchema",
permission: "MANAGE_CHANNELS",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// changes guild channel position

View File

@ -16,37 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { emitEvent, GuildDeleteEvent, Guild } from "@spacebar/util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router = Router();
// discord prefixes this route with /delete instead of using the delete method
// docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
router.post("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: ["owner_id"],
});
if (guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([
Guild.delete({ id: guild_id }), // this will also delete all guild related data
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
router.post(
"/",
route({
responses: {
204: {},
401: {
body: "APIErrorResponse",
},
guild_id: guild_id,
} as GuildDeleteEvent),
]);
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
return res.sendStatus(204);
});
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: ["owner_id"],
});
if (guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([
Guild.delete({ id: guild_id }), // this will also delete all guild related data
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
},
guild_id: guild_id,
} as GuildDeleteEvent),
]);
return res.sendStatus(204);
},
);
export default router;

View File

@ -16,40 +16,50 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
// TODO:
// Load from database
// Admin control, but for now it allows anyone to be discoverable
res.send({
guild_id: guild_id,
safe_environment: true,
healthy: true,
health_score_pending: false,
size: true,
nsfw_properties: {},
protected: true,
sufficient: true,
sufficient_without_grace_period: true,
valid_rules_channel: true,
retention_healthy: true,
engagement_healthy: true,
age: true,
minimum_age: 0,
health_score: {
avg_nonnew_participators: 0,
avg_nonnew_communicators: 0,
num_intentful_joiners: 0,
perc_ret_w1_intentful: 0,
router.get(
"/",
route({
responses: {
200: {
body: "GuildDiscoveryRequirements",
},
},
minimum_size: 0,
});
});
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
// TODO:
// Load from database
// Admin control, but for now it allows anyone to be discoverable
res.send({
guild_id: guild_id,
safe_environment: true,
healthy: true,
health_score_pending: false,
size: true,
nsfw_properties: {},
protected: true,
sufficient: true,
sufficient_without_grace_period: true,
valid_rules_channel: true,
retention_healthy: true,
engagement_healthy: true,
age: true,
minimum_age: 0,
health_score: {
avg_nonnew_participators: 0,
avg_nonnew_communicators: 0,
num_intentful_joiners: 0,
perc_ret_w1_intentful: 0,
},
minimum_size: 0,
});
},
);
export default router;

View File

@ -34,37 +34,77 @@ import { Request, Response, Router } from "express";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
200: {
body: "GuildEmojisResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
await Member.IsInGuildOrFail(req.user_id, guild_id);
const emojis = await Emoji.find({
where: { guild_id: guild_id },
relations: ["user"],
});
const emojis = await Emoji.find({
where: { guild_id: guild_id },
relations: ["user"],
});
return res.json(emojis);
});
return res.json(emojis);
},
);
router.get("/:emoji_id", route({}), async (req: Request, res: Response) => {
const { guild_id, emoji_id } = req.params;
router.get(
"/:emoji_id",
route({
responses: {
200: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, emoji_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
await Member.IsInGuildOrFail(req.user_id, guild_id);
const emoji = await Emoji.findOneOrFail({
where: { guild_id: guild_id, id: emoji_id },
relations: ["user"],
});
const emoji = await Emoji.findOneOrFail({
where: { guild_id: guild_id, id: emoji_id },
relations: ["user"],
});
return res.json(emoji);
});
return res.json(emoji);
},
);
router.post(
"/",
route({
requestBody: "EmojiCreateSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
201: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
@ -115,6 +155,14 @@ router.patch(
route({
requestBody: "EmojiModifySchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
200: {
body: "Emoji",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params;
@ -141,7 +189,15 @@ router.patch(
router.delete(
"/:emoji_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { emoji_id, guild_id } = req.params;

View File

@ -34,28 +34,61 @@ import { HTTPError } from "lambert-server";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
"200": {
body: "GuildResponse",
},
401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
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,
);
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,
});
});
return res.send({
...guild,
joined_at: member?.joined_at,
});
},
);
router.patch(
"/",
route({ requestBody: "GuildUpdateSchema", permission: "MANAGE_GUILD" }),
route({
requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD",
responses: {
"200": {
body: "GuildUpdateSchema",
},
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;

View File

@ -16,15 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Invite, PublicInviteRelation } from "@spacebar/util";
import { route } from "@spacebar/api";
import { Invite, PublicInviteRelation } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
router.get(
"/",
route({ permission: "MANAGE_GUILD" }),
route({
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildInvitesResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;

View File

@ -16,17 +16,27 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
// TODO: member verification
router.get(
"/",
route({
responses: {
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: member verification
res.status(404).json({
message: "Unknown Guild Member Verification Form",
code: 10068,
});
});
res.status(404).json({
message: "Unknown Guild Member Verification Form",
code: 10068,
});
},
);
export default router;

View File

@ -34,20 +34,52 @@ import { Request, Response, Router } from "express";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
router.get(
"/",
route({
responses: {
200: {
body: "Member",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
const member = await Member.findOneOrFail({
where: { id: member_id, guild_id },
});
const member = await Member.findOneOrFail({
where: { id: member_id, guild_id },
});
return res.json(member);
});
return res.json(member);
},
);
router.patch(
"/",
route({ requestBody: "MemberChangeSchema" }),
route({
requestBody: "MemberChangeSchema",
responses: {
200: {
body: "Member",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const member_id =
@ -119,54 +151,81 @@ router.patch(
},
);
router.put("/", route({}), async (req: Request, res: Response) => {
// TODO: Lurker mode
router.put(
"/",
route({
responses: {
200: {
body: "MemberJoinGuildResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
// TODO: Lurker mode
const rights = await getRights(req.user_id);
const rights = await getRights(req.user_id);
const { guild_id } = req.params;
let { member_id } = req.params;
if (member_id === "@me") {
member_id = req.user_id;
rights.hasThrow("JOIN_GUILDS");
} else {
// TODO: join others by controller
}
const { guild_id } = req.params;
let { member_id } = req.params;
if (member_id === "@me") {
member_id = req.user_id;
rights.hasThrow("JOIN_GUILDS");
} else {
// TODO: join others by controller
}
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
});
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
});
const emoji = await Emoji.find({
where: { guild_id: guild_id },
});
const emoji = await Emoji.find({
where: { guild_id: guild_id },
});
const roles = await Role.find({
where: { guild_id: guild_id },
});
const roles = await Role.find({
where: { guild_id: guild_id },
});
const stickers = await Sticker.find({
where: { guild_id: guild_id },
});
const stickers = await Sticker.find({
where: { guild_id: guild_id },
});
await Member.addToGuild(member_id, guild_id);
res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
});
await Member.addToGuild(member_id, guild_id);
res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
},
);
router.delete("/", route({}), async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
const permission = await getPermission(req.user_id, guild_id);
const rights = await getRights(req.user_id);
if (member_id === "@me" || member_id === req.user_id) {
// TODO: unless force-joined
rights.hasThrow("SELF_LEAVE_GROUPS");
} else {
rights.hasThrow("KICK_BAN_MEMBERS");
permission.hasThrow("KICK_MEMBERS");
}
router.delete(
"/",
route({
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
const permission = await getPermission(req.user_id, guild_id);
const rights = await getRights(req.user_id);
if (member_id === "@me" || member_id === req.user_id) {
// TODO: unless force-joined
rights.hasThrow("SELF_LEAVE_GROUPS");
} else {
rights.hasThrow("KICK_BAN_MEMBERS");
permission.hasThrow("KICK_MEMBERS");
}
await Member.removeFromGuild(member_id, guild_id);
res.sendStatus(204);
});
await Member.removeFromGuild(member_id, guild_id);
res.sendStatus(204);
},
);
export default router;

View File

@ -24,7 +24,18 @@ const router = Router();
router.patch(
"/",
route({ requestBody: "MemberNickChangeSchema" }),
route({
requestBody: "MemberNickChangeSchema",
responses: {
200: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";

View File

@ -16,15 +16,23 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Member } from "@spacebar/util";
import { route } from "@spacebar/api";
import { Member } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
router.delete(
"/",
route({ permission: "MANAGE_ROLES" }),
route({
permission: "MANAGE_ROLES",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;
@ -35,7 +43,13 @@ router.delete(
router.put(
"/",
route({ permission: "MANAGE_ROLES" }),
route({
permission: "MANAGE_ROLES",
responses: {
204: {},
403: {},
},
}),
async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;

View File

@ -16,35 +16,58 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request, Response, Router } from "express";
import { Member, PublicMemberProjection } from "@spacebar/util";
import { route } from "@spacebar/api";
import { MoreThan } from "typeorm";
import { Member, PublicMemberProjection } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { MoreThan } from "typeorm";
const router = Router();
// TODO: send over websocket
// TODO: check for GUILD_MEMBERS intent
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {};
router.get(
"/",
route({
query: {
limit: {
type: "number",
description:
"max number of members to return (1-1000). default 1",
},
after: {
type: "string",
},
},
responses: {
200: {
body: "GuildMembersResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {};
await Member.IsInGuildOrFail(req.user_id, guild_id);
await Member.IsInGuildOrFail(req.user_id, guild_id);
const members = await Member.find({
where: { guild_id, ...query },
select: PublicMemberProjection,
take: limit,
order: { id: "ASC" },
});
const members = await Member.find({
where: { guild_id, ...query },
select: PublicMemberProjection,
take: limit,
order: { id: "ASC" },
});
return res.json(members);
});
return res.json(members);
},
);
export default router;

View File

@ -18,140 +18,159 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Request, Response, Router } from "express";
import { route } from "@spacebar/api";
import { getPermission, FieldErrors, Message, Channel } from "@spacebar/util";
import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { FindManyOptions, In, Like } from "typeorm";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const {
channel_id,
content,
// include_nsfw, // TODO
offset,
sort_order,
// sort_by, // TODO: Handle 'relevance'
limit,
author_id,
} = req.query;
const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
if (sort_order) {
if (
typeof sort_order != "string" ||
["desc", "asc"].indexOf(sort_order) == -1
)
throw FieldErrors({
sort_order: {
message: "Value must be one of ('desc', 'asc').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
}
const permissions = await getPermission(
req.user_id,
req.params.guild_id,
channel_id as string | undefined,
);
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
const query: FindManyOptions<Message> = {
order: {
timestamp: sort_order
? (sort_order.toUpperCase() as "ASC" | "DESC")
: "DESC",
},
take: parsedLimit || 0,
where: {
guild: {
id: req.params.guild_id,
router.get(
"/",
route({
responses: {
200: {
body: "GuildMessagesSearchResponse",
},
403: {
body: "APIErrorResponse",
},
422: {
body: "APIErrorResponse",
},
},
relations: [
"author",
"webhook",
"application",
"mentions",
"mention_roles",
"mention_channels",
"sticker_items",
"attachments",
],
skip: offset ? Number(offset) : 0,
};
//@ts-ignore
if (channel_id) query.where.channel = { id: channel_id };
else {
// get all channel IDs that this user can access
const channels = await Channel.find({
where: { guild_id: req.params.guild_id },
select: ["id"],
});
const ids = [];
}),
async (req: Request, res: Response) => {
const {
channel_id,
content,
// include_nsfw, // TODO
offset,
sort_order,
// sort_by, // TODO: Handle 'relevance'
limit,
author_id,
} = req.query;
for (const channel of channels) {
const perm = await getPermission(
req.user_id,
req.params.guild_id,
channel.id,
);
if (!perm.has("VIEW_CHANNEL") || !perm.has("READ_MESSAGE_HISTORY"))
continue;
ids.push(channel.id);
const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
if (sort_order) {
if (
typeof sort_order != "string" ||
["desc", "asc"].indexOf(sort_order) == -1
)
throw FieldErrors({
sort_order: {
message: "Value must be one of ('desc', 'asc').",
code: "BASE_TYPE_CHOICES",
},
}); // todo this is wrong
}
//@ts-ignore
query.where.channel = { id: In(ids) };
}
//@ts-ignore
if (author_id) query.where.author = { id: author_id };
//@ts-ignore
if (content) query.where.content = Like(`%${content}%`);
const permissions = await getPermission(
req.user_id,
req.params.guild_id,
channel_id as string | undefined,
);
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
const messages: Message[] = await Message.find(query);
const messagesDto = messages.map((x) => [
{
id: x.id,
type: x.type,
content: x.content,
channel_id: x.channel_id,
author: {
id: x.author?.id,
username: x.author?.username,
avatar: x.author?.avatar,
avatar_decoration: null,
discriminator: x.author?.discriminator,
public_flags: x.author?.public_flags,
const query: FindManyOptions<Message> = {
order: {
timestamp: sort_order
? (sort_order.toUpperCase() as "ASC" | "DESC")
: "DESC",
},
attachments: x.attachments,
embeds: x.embeds,
mentions: x.mentions,
mention_roles: x.mention_roles,
pinned: x.pinned,
mention_everyone: x.mention_everyone,
tts: x.tts,
timestamp: x.timestamp,
edited_timestamp: x.edited_timestamp,
flags: x.flags,
components: x.components,
hit: true,
},
]);
take: parsedLimit || 0,
where: {
guild: {
id: req.params.guild_id,
},
},
relations: [
"author",
"webhook",
"application",
"mentions",
"mention_roles",
"mention_channels",
"sticker_items",
"attachments",
],
skip: offset ? Number(offset) : 0,
};
//@ts-ignore
if (channel_id) query.where.channel = { id: channel_id };
else {
// get all channel IDs that this user can access
const channels = await Channel.find({
where: { guild_id: req.params.guild_id },
select: ["id"],
});
const ids = [];
return res.json({
messages: messagesDto,
total_results: messages.length,
});
});
for (const channel of channels) {
const perm = await getPermission(
req.user_id,
req.params.guild_id,
channel.id,
);
if (
!perm.has("VIEW_CHANNEL") ||
!perm.has("READ_MESSAGE_HISTORY")
)
continue;
ids.push(channel.id);
}
//@ts-ignore
query.where.channel = { id: In(ids) };
}
//@ts-ignore
if (author_id) query.where.author = { id: author_id };
//@ts-ignore
if (content) query.where.content = Like(`%${content}%`);
const messages: Message[] = await Message.find(query);
const messagesDto = messages.map((x) => [
{
id: x.id,
type: x.type,
content: x.content,
channel_id: x.channel_id,
author: {
id: x.author?.id,
username: x.author?.username,
avatar: x.author?.avatar,
avatar_decoration: null,
discriminator: x.author?.discriminator,
public_flags: x.author?.public_flags,
},
attachments: x.attachments,
embeds: x.embeds,
mentions: x.mentions,
mention_roles: x.mention_roles,
pinned: x.pinned,
mention_everyone: x.mention_everyone,
tts: x.tts,
timestamp: x.timestamp,
edited_timestamp: x.edited_timestamp,
flags: x.flags,
components: x.components,
hit: true,
},
]);
return res.json({
messages: messagesDto,
total_results: messages.length,
});
},
);
export default router;

View File

@ -31,7 +31,20 @@ const router = Router();
router.patch(
"/:member_id",
route({ requestBody: "MemberChangeProfileSchema" }),
route({
requestBody: "MemberChangeProfileSchema",
responses: {
200: {
body: "Member",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
// const member_id =

View File

@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Router, Request, Response } from "express";
import { Guild, Member, Snowflake } from "@spacebar/util";
import { LessThan, IsNull } from "typeorm";
import { route } from "@spacebar/api";
import { Guild, Member, Snowflake } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { IsNull, LessThan } from "typeorm";
const router = Router();
//Returns all inactive members, respecting role hierarchy
@ -80,25 +80,46 @@ export const inactiveMembers = async (
return members;
};
router.get("/", route({}), async (req: Request, res: Response) => {
const days = parseInt(req.query.days as string);
router.get(
"/",
route({
responses: {
"200": {
body: "GuildPruneResponse",
},
},
}),
async (req: Request, res: Response) => {
const days = parseInt(req.query.days as string);
let roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; //express will return array otherwise
let roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; //express will return array otherwise
const members = await inactiveMembers(
req.params.guild_id,
req.user_id,
days,
roles as string[],
);
const members = await inactiveMembers(
req.params.guild_id,
req.user_id,
days,
roles as string[],
);
res.send({ pruned: members.length });
});
res.send({ pruned: members.length });
},
);
router.post(
"/",
route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }),
route({
permission: "KICK_MEMBERS",
right: "KICK_BAN_MEMBERS",
responses: {
200: {
body: "GuildPurgeResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const days = parseInt(req.body.days);

View File

@ -16,22 +16,35 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { getVoiceRegions, route, getIpAdress } from "@spacebar/api";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings
return res.json(
await getVoiceRegions(
getIpAdress(req),
guild.features.includes("VIP_REGIONS"),
),
);
});
router.get(
"/",
route({
responses: {
200: {
body: "GuildVoiceRegionsResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings
return res.json(
await getVoiceRegions(
getIpAdress(req),
guild.features.includes("VIP_REGIONS"),
),
);
},
);
export default router;

View File

@ -31,16 +31,48 @@ import { HTTPError } from "lambert-server";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } });
return res.json(role);
});
router.get(
"/",
route({
responses: {
200: {
body: "Role",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
const role = await Role.findOneOrFail({
where: { guild_id, id: role_id },
});
return res.json(role);
},
);
router.delete(
"/",
route({ permission: "MANAGE_ROLES" }),
route({
permission: "MANAGE_ROLES",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params;
if (role_id === guild_id)
@ -69,7 +101,24 @@ router.delete(
router.patch(
"/",
route({ requestBody: "RoleModifySchema", permission: "MANAGE_ROLES" }),
route({
requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "Role",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema;

View File

@ -21,7 +21,6 @@ import {
Config,
DiscordApiErrors,
emitEvent,
getPermission,
GuildRoleCreateEvent,
GuildRoleUpdateEvent,
Member,
@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
router.post(
"/",
route({ requestBody: "RoleModifySchema", permission: "MANAGE_ROLES" }),
route({
requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "Role",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema;
@ -104,14 +117,25 @@ router.post(
router.patch(
"/",
route({ requestBody: "RolePositionUpdateSchema" }),
route({
requestBody: "RolePositionUpdateSchema",
permission: "MANAGE_ROLES",
responses: {
200: {
body: "GuildRolesResponse",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const body = req.body as RolePositionUpdateSchema;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
await Promise.all(
body.map(async (x) =>
Role.update({ guild_id, id: x.id }, { position: x.position }),

View File

@ -33,12 +33,25 @@ import { HTTPError } from "lambert-server";
import multer from "multer";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
router.get(
"/",
route({
responses: {
200: {
body: "GuildStickersResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(await Sticker.find({ where: { guild_id } }));
});
res.json(await Sticker.find({ where: { guild_id } }));
},
);
const bodyParser = multer({
limits: {
@ -55,6 +68,17 @@ router.post(
route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
requestBody: "ModifyGuildStickerSchema",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
if (!req.file) throw new HTTPError("missing file");
@ -98,20 +122,46 @@ export function getStickerFormat(mime_type: string) {
}
}
router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
router.get(
"/:sticker_id",
route({
responses: {
200: {
body: "Sticker",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params;
await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(
await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }),
);
});
res.json(
await Sticker.findOneOrFail({
where: { guild_id, id: sticker_id },
}),
);
},
);
router.patch(
"/:sticker_id",
route({
requestBody: "ModifyGuildStickerSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params;
@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) {
router.delete(
"/:sticker_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params;

View File

@ -40,19 +40,46 @@ const TemplateGuildProjection: (keyof Guild)[] = [
"icon",
];
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
200: {
body: "GuildTemplatesResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const templates = await Template.find({
where: { source_guild_id: guild_id },
});
const templates = await Template.find({
where: { source_guild_id: guild_id },
});
return res.json(templates);
});
return res.json(templates);
},
);
router.post(
"/",
route({ requestBody: "TemplateCreateSchema", permission: "MANAGE_GUILD" }),
route({
requestBody: "TemplateCreateSchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "Template",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({
@ -80,7 +107,13 @@ router.post(
router.delete(
"/:code",
route({ permission: "MANAGE_GUILD" }),
route({
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params;
@ -95,7 +128,13 @@ router.delete(
router.put(
"/:code",
route({ permission: "MANAGE_GUILD" }),
route({
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params;
const guild = await Guild.findOneOrFail({
@ -114,7 +153,14 @@ router.put(
router.patch(
"/:code",
route({ requestBody: "TemplateModifySchema", permission: "MANAGE_GUILD" }),
route({
requestBody: "TemplateModifySchema",
permission: "MANAGE_GUILD",
responses: {
200: { body: "Template" },
403: { body: "APIErrorResponse" },
},
}),
async (req: Request, res: Response) => {
const { code, guild_id } = req.params;
const { name, description } = req.body;

View File

@ -33,7 +33,20 @@ const InviteRegex = /\W/g;
router.get(
"/",
route({ permission: "MANAGE_GUILD" }),
route({
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildVanityUrlResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@ -60,7 +73,21 @@ router.get(
router.patch(
"/",
route({ requestBody: "VanityUrlSchema", permission: "MANAGE_GUILD" }),
route({
requestBody: "VanityUrlSchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "GuildVanityUrlCreateResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const body = req.body as VanityUrlSchema;

View File

@ -34,7 +34,21 @@ const router = Router();
router.patch(
"/",
route({ requestBody: "VoiceStateUpdateSchema" }),
route({
requestBody: "VoiceStateUpdateSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const body = req.body as VoiceStateUpdateSchema;
const { guild_id } = req.params;

View File

@ -23,20 +23,42 @@ import { HTTPError } from "lambert-server";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
router.get(
"/",
route({
responses: {
200: {
body: "GuildWelcomeScreen",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
await Member.IsInGuildOrFail(req.user_id, guild_id);
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(guild.welcome_screen);
});
res.json(guild.welcome_screen);
},
);
router.patch(
"/",
route({
requestBody: "GuildUpdateWelcomeScreenSchema",
permission: "MANAGE_GUILD",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;

View File

@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request, Response, Router } from "express";
import { Permissions, Guild, Invite, Channel, Member } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { random, route } from "@spacebar/api";
import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router();
@ -32,77 +32,90 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget
// TODO: Cache the response for a guild for 5 minutes regardless of response
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
200: {
body: "GuildWidgetJsonResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
// Fetch existing widget invite for widget channel
let invite = await Invite.findOne({
where: { channel_id: guild.widget_channel_id },
});
// Fetch existing widget invite for widget channel
let invite = await Invite.findOne({
where: { channel_id: guild.widget_channel_id },
});
if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists
// TODO: Refactor invite create code to a shared function
const max_age = 86400; // 24 hours
const expires_at = new Date(max_age * 1000 + Date.now());
if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists
// TODO: Refactor invite create code to a shared function
const max_age = 86400; // 24 hours
const expires_at = new Date(max_age * 1000 + Date.now());
invite = await Invite.create({
code: random(),
temporary: false,
uses: 0,
max_uses: 0,
max_age: max_age,
expires_at,
created_at: new Date(),
guild_id,
channel_id: guild.widget_channel_id,
}).save();
}
// Fetch voice channels, and the @everyone permissions object
const channels: { id: string; name: string; position: number }[] = [];
(
await Channel.find({
where: { guild_id: guild_id, type: 2 },
order: { position: "ASC" },
})
).filter((doc) => {
// Only return channels where @everyone has the CONNECT permission
if (
doc.permission_overwrites === undefined ||
Permissions.channelPermission(
doc.permission_overwrites,
Permissions.FLAGS.CONNECT,
) === Permissions.FLAGS.CONNECT
) {
channels.push({
id: doc.id,
name: doc.name ?? "Unknown channel",
position: doc.position ?? 0,
});
invite = await Invite.create({
code: random(),
temporary: false,
uses: 0,
max_uses: 0,
max_age: max_age,
expires_at,
created_at: new Date(),
guild_id,
channel_id: guild.widget_channel_id,
}).save();
}
});
// Fetch members
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
const members = await Member.find({ where: { guild_id: guild_id } });
// Fetch voice channels, and the @everyone permissions object
const channels: { id: string; name: string; position: number }[] = [];
// Construct object to respond with
const data = {
id: guild_id,
name: guild.name,
instant_invite: invite?.code,
channels: channels,
members: members,
presence_count: guild.presence_count,
};
(
await Channel.find({
where: { guild_id: guild_id, type: 2 },
order: { position: "ASC" },
})
).filter((doc) => {
// Only return channels where @everyone has the CONNECT permission
if (
doc.permission_overwrites === undefined ||
Permissions.channelPermission(
doc.permission_overwrites,
Permissions.FLAGS.CONNECT,
) === Permissions.FLAGS.CONNECT
) {
channels.push({
id: doc.id,
name: doc.name ?? "Unknown channel",
position: doc.position ?? 0,
});
}
});
res.set("Cache-Control", "public, max-age=300");
return res.json(data);
});
// Fetch members
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
const members = await Member.find({ where: { guild_id: guild_id } });
// Construct object to respond with
const data = {
id: guild_id,
name: guild.name,
instant_invite: invite?.code,
channels: channels,
members: members,
presence_count: guild.presence_count,
};
res.set("Cache-Control", "public, max-age=300");
return res.json(data);
},
);
export default router;

View File

@ -18,11 +18,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, Router } from "express";
import { Guild } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fs from "fs";
import { HTTPError } from "lambert-server";
import path from "path";
const router: Router = Router();
@ -31,130 +31,178 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image
// TODO: Cache the response
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
200: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
// Fetch guild information
const icon = guild.icon;
const name = guild.name;
const presence = guild.presence_count + " ONLINE";
// Fetch guild information
const icon = guild.icon;
const name = guild.name;
const presence = guild.presence_count + " ONLINE";
// Fetch parameter
const style = req.query.style?.toString() || "shield";
if (
!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)
) {
throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
}
// Setup canvas
const { createCanvas } = require("canvas");
const { loadImage } = require("canvas");
const sizeOf = require("image-size");
// TODO: Widget style templates need Spacebar branding
const source = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"assets",
"widget",
`${style}.png`,
);
if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400);
}
// Create base template image for parameter
const { width, height } = await sizeOf(source);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const template = await loadImage(source);
ctx.drawImage(template, 0, 0);
// Add the guild specific information to the template asset image
switch (style) {
case "shield":
ctx.textAlign = "center";
await drawText(
ctx,
73,
13,
"#FFFFFF",
"thin 10px Verdana",
presence,
);
break;
case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
await drawText(
ctx,
83,
66,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
await drawText(
ctx,
62,
49,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
await drawText(
ctx,
83,
58,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
await drawText(
ctx,
84,
171,
"#C9D2F0FF",
"thin 12px Verdana",
presence,
);
break;
default:
// Fetch parameter
const style = req.query.style?.toString() || "shield";
if (
!["shield", "banner1", "banner2", "banner3", "banner4"].includes(
style,
)
) {
throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
}
}
// Return final image
const buffer = canvas.toBuffer("image/png");
res.set("Content-Type", "image/png");
res.set("Cache-Control", "public, max-age=3600");
return res.send(buffer);
});
// Setup canvas
const { createCanvas } = require("canvas");
const { loadImage } = require("canvas");
const sizeOf = require("image-size");
// TODO: Widget style templates need Spacebar branding
const source = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"assets",
"widget",
`${style}.png`,
);
if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400);
}
// Create base template image for parameter
const { width, height } = await sizeOf(source);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const template = await loadImage(source);
ctx.drawImage(template, 0, 0);
// Add the guild specific information to the template asset image
switch (style) {
case "shield":
ctx.textAlign = "center";
await drawText(
ctx,
73,
13,
"#FFFFFF",
"thin 10px Verdana",
presence,
);
break;
case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(
ctx,
83,
51,
"#FFFFFF",
"12px Verdana",
name,
22,
);
await drawText(
ctx,
83,
66,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(
ctx,
62,
34,
"#FFFFFF",
"12px Verdana",
name,
15,
);
await drawText(
ctx,
62,
49,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(
ctx,
83,
44,
"#FFFFFF",
"12px Verdana",
name,
27,
);
await drawText(
ctx,
83,
58,
"#C9D2F0FF",
"thin 11px Verdana",
presence,
);
break;
case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(
ctx,
84,
156,
"#FFFFFF",
"13px Verdana",
name,
27,
);
await drawText(
ctx,
84,
171,
"#C9D2F0FF",
"thin 12px Verdana",
presence,
);
break;
default:
throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400,
);
}
// Return final image
const buffer = canvas.toBuffer("image/png");
res.set("Content-Type", "image/png");
res.set("Cache-Control", "public, max-age=3600");
return res.send(buffer);
},
);
async function drawIcon(
canvas: any,

View File

@ -23,21 +23,48 @@ import { Request, Response, Router } from "express";
const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
router.get(
"/",
route({
responses: {
200: {
body: "GuildWidgetSettingsResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
return res.json({
enabled: guild.widget_enabled || false,
channel_id: guild.widget_channel_id || null,
});
});
return res.json({
enabled: guild.widget_enabled || false,
channel_id: guild.widget_channel_id || null,
});
},
);
// https://discord.com/developers/docs/resources/guild#modify-guild-widget
router.patch(
"/",
route({ requestBody: "WidgetModifySchema", permission: "MANAGE_GUILD" }),
route({
requestBody: "WidgetModifySchema",
permission: "MANAGE_GUILD",
responses: {
200: {
body: "WidgetModifySchema",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const body = req.body as WidgetModifySchema;
const { guild_id } = req.params;

View File

@ -33,7 +33,21 @@ const router: Router = Router();
router.post(
"/",
route({ requestBody: "GuildCreateSchema", right: "CREATE_GUILDS" }),
route({
requestBody: "GuildCreateSchema",
right: "CREATE_GUILDS",
responses: {
201: {
body: "GuildCreateResponse",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema;

View File

@ -31,53 +31,72 @@ import { Request, Response, Router } from "express";
import fetch from "node-fetch";
const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => {
const { allowDiscordTemplates, allowRaws, enabled } =
Config.get().templates;
if (!enabled)
res.json({
code: 403,
message: "Template creation & usage is disabled on this instance.",
}).sendStatus(403);
const { code } = req.params;
if (code.startsWith("discord:")) {
if (!allowDiscordTemplates)
return res
.json({
code: 403,
message:
"Discord templates cannot be used on this instance.",
})
.sendStatus(403);
const discordTemplateID = code.split("discord:", 2)[1];
const discordTemplateData = await fetch(
`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
{
method: "get",
headers: { "Content-Type": "application/json" },
router.get(
"/:code",
route({
responses: {
200: {
body: "GuildTemplate",
},
);
return res.json(await discordTemplateData.json());
}
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { allowDiscordTemplates, allowRaws, enabled } =
Config.get().templates;
if (!enabled)
res.json({
code: 403,
message:
"Template creation & usage is disabled on this instance.",
}).sendStatus(403);
if (code.startsWith("external:")) {
if (!allowRaws)
return res
.json({
code: 403,
message: "Importing raws is disabled on this instance.",
})
.sendStatus(403);
const { code } = req.params;
return res.json(code.split("external:", 2)[1]);
}
if (code.startsWith("discord:")) {
if (!allowDiscordTemplates)
return res
.json({
code: 403,
message:
"Discord templates cannot be used on this instance.",
})
.sendStatus(403);
const discordTemplateID = code.split("discord:", 2)[1];
const template = await Template.findOneOrFail({ where: { code: code } });
res.json(template);
});
const discordTemplateData = await fetch(
`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
{
method: "get",
headers: { "Content-Type": "application/json" },
},
);
return res.json(await discordTemplateData.json());
}
if (code.startsWith("external:")) {
if (!allowRaws)
return res
.json({
code: 403,
message: "Importing raws is disabled on this instance.",
})
.sendStatus(403);
return res.json(code.split("external:", 2)[1]);
}
const template = await Template.findOneOrFail({
where: { code: code },
});
res.json(template);
},
);
router.post(
"/:code",

View File

@ -24,7 +24,7 @@ import {
OneToMany,
RelationId,
} from "typeorm";
import { Config, handleFile, Snowflake } from "..";
import { Config, GuildWelcomeScreen, handleFile, Snowflake } from "..";
import { Ban } from "./Ban";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
@ -270,16 +270,7 @@ export class Guild extends BaseClass {
verification_level?: number;
@Column({ type: "simple-json" })
welcome_screen: {
enabled: boolean;
description: string;
welcome_channels: {
description: string;
emoji_id?: string;
emoji_name?: string;
channel_id: string;
}[];
};
welcome_screen: GuildWelcomeScreen;
@Column({ nullable: true })
@RelationId((guild: Guild) => guild.widget_channel)

View File

@ -0,0 +1,10 @@
export interface GuildWelcomeScreen {
enabled: boolean;
description: string;
welcome_channels: {
description: string;
emoji_id?: string;
emoji_name?: string;
channel_id: string;
}[];
}

View File

@ -19,6 +19,7 @@
export * from "./Activity";
export * from "./ConnectedAccount";
export * from "./Event";
export * from "./GuildWelcomeScreen";
export * from "./Interaction";
export * from "./Presence";
export * from "./Status";

View File

@ -0,0 +1,10 @@
export interface GuildBansResponse {
reason: string;
user: {
username: string;
discriminator: string;
id: string;
avatar: string | null;
public_flags: number;
};
}

View File

@ -0,0 +1,3 @@
import { Channel } from "../../entities";
export type GuildChannelsResponse = Channel[];

View File

@ -0,0 +1,3 @@
export interface GuildCreateResponse {
id: string;
}

View File

@ -0,0 +1,23 @@
export interface GuildDiscoveryRequirements {
uild_id: string;
safe_environment: boolean;
healthy: boolean;
health_score_pending: boolean;
size: boolean;
nsfw_properties: unknown;
protected: boolean;
sufficient: boolean;
sufficient_without_grace_period: boolean;
valid_rules_channel: boolean;
retention_healthy: boolean;
engagement_healthy: boolean;
age: boolean;
minimum_age: number;
health_score: {
avg_nonnew_participators: number;
avg_nonnew_communicators: number;
num_intentful_joiners: number;
perc_ret_w1_intentful: number;
};
minimum_size: number;
}

View File

@ -0,0 +1,3 @@
import { Emoji } from "../../entities";
export type GuildEmojisResponse = Emoji[];

View File

@ -0,0 +1,3 @@
import { Invite } from "../../entities";
export type GuildInvitesResponse = Invite[];

View File

@ -0,0 +1,3 @@
import { Member } from "../../entities";
export type GuildMembersResponse = Member[];

View File

@ -0,0 +1,32 @@
import {
Attachment,
Embed,
MessageType,
PublicUser,
Role,
} from "../../entities";
export interface GuildMessagesSearchMessage {
id: string;
type: MessageType;
content?: string;
channel_id: string;
author: PublicUser;
attachments: Attachment[];
embeds: Embed[];
mentions: PublicUser[];
mention_roles: Role[];
pinned: boolean;
mention_everyone?: boolean;
tts: boolean;
timestamp: string;
edited_timestamp: string | null;
flags: number;
components: unknown[];
hit: true;
}
export interface GuildMessagesSearchResponse {
messages: GuildMessagesSearchMessage[];
total_results: number;
}

View File

@ -0,0 +1,7 @@
export interface GuildPruneResponse {
pruned: number;
}
export interface GuildPurgeResponse {
purged: number;
}

View File

@ -0,0 +1,3 @@
import { Guild } from "../../entities";
export type GuildResponse = Guild & { joined_at: string };

View File

@ -0,0 +1,3 @@
import { Role } from "../../entities";
export type GuildRolesResponse = Role[];

View File

@ -0,0 +1,3 @@
import { Sticker } from "../../entities";
export type GuildStickersResponse = Sticker[];

View File

@ -0,0 +1,3 @@
import { Template } from "../../entities";
export type GuildTemplatesResponse = Template[];

View File

@ -0,0 +1,17 @@
export interface GuildVanityUrl {
code: string;
uses: number;
}
export interface GuildVanityUrlNoInvite {
code: null;
}
export type GuildVanityUrlResponse =
| GuildVanityUrl
| GuildVanityUrl[]
| GuildVanityUrlNoInvite;
export interface GuildVanityUrlCreateResponse {
code: string;
}

View File

@ -0,0 +1,9 @@
export interface GuildVoiceRegion {
id: string;
name: string;
custom: boolean;
deprecated: boolean;
optimal: boolean;
}
export type GuildVoiceRegionsResponse = GuildVoiceRegion[];

View File

@ -0,0 +1,21 @@
import { ClientStatus } from "../../interfaces";
export interface GuildWidgetJsonResponse {
id: string;
name: string;
instant_invite: string;
channels: {
id: string;
name: string;
position: number;
}[];
members: {
id: string;
username: string;
discriminator: string;
avatar: string | null;
status: ClientStatus;
avatar_url: string;
}[];
presence_count: number;
}

View File

@ -0,0 +1,6 @@
import { Snowflake } from "../../util";
export interface GuildWidgetSettingsResponse {
enabled: boolean;
channel_id: Snowflake | null;
}

View File

@ -0,0 +1,8 @@
import { Emoji, Guild, Role, Sticker } from "../../entities";
export interface MemberJoinGuildResponse {
guild: Guild;
emojis: Emoji[];
roles: Role[];
stickers: Sticker[];
}

View File

@ -12,7 +12,25 @@ export * from "./ChannelWebhooksResponse";
export * from "./GatewayBotResponse";
export * from "./GatewayResponse";
export * from "./GenerateRegistrationTokensResponse";
export * from "./GuildBansResponse";
export * from "./GuildChannelsResponse";
export * from "./GuildCreateResponse";
export * from "./GuildDiscoveryRequirements";
export * from "./GuildEmojisResponse";
export * from "./GuildInvitesResponse";
export * from "./GuildMembersResponse";
export * from "./GuildMessagesSearchResponse";
export * from "./GuildPruneResponse";
export * from "./GuildResponse";
export * from "./GuildRolesResponse";
export * from "./GuildStickersResponse";
export * from "./GuildTemplatesResponse";
export * from "./GuildVanityUrl";
export * from "./GuildVoiceRegionsResponse";
export * from "./GuildWidgetJsonResponse";
export * from "./GuildWidgetSettingsResponse";
export * from "./LocationMetadataResponse";
export * from "./MemberJoinGuildResponse";
export * from "./Tenor";
export * from "./TokenResponse";
export * from "./UserProfileResponse";