oapi: users progress

This commit is contained in:
Puyodead1 2023-03-25 16:09:04 -04:00
parent c97ce59a0a
commit 1ce7879ee8
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
27 changed files with 44374 additions and 7224 deletions

View File

@ -3865,71 +3865,6 @@
"width" "width"
] ]
}, },
"UserPublic": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"premium_since": {
"type": "string",
"format": "date-time"
},
"username": {
"type": "string"
},
"discriminator": {
"type": "string"
},
"public_flags": {
"type": "integer"
},
"avatar": {
"type": "string"
},
"accent_color": {
"type": "integer"
},
"banner": {
"type": "string"
},
"bio": {
"type": "string"
},
"bot": {
"type": "boolean"
},
"premium_type": {
"type": "integer"
},
"theme_colors": {
"type": "array",
"items": [
{
"type": "integer"
},
{
"type": "integer"
}
],
"minItems": 2,
"maxItems": 2
},
"pronouns": {
"type": "string"
}
},
"required": [
"bio",
"bot",
"discriminator",
"id",
"premium_since",
"premium_type",
"public_flags",
"username"
]
},
"PublicConnectedAccount": { "PublicConnectedAccount": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3948,6 +3883,78 @@
"type" "type"
] ]
}, },
"DmChannelDTO": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"nullable": true
},
"id": {
"type": "string"
},
"last_message_id": {
"type": "string",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
},
"origin_channel_id": {
"type": "string",
"nullable": true
},
"owner_id": {
"type": "string"
},
"recipients": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MinimalPublicUserDTO"
}
},
"type": {
"type": "integer"
}
},
"required": [
"icon",
"id",
"last_message_id",
"name",
"origin_channel_id",
"recipients",
"type"
]
},
"MinimalPublicUserDTO": {
"type": "object",
"properties": {
"avatar": {
"type": "string",
"nullable": true
},
"discriminator": {
"type": "string"
},
"id": {
"type": "string"
},
"public_flags": {
"type": "integer"
},
"username": {
"type": "string"
}
},
"required": [
"discriminator",
"id",
"public_flags",
"username"
]
},
"ChannelPermissionOverwriteSchema": { "ChannelPermissionOverwriteSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5386,6 +5393,17 @@
} }
} }
}, },
"UserNoteUpdateSchema": {
"type": "object",
"properties": {
"note": {
"type": "string"
}
},
"required": [
"note"
]
},
"UserProfileModifySchema": { "UserProfileModifySchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6581,11 +6599,30 @@
"token" "token"
] ]
}, },
"UserNoteResponse": {
"type": "object",
"properties": {
"note": {
"type": "string"
},
"note_user_id": {
"type": "string"
},
"user_id": {
"type": "string"
}
},
"required": [
"note",
"note_user_id",
"user_id"
]
},
"UserProfileResponse": { "UserProfileResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"user": { "user": {
"$ref": "#/components/schemas/UserPublic" "$ref": "#/components/schemas/PublicUser"
}, },
"connected_accounts": { "connected_accounts": {
"$ref": "#/components/schemas/PublicConnectedAccount" "$ref": "#/components/schemas/PublicConnectedAccount"
@ -6604,6 +6641,29 @@
"user" "user"
] ]
}, },
"UserRelationshipsResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"$ref": "#/components/schemas/RelationshipType"
},
"nickname": {
"type": "null"
},
"user": {
"$ref": "#/components/schemas/PublicUser"
}
},
"required": [
"id",
"nickname",
"type",
"user"
]
},
"UserRelationsResponse": { "UserRelationsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6633,6 +6693,255 @@
"object" "object"
] ]
}, },
"PublicUserResponse": {
"$ref": "#/components/schemas/PublicUser"
},
"PrivateUserResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"premium_since": {
"type": "string",
"format": "date-time"
},
"verified": {
"type": "boolean"
},
"username": {
"type": "string"
},
"discriminator": {
"type": "string"
},
"public_flags": {
"type": "integer"
},
"avatar": {
"type": "string"
},
"accent_color": {
"type": "integer"
},
"banner": {
"type": "string"
},
"bio": {
"type": "string"
},
"bot": {
"type": "boolean"
},
"premium_type": {
"type": "integer"
},
"theme_colors": {
"type": "array",
"items": [
{
"type": "integer"
},
{
"type": "integer"
}
],
"minItems": 2,
"maxItems": 2
},
"pronouns": {
"type": "string"
},
"flags": {
"type": "string"
},
"mfa_enabled": {
"type": "boolean"
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
},
"nsfw_allowed": {
"type": "boolean"
},
"premium": {
"type": "boolean"
},
"purchased_flags": {
"type": "integer"
},
"premium_usage_flags": {
"type": "integer"
},
"disabled": {
"type": "boolean"
}
},
"required": [
"bio",
"bot",
"disabled",
"discriminator",
"flags",
"id",
"mfa_enabled",
"nsfw_allowed",
"premium",
"premium_since",
"premium_type",
"premium_usage_flags",
"public_flags",
"purchased_flags",
"username",
"verified"
]
},
"UserUpdateResponse": {
"type": "object",
"properties": {
"newToken": {
"type": "string"
},
"id": {
"type": "string"
},
"premium_since": {
"type": "string",
"format": "date-time"
},
"verified": {
"type": "boolean"
},
"username": {
"type": "string"
},
"discriminator": {
"type": "string"
},
"public_flags": {
"type": "integer"
},
"avatar": {
"type": "string"
},
"accent_color": {
"type": "integer"
},
"banner": {
"type": "string"
},
"bio": {
"type": "string"
},
"bot": {
"type": "boolean"
},
"premium_type": {
"type": "integer"
},
"theme_colors": {
"type": "array",
"items": [
{
"type": "integer"
},
{
"type": "integer"
}
],
"minItems": 2,
"maxItems": 2
},
"pronouns": {
"type": "string"
},
"flags": {
"type": "string"
},
"mfa_enabled": {
"type": "boolean"
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
},
"nsfw_allowed": {
"type": "boolean"
},
"premium": {
"type": "boolean"
},
"purchased_flags": {
"type": "integer"
},
"premium_usage_flags": {
"type": "integer"
},
"disabled": {
"type": "boolean"
}
},
"required": [
"bio",
"bot",
"disabled",
"discriminator",
"flags",
"id",
"mfa_enabled",
"nsfw_allowed",
"premium",
"premium_since",
"premium_type",
"premium_usage_flags",
"public_flags",
"purchased_flags",
"username",
"verified"
]
},
"UserGuildsResponse": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Guild"
}
},
"UserChannelsResponse": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DmChannelDTO"
}
},
"UserBackupCodesResponse": {
"type": "object",
"properties": {
"expired": {},
"user": {
"$ref": "#/components/schemas/User"
},
"code": {
"type": "string"
},
"consumed": {
"type": "boolean"
},
"id": {
"type": "string"
}
},
"required": [
"code",
"consumed",
"expired",
"id",
"user"
]
},
"WebhookCreateResponse": { "WebhookCreateResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6841,8 +7150,35 @@
} }
}, },
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSettings"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -6868,8 +7204,28 @@
} }
}, },
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -6885,8 +7241,28 @@
} }
], ],
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -6912,9 +7288,29 @@
"bearer": [] "bearer": []
} }
], ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserNoteUpdateSchema"
}
}
}
},
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -7049,6 +7445,8 @@
"bearer": [] "bearer": []
} }
], ],
"description": "This route is replaced with users/@me/mfa/codes-verification in newer clients",
"deprecated": true,
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@ -7060,8 +7458,35 @@
} }
}, },
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserBackupCodesResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7087,8 +7512,35 @@
} }
}, },
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserBackupCodesResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7131,8 +7583,35 @@
} }
}, },
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdateResponse"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7148,8 +7627,15 @@
} }
], ],
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserGuildsResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7165,8 +7651,28 @@
} }
], ],
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -7282,8 +7788,28 @@
} }
], ],
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7316,8 +7842,28 @@
} }
], ],
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"401": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7434,8 +7980,15 @@
} }
}, },
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DmChannelDTO"
}
}
}
} }
}, },
"tags": [ "tags": [
@ -7590,6 +8143,16 @@
} }
} }
} }
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -7654,8 +8217,15 @@
} }
], ],
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicUserResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -7683,8 +8253,28 @@
} }
], ],
"responses": { "responses": {
"default": { "204": {
"description": "No description available" "description": "No description available"
},
"403": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -11252,7 +11842,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/UserPublic" "$ref": "#/components/schemas/PublicUser"
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -145,6 +145,7 @@ function apiRoutes() {
if (route.description) obj.description = route.description; if (route.description) obj.description = route.description;
if (route.summary) obj.summary = route.summary; if (route.summary) obj.summary = route.summary;
if (route.deprecated) obj.deprecated = route.deprecated;
if (route.requestBody) { if (route.requestBody) {
obj.requestBody = { obj.requestBody = {

View File

@ -144,7 +144,7 @@ router.get(
permission: "VIEW_CHANNEL", permission: "VIEW_CHANNEL",
responses: { responses: {
200: { 200: {
body: "UserPublic", body: "PublicUser",
}, },
400: { 400: {
body: "APIErrorResponse", body: "APIErrorResponse",

View File

@ -30,7 +30,18 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ right: "MANAGE_USERS" }), route({
right: "MANAGE_USERS",
responses: {
204: {},
403: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
await User.findOneOrFail({ await User.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },

View File

@ -16,16 +16,26 @@
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 { User } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const { id } = req.params; "/",
route({
responses: {
200: {
body: "PublicUserResponse",
},
},
}),
async (req: Request, res: Response) => {
const { id } = req.params;
res.json(await User.getPublicUser(id)); res.json(await User.getPublicUser(id));
}); },
);
export default router; export default router;

View File

@ -24,7 +24,14 @@ const router: Router = Router();
router.get( router.get(
"/", "/",
route({ responses: { 200: { body: "UserRelationsResponse" } } }), route({
responses: {
200: { body: "UserRelationsResponse" },
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const mutual_relations: object[] = []; const mutual_relations: object[] = [];
const requested_relations = await User.findOneOrFail({ const requested_relations = await User.findOneOrFail({

View File

@ -27,21 +27,40 @@ import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const recipients = await Recipient.find({ "/",
where: { user_id: req.user_id, closed: false }, route({
relations: ["channel", "channel.recipients"], responses: {
}); 200: {
res.json( body: "UserChannelsResponse",
await Promise.all( },
recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])), },
), }),
); async (req: Request, res: Response) => {
}); const recipients = await Recipient.find({
where: { user_id: req.user_id, closed: false },
relations: ["channel", "channel.recipients"],
});
res.json(
await Promise.all(
recipients.map((r) =>
DmChannelDTO.from(r.channel, [req.user_id]),
),
),
);
},
);
router.post( router.post(
"/", "/",
route({ requestBody: "DmChannelCreateSchema" }), route({
requestBody: "DmChannelCreateSchema",
responses: {
200: {
body: "DmChannelDTO",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as DmChannelCreateSchema; const body = req.body as DmChannelCreateSchema;
res.json( res.json(

View File

@ -16,41 +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 { Router, Request, Response } from "express";
import { Member, User } from "@spacebar/util";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Member, User } from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
const router = Router(); const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
select: ["data"], responses: {
}); //User object 204: {},
let correctpass = true; 401: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["data"],
}); //User object
let correctpass = true;
if (user.data.hash) { if (user.data.hash) {
// guest accounts can delete accounts without password // guest accounts can delete accounts without password
correctpass = await bcrypt.compare(req.body.password, user.data.hash); correctpass = await bcrypt.compare(
if (!correctpass) { req.body.password,
throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); user.data.hash,
);
if (!correctpass) {
throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
}
} }
}
// TODO: decrement guild member count // TODO: decrement guild member count
if (correctpass) { if (correctpass) {
await Promise.all([ await Promise.all([
User.delete({ id: req.user_id }), User.delete({ id: req.user_id }),
Member.delete({ id: req.user_id }), Member.delete({ id: req.user_id }),
]); ]);
res.sendStatus(204); res.sendStatus(204);
} else { } else {
res.sendStatus(401); res.sendStatus(401);
} }
}); },
);
export default router; export default router;

View File

@ -16,35 +16,52 @@
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 { User } from "@spacebar/util";
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User } from "@spacebar/util";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => { router.post(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
select: ["data"], responses: {
}); //User object 204: {},
let correctpass = true; 400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
select: ["data"],
}); //User object
let correctpass = true;
if (user.data.hash) { if (user.data.hash) {
// guest accounts can delete accounts without password // guest accounts can delete accounts without password
correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ correctpass = await bcrypt.compare(
} req.body.password,
user.data.hash,
); //Not sure if user typed right password :/
}
if (correctpass) { if (correctpass) {
await User.update({ id: req.user_id }, { disabled: true }); await User.update({ id: req.user_id }, { disabled: true });
res.sendStatus(204); res.sendStatus(204);
} else { } else {
res.status(400).json({ res.status(400).json({
message: "Password does not match", message: "Password does not match",
code: 50018, code: 50018,
}); });
} }
}); },
);
export default router; export default router;

View File

@ -16,79 +16,106 @@
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 { import {
Config,
Guild, Guild,
Member,
User,
GuildDeleteEvent, GuildDeleteEvent,
GuildMemberRemoveEvent, GuildMemberRemoveEvent,
Member,
User,
emitEvent, emitEvent,
Config,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@spacebar/api";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const members = await Member.find({ "/",
relations: ["guild"], route({
where: { id: req.user_id }, responses: {
}); 200: {
body: "UserGuildsResponse",
},
},
}),
async (req: Request, res: Response) => {
const members = await Member.find({
relations: ["guild"],
where: { id: req.user_id },
});
let guild = members.map((x) => x.guild); let guild = members.map((x) => x.guild);
if ("with_counts" in req.query && req.query.with_counts == "true") { if ("with_counts" in req.query && req.query.with_counts == "true") {
guild = []; // TODO: Load guilds with user role permissions number guild = []; // TODO: Load guilds with user role permissions number
} }
res.json(guild); res.json(guild);
}); },
);
// user send to leave a certain guild // user send to leave a certain guild
router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { router.delete(
const { autoJoin } = Config.get().guild; "/:guild_id",
const { guild_id } = req.params; route({
const guild = await Guild.findOneOrFail({ responses: {
where: { id: guild_id }, 204: {},
select: ["owner_id"], 400: {
}); body: "APIErrorResponse",
},
if (!guild) throw new HTTPError("Guild doesn't exist", 404); 404: {
if (guild.owner_id === req.user_id) body: "APIErrorResponse",
throw new HTTPError("You can't leave your own guild", 400);
if (
autoJoin.enabled &&
autoJoin.guilds.includes(guild_id) &&
!autoJoin.canLeave
) {
throw new HTTPError("You can't leave instance auto join guilds", 400);
}
await Promise.all([
Member.delete({ id: req.user_id, guild_id: guild_id }),
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
}, },
user_id: req.user_id,
} as GuildDeleteEvent),
]);
const user = await User.getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guild_id,
user: user,
}, },
guild_id: guild_id, }),
} as GuildMemberRemoveEvent); async (req: Request, res: Response) => {
const { autoJoin } = Config.get().guild;
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: ["owner_id"],
});
return res.sendStatus(204); if (!guild) throw new HTTPError("Guild doesn't exist", 404);
}); if (guild.owner_id === req.user_id)
throw new HTTPError("You can't leave your own guild", 400);
if (
autoJoin.enabled &&
autoJoin.guilds.includes(guild_id) &&
!autoJoin.canLeave
) {
throw new HTTPError(
"You can't leave instance auto join guilds",
400,
);
}
await Promise.all([
Member.delete({ id: req.user_id, guild_id: guild_id }),
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
},
user_id: req.user_id,
} as GuildDeleteEvent),
]);
const user = await User.getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guild_id,
user: user,
},
guild_id: guild_id,
} as GuildMemberRemoveEvent);
return res.sendStatus(204);
},
);
export default router; export default router;

View File

@ -34,18 +34,41 @@ import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
res.json( "/",
await User.findOne({ route({
select: PrivateUserProjection, responses: {
where: { id: req.user_id }, 200: {
}), body: "PrivateUserResponse",
); },
}); },
}),
async (req: Request, res: Response) => {
res.json(
await User.findOne({
select: PrivateUserProjection,
where: { id: req.user_id },
}),
);
},
);
router.patch( router.patch(
"/", "/",
route({ requestBody: "UserModifySchema" }), route({
requestBody: "UserModifySchema",
responses: {
200: {
body: "UserUpdateResponse",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as UserModifySchema; const body = req.body as UserModifySchema;

View File

@ -30,7 +30,20 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ requestBody: "CodesVerificationSchema" }), route({
requestBody: "CodesVerificationSchema",
responses: {
200: {
body: "UserBackupCodesResponse",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// const { key, nonce, regenerate } = req.body as CodesVerificationSchema; // const { key, nonce, regenerate } = req.body as CodesVerificationSchema;
const { regenerate } = req.body as CodesVerificationSchema; const { regenerate } = req.body as CodesVerificationSchema;

View File

@ -33,7 +33,23 @@ const router = Router();
router.post( router.post(
"/", "/",
route({ requestBody: "MfaCodesSchema" }), route({
requestBody: "MfaCodesSchema",
deprecated: true,
description:
"This route is replaced with users/@me/mfa/codes-verification in newer clients",
responses: {
200: {
body: "UserBackupCodesResponse",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { password, regenerate } = req.body as MfaCodesSchema; const { password, regenerate } = req.body as MfaCodesSchema;

View File

@ -16,71 +16,99 @@
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 { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { User, Note, emitEvent, Snowflake } from "@spacebar/util"; import { Note, Snowflake, User, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router(); const router: Router = Router();
router.get("/:id", route({}), async (req: Request, res: Response) => { router.get(
const { id } = req.params; "/:id",
route({
const note = await Note.findOneOrFail({ responses: {
where: { 200: {
owner: { id: req.user_id }, body: "UserNoteResponse",
target: { id: id }, },
404: {
body: "APIErrorResponse",
},
}, },
}); }),
async (req: Request, res: Response) => {
const { id } = req.params;
return res.json({ const note = await Note.findOneOrFail({
note: note?.content, where: {
note_user_id: id, owner: { id: req.user_id },
user_id: req.user_id, target: { id: id },
}); },
}); });
router.put("/:id", route({}), async (req: Request, res: Response) => { return res.json({
const { id } = req.params; note: note?.content,
const owner = await User.findOneOrFail({ where: { id: req.user_id } }); note_user_id: id,
const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw user_id: req.user_id,
const { note } = req.body; });
},
);
if (note && note.length) { router.put(
// upsert a note "/:id",
if ( route({
await Note.findOne({ requestBody: "UserNoteUpdateSchema",
where: { owner: { id: owner.id }, target: { id: target.id } }, responses: {
}) 204: {},
) { 404: {
Note.update( body: "APIErrorResponse",
{ owner: { id: owner.id }, target: { id: target.id } }, },
{ owner, target, content: note }, },
); }),
async (req: Request, res: Response) => {
const { id } = req.params;
const owner = await User.findOneOrFail({ where: { id: req.user_id } });
const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw
const { note } = req.body;
if (note && note.length) {
// upsert a note
if (
await Note.findOne({
where: {
owner: { id: owner.id },
target: { id: target.id },
},
})
) {
Note.update(
{ owner: { id: owner.id }, target: { id: target.id } },
{ owner, target, content: note },
);
} else {
Note.insert({
id: Snowflake.generate(),
owner,
target,
content: note,
});
}
} else { } else {
Note.insert({ await Note.delete({
id: Snowflake.generate(), owner: { id: owner.id },
owner, target: { id: target.id },
target,
content: note,
}); });
} }
} else {
await Note.delete({ await emitEvent({
owner: { id: owner.id }, event: "USER_NOTE_UPDATE",
target: { id: target.id }, data: {
note: note,
id: target.id,
},
user_id: owner.id,
}); });
}
await emitEvent({ return res.status(204);
event: "USER_NOTE_UPDATE", },
data: { );
note: note,
id: target.id,
},
user_id: owner.id,
});
return res.status(204);
});
export default router; export default router;

View File

@ -38,29 +38,53 @@ const userProjection: (keyof User)[] = [
...PublicUserProjection, ...PublicUserProjection,
]; ];
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
relations: ["relationships", "relationships.to"], responses: {
select: ["id", "relationships"], 200: {
}); body: "UserRelationshipsResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
relations: ["relationships", "relationships.to"],
select: ["id", "relationships"],
});
//TODO DTO //TODO DTO
const related_users = user.relationships.map((r) => { const related_users = user.relationships.map((r) => {
return { return {
id: r.to.id, id: r.to.id,
type: r.type, type: r.type,
nickname: null, nickname: null,
user: r.to.toPublicUser(), user: r.to.toPublicUser(),
}; };
}); });
return res.json(related_users); return res.json(related_users);
}); },
);
router.put( router.put(
"/:id", "/:id",
route({ requestBody: "RelationshipPutSchema" }), route({
requestBody: "RelationshipPutSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
return await updateRelationship( return await updateRelationship(
req, req,
@ -77,7 +101,18 @@ router.put(
router.post( router.post(
"/", "/",
route({ requestBody: "RelationshipPostSchema" }), route({
requestBody: "RelationshipPostSchema",
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
return await updateRelationship( return await updateRelationship(
req, req,
@ -98,64 +133,78 @@ router.post(
}, },
); );
router.delete("/:id", route({}), async (req: Request, res: Response) => { router.delete(
const { id } = req.params; "/:id",
if (id === req.user_id) route({
throw new HTTPError("You can't remove yourself as a friend"); responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { id } = req.params;
if (id === req.user_id)
throw new HTTPError("You can't remove yourself as a friend");
const user = await User.findOneOrFail({ const user = await User.findOneOrFail({
where: { id: req.user_id }, where: { id: req.user_id },
select: userProjection, select: userProjection,
relations: ["relationships"], relations: ["relationships"],
}); });
const friend = await User.findOneOrFail({ const friend = await User.findOneOrFail({
where: { id: id }, where: { id: id },
select: userProjection, select: userProjection,
relations: ["relationships"], relations: ["relationships"],
}); });
const relationship = user.relationships.find((x) => x.to_id === id); const relationship = user.relationships.find((x) => x.to_id === id);
const friendRequest = friend.relationships.find( const friendRequest = friend.relationships.find(
(x) => x.to_id === req.user_id, (x) => x.to_id === req.user_id,
); );
if (!relationship) if (!relationship)
throw new HTTPError("You are not friends with the user", 404); throw new HTTPError("You are not friends with the user", 404);
if (relationship?.type === RelationshipType.blocked) { if (relationship?.type === RelationshipType.blocked) {
// unblock user // unblock user
await Promise.all([
Relationship.delete({ id: relationship.id }),
emitEvent({
event: "RELATIONSHIP_REMOVE",
user_id: req.user_id,
data: relationship.toPublicRelationship(),
} as RelationshipRemoveEvent),
]);
return res.sendStatus(204);
}
if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
await Promise.all([
Relationship.delete({ id: friendRequest.id }),
await emitEvent({
event: "RELATIONSHIP_REMOVE",
data: friendRequest.toPublicRelationship(),
user_id: id,
} as RelationshipRemoveEvent),
]);
}
await Promise.all([ await Promise.all([
Relationship.delete({ id: relationship.id }), Relationship.delete({ id: relationship.id }),
emitEvent({ emitEvent({
event: "RELATIONSHIP_REMOVE", event: "RELATIONSHIP_REMOVE",
user_id: req.user_id,
data: relationship.toPublicRelationship(), data: relationship.toPublicRelationship(),
user_id: req.user_id,
} as RelationshipRemoveEvent), } as RelationshipRemoveEvent),
]); ]);
return res.sendStatus(204); return res.sendStatus(204);
} },
if (friendRequest && friendRequest.type !== RelationshipType.blocked) { );
await Promise.all([
Relationship.delete({ id: friendRequest.id }),
await emitEvent({
event: "RELATIONSHIP_REMOVE",
data: friendRequest.toPublicRelationship(),
user_id: id,
} as RelationshipRemoveEvent),
]);
}
await Promise.all([
Relationship.delete({ id: relationship.id }),
emitEvent({
event: "RELATIONSHIP_REMOVE",
data: relationship.toPublicRelationship(),
user_id: req.user_id,
} as RelationshipRemoveEvent),
]);
return res.sendStatus(204);
});
export default router; export default router;

View File

@ -22,17 +22,43 @@ import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => { router.get(
const user = await User.findOneOrFail({ "/",
where: { id: req.user_id }, route({
relations: ["settings"], responses: {
}); 200: {
return res.json(user.settings); body: "UserSettings",
}); },
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const user = await User.findOneOrFail({
where: { id: req.user_id },
relations: ["settings"],
});
return res.json(user.settings);
},
);
router.patch( router.patch(
"/", "/",
route({ requestBody: "UserSettingsSchema" }), route({
requestBody: "UserSettingsSchema",
responses: {
200: {
body: "UserSettings",
},
400: {
body: "APIErrorResponse",
},
404: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const body = req.body as UserSettingsSchema; const body = req.body as UserSettingsSchema;
if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale

View File

@ -70,6 +70,7 @@ export interface RouteOptions {
values?: string[]; values?: string[];
}; };
}; };
deprecated?: boolean;
// test?: { // test?: {
// response?: RouteResponse; // response?: RouteResponse;
// body?: unknown; // body?: unknown;

View File

@ -86,8 +86,7 @@ export const PrivateUserProjection = [
// Private user data that should never get sent to the client // Private user data that should never get sent to the client
export type PublicUser = Pick<User, PublicUserKeys>; export type PublicUser = Pick<User, PublicUserKeys>;
export type PrivateUser = Pick<User, PrivateUserKeys>;
export type UserPublic = Pick<User, PublicUserKeys>;
export interface UserPrivate extends Pick<User, PrivateUserKeys> { export interface UserPrivate extends Pick<User, PrivateUserKeys> {
locale: string; locale: string;

View File

@ -0,0 +1,3 @@
export interface UserNoteUpdateSchema {
note: string;
}

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 { PublicConnectedAccount, UserPublic } from ".."; import { PublicConnectedAccount, PublicUserResponse } from "..";
export interface UserProfileResponse { export interface UserProfileResponse {
user: UserPublic; user: PublicUserResponse;
connected_accounts: PublicConnectedAccount; connected_accounts: PublicConnectedAccount;
premium_guild_since?: Date; premium_guild_since?: Date;
premium_since?: Date; premium_since?: Date;

View File

@ -69,6 +69,7 @@ export * from "./TotpSchema";
export * from "./UserDeleteSchema"; export * from "./UserDeleteSchema";
export * from "./UserGuildSettingsSchema"; export * from "./UserGuildSettingsSchema";
export * from "./UserModifySchema"; export * from "./UserModifySchema";
export * from "./UserNoteUpdateSchema";
export * from "./UserProfileModifySchema"; export * from "./UserProfileModifySchema";
export * from "./UserSettingsSchema"; export * from "./UserSettingsSchema";
export * from "./Validator"; export * from "./Validator";

View File

@ -0,0 +1,5 @@
export interface UserNoteResponse {
note: string;
note_user_id: string;
user_id: string;
}

View File

@ -1,7 +1,7 @@
import { PublicConnectedAccount, UserPublic } from "../../entities"; import { PublicConnectedAccount, PublicUser } from "../../entities";
export interface UserProfileResponse { export interface UserProfileResponse {
user: UserPublic; user: PublicUser;
connected_accounts: PublicConnectedAccount; connected_accounts: PublicConnectedAccount;
premium_guild_since?: Date; premium_guild_since?: Date;
premium_since?: Date; premium_since?: Date;

View File

@ -0,0 +1,8 @@
import { PublicUser, RelationshipType } from "../../entities";
export interface UserRelationshipsResponse {
id: string;
type: RelationshipType;
nickname: null;
user: PublicUser;
}

View File

@ -0,0 +1,22 @@
import { DmChannelDTO } from "../../dtos";
import { Guild, PrivateUser, PublicUser, User } from "../../entities";
export type PublicUserResponse = PublicUser;
export type PrivateUserResponse = PrivateUser;
export interface UserUpdateResponse extends PrivateUserResponse {
newToken?: string;
}
export type UserGuildsResponse = Guild[];
export type UserChannelsResponse = DmChannelDTO[];
export interface UserBackupCodesResponse {
expired: unknown;
user: User;
code: string;
consumed: boolean;
id: string;
}
[];

View File

@ -39,6 +39,9 @@ export * from "./OAuthAuthorizeResponse";
export * from "./StickerPacksResponse"; export * from "./StickerPacksResponse";
export * from "./Tenor"; export * from "./Tenor";
export * from "./TokenResponse"; export * from "./TokenResponse";
export * from "./UserNoteResponse";
export * from "./UserProfileResponse"; export * from "./UserProfileResponse";
export * from "./UserRelationshipsResponse";
export * from "./UserRelationsResponse"; export * from "./UserRelationsResponse";
export * from "./UserResponse";
export * from "./WebhookCreateResponse"; export * from "./WebhookCreateResponse";