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 94174 additions and 4585 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -16,37 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. 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 { 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(); const router = Router();
// discord prefixes this route with /delete instead of using the delete method // discord prefixes this route with /delete instead of using the delete method
// docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const { guild_id } = req.params; "/",
route({
const guild = await Guild.findOneOrFail({ responses: {
where: { id: guild_id }, 204: {},
select: ["owner_id"], 401: {
}); body: "APIErrorResponse",
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, 404: {
} as GuildDeleteEvent), 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; export default router;

View File

@ -16,40 +16,50 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Request, Response } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
// TODO: route({
// Load from database responses: {
// Admin control, but for now it allows anyone to be discoverable 200: {
body: "GuildDiscoveryRequirements",
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, }),
}); 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; export default router;

View File

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

View File

@ -34,28 +34,61 @@ import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
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([ const [guild, member] = await Promise.all([
Guild.findOneOrFail({ where: { id: guild_id } }), Guild.findOneOrFail({ where: { id: guild_id } }),
Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }), Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
]); ]);
if (!member) if (!member)
throw new HTTPError( throw new HTTPError(
"You are not a member of the guild you are trying to access", "You are not a member of the guild you are trying to access",
401, 401,
); );
return res.send({ return res.send({
...guild, ...guild,
joined_at: member?.joined_at, joined_at: member?.joined_at,
}); });
}); },
);
router.patch( 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) => { async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema; const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params; const { guild_id } = req.params;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. 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 { 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(); const router = Router();
//Returns all inactive members, respecting role hierarchy //Returns all inactive members, respecting role hierarchy
@ -80,25 +80,46 @@ export const inactiveMembers = async (
return members; return members;
}; };
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const days = parseInt(req.query.days as string); "/",
route({
responses: {
"200": {
body: "GuildPruneResponse",
},
},
}),
async (req: Request, res: Response) => {
const days = parseInt(req.query.days as string);
let roles = req.query.include_roles; let roles = req.query.include_roles;
if (typeof roles === "string") roles = [roles]; //express will return array otherwise if (typeof roles === "string") roles = [roles]; //express will return array otherwise
const members = await inactiveMembers( const members = await inactiveMembers(
req.params.guild_id, req.params.guild_id,
req.user_id, req.user_id,
days, days,
roles as string[], roles as string[],
); );
res.send({ pruned: members.length }); res.send({ pruned: members.length });
}); },
);
router.post( 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) => { async (req: Request, res: Response) => {
const days = parseInt(req.body.days); const days = parseInt(req.body.days);

View File

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

View File

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

View File

@ -21,7 +21,6 @@ import {
Config, Config,
DiscordApiErrors, DiscordApiErrors,
emitEvent, emitEvent,
getPermission,
GuildRoleCreateEvent, GuildRoleCreateEvent,
GuildRoleUpdateEvent, GuildRoleUpdateEvent,
Member, Member,
@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
router.post( 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) => { async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema; const body = req.body as RoleModifySchema;
@ -104,14 +117,25 @@ router.post(
router.patch( 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) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as RolePositionUpdateSchema; const body = req.body as RolePositionUpdateSchema;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
await Promise.all( await Promise.all(
body.map(async (x) => body.map(async (x) =>
Role.update({ guild_id, id: x.id }, { position: x.position }), 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"; import multer from "multer";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
await Member.IsInGuildOrFail(req.user_id, guild_id); 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({ const bodyParser = multer({
limits: { limits: {
@ -55,6 +68,17 @@ router.post(
route({ route({
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
requestBody: "ModifyGuildStickerSchema", requestBody: "ModifyGuildStickerSchema",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (!req.file) throw new HTTPError("missing file"); 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) => { router.get(
const { guild_id, sticker_id } = req.params; "/:sticker_id",
await Member.IsInGuildOrFail(req.user_id, guild_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( res.json(
await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }), await Sticker.findOneOrFail({
); where: { guild_id, id: sticker_id },
}); }),
);
},
);
router.patch( router.patch(
"/:sticker_id", "/:sticker_id",
route({ route({
requestBody: "ModifyGuildStickerSchema", requestBody: "ModifyGuildStickerSchema",
permission: "MANAGE_EMOJIS_AND_STICKERS", permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
200: {
body: "Sticker",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;
@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) {
router.delete( router.delete(
"/:sticker_id", "/:sticker_id",
route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), route({
permission: "MANAGE_EMOJIS_AND_STICKERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, sticker_id } = req.params; const { guild_id, sticker_id } = req.params;

View File

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

View File

@ -33,7 +33,20 @@ const InviteRegex = /\W/g;
router.get( 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) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@ -60,7 +73,21 @@ router.get(
router.patch( 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) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id } = req.params;
const body = req.body as VanityUrlSchema; const body = req.body as VanityUrlSchema;

View File

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

View File

@ -23,20 +23,42 @@ import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const guild_id = req.params.guild_id; "/",
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 } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
await Member.IsInGuildOrFail(req.user_id, guild_id); await Member.IsInGuildOrFail(req.user_id, guild_id);
res.json(guild.welcome_screen); res.json(guild.welcome_screen);
}); },
);
router.patch( router.patch(
"/", "/",
route({ route({
requestBody: "GuildUpdateWelcomeScreenSchema", requestBody: "GuildUpdateWelcomeScreenSchema",
permission: "MANAGE_GUILD", permission: "MANAGE_GUILD",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const guild_id = req.params.guild_id; 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/>. 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 { 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(); const router: Router = Router();
@ -32,77 +32,90 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget // https://discord.com/developers/docs/resources/guild#get-guild-widget
// TODO: Cache the response for a guild for 5 minutes regardless of response // TODO: Cache the response for a guild for 5 minutes regardless of response
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
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 } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
// Fetch existing widget invite for widget channel // Fetch existing widget invite for widget channel
let invite = await Invite.findOne({ let invite = await Invite.findOne({
where: { channel_id: guild.widget_channel_id }, where: { channel_id: guild.widget_channel_id },
}); });
if (guild.widget_channel_id && !invite) { if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists // Create invite for channel if none exists
// TODO: Refactor invite create code to a shared function // TODO: Refactor invite create code to a shared function
const max_age = 86400; // 24 hours const max_age = 86400; // 24 hours
const expires_at = new Date(max_age * 1000 + Date.now()); const expires_at = new Date(max_age * 1000 + Date.now());
invite = await Invite.create({ invite = await Invite.create({
code: random(), code: random(),
temporary: false, temporary: false,
uses: 0, uses: 0,
max_uses: 0, max_uses: 0,
max_age: max_age, max_age: max_age,
expires_at, expires_at,
created_at: new Date(), created_at: new Date(),
guild_id, guild_id,
channel_id: guild.widget_channel_id, channel_id: guild.widget_channel_id,
}).save(); }).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,
});
} }
});
// Fetch members // Fetch voice channels, and the @everyone permissions object
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) const channels: { id: string; name: string; position: number }[] = [];
const members = await Member.find({ where: { guild_id: guild_id } });
// Construct object to respond with (
const data = { await Channel.find({
id: guild_id, where: { guild_id: guild_id, type: 2 },
name: guild.name, order: { position: "ASC" },
instant_invite: invite?.code, })
channels: channels, ).filter((doc) => {
members: members, // Only return channels where @everyone has the CONNECT permission
presence_count: guild.presence_count, 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"); // Fetch members
return res.json(data); // 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; export default router;

View File

@ -18,11 +18,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 { route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
import fs from "fs"; import fs from "fs";
import { HTTPError } from "lambert-server";
import path from "path"; import path from "path";
const router: Router = Router(); const router: Router = Router();
@ -31,130 +31,178 @@ const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
// TODO: Cache the response // TODO: Cache the response
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
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 } }); const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
// Fetch guild information // Fetch guild information
const icon = guild.icon; const icon = guild.icon;
const name = guild.name; const name = guild.name;
const presence = guild.presence_count + " ONLINE"; const presence = guild.presence_count + " ONLINE";
// Fetch parameter // Fetch parameter
const style = req.query.style?.toString() || "shield"; const style = req.query.style?.toString() || "shield";
if ( if (
!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style) !["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:
throw new HTTPError( throw new HTTPError(
"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", "Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
400, 400,
); );
} }
// Return final image // Setup canvas
const buffer = canvas.toBuffer("image/png"); const { createCanvas } = require("canvas");
res.set("Content-Type", "image/png"); const { loadImage } = require("canvas");
res.set("Cache-Control", "public, max-age=3600"); const sizeOf = require("image-size");
return res.send(buffer);
}); // 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( async function drawIcon(
canvas: any, canvas: any,

View File

@ -23,21 +23,48 @@ import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { guild_id } = req.params; "/",
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({ return res.json({
enabled: guild.widget_enabled || false, enabled: guild.widget_enabled || false,
channel_id: guild.widget_channel_id || null, channel_id: guild.widget_channel_id || null,
}); });
}); },
);
// https://discord.com/developers/docs/resources/guild#modify-guild-widget // https://discord.com/developers/docs/resources/guild#modify-guild-widget
router.patch( 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) => { async (req: Request, res: Response) => {
const body = req.body as WidgetModifySchema; const body = req.body as WidgetModifySchema;
const { guild_id } = req.params; const { guild_id } = req.params;

View File

@ -33,7 +33,21 @@ const router: Router = Router();
router.post( 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) => { async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema; const body = req.body as GuildCreateSchema;

View File

@ -31,53 +31,72 @@ import { Request, Response, Router } from "express";
import fetch from "node-fetch"; import fetch from "node-fetch";
const router: Router = Router(); const router: Router = Router();
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get(
const { allowDiscordTemplates, allowRaws, enabled } = "/:code",
Config.get().templates; route({
if (!enabled) responses: {
res.json({ 200: {
code: 403, body: "GuildTemplate",
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" },
}, },
); 403: {
return res.json(await discordTemplateData.json()); 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:")) { const { code } = req.params;
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]); 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 } }); const discordTemplateData = await fetch(
res.json(template); `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( router.post(
"/:code", "/:code",

View File

@ -24,7 +24,7 @@ import {
OneToMany, OneToMany,
RelationId, RelationId,
} from "typeorm"; } from "typeorm";
import { Config, handleFile, Snowflake } from ".."; import { Config, GuildWelcomeScreen, handleFile, Snowflake } from "..";
import { Ban } from "./Ban"; import { Ban } from "./Ban";
import { BaseClass } from "./BaseClass"; import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel"; import { Channel } from "./Channel";
@ -270,16 +270,7 @@ export class Guild extends BaseClass {
verification_level?: number; verification_level?: number;
@Column({ type: "simple-json" }) @Column({ type: "simple-json" })
welcome_screen: { welcome_screen: GuildWelcomeScreen;
enabled: boolean;
description: string;
welcome_channels: {
description: string;
emoji_id?: string;
emoji_name?: string;
channel_id: string;
}[];
};
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((guild: Guild) => guild.widget_channel) @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 "./Activity";
export * from "./ConnectedAccount"; export * from "./ConnectedAccount";
export * from "./Event"; export * from "./Event";
export * from "./GuildWelcomeScreen";
export * from "./Interaction"; export * from "./Interaction";
export * from "./Presence"; export * from "./Presence";
export * from "./Status"; 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 "./GatewayBotResponse";
export * from "./GatewayResponse"; export * from "./GatewayResponse";
export * from "./GenerateRegistrationTokensResponse"; 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 "./LocationMetadataResponse";
export * from "./MemberJoinGuildResponse";
export * from "./Tenor"; export * from "./Tenor";
export * from "./TokenResponse"; export * from "./TokenResponse";
export * from "./UserProfileResponse"; export * from "./UserProfileResponse";