summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel <34555296+Flam3rboy@users.noreply.github.com>2021-10-15 18:39:57 +0200
committerGitHub <noreply@github.com>2021-10-15 18:39:57 +0200
commit0c811dbee9b960b7b1ef28a85ff79e86672af856 (patch)
treea25db8eb098d5651df8228911bc01716abef1cf6
parentMerge pull request #462 from hbjydev/unit-tests-expanded (diff)
parent:sparkles: sticker events (diff)
downloadserver-0c811dbee9b960b7b1ef28a85ff79e86672af856.tar.xz
Merge pull request #455 from fosscord/sticker
Stickers
-rw-r--r--api/assets/schemas.json308
-rw-r--r--api/client_test/index.html4
-rw-r--r--api/src/Server.ts2
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts1
-rw-r--r--api/src/routes/guilds/#guild_id/emojis.ts5
-rw-r--r--api/src/routes/guilds/#guild_id/premium.ts10
-rw-r--r--api/src/routes/guilds/#guild_id/stickers.ts135
-rw-r--r--api/src/routes/sticker-packs/#id/index.ts19
-rw-r--r--api/src/routes/sticker-packs/index.ts6
-rw-r--r--api/src/routes/stickers/#sticker_id/index.ts12
-rw-r--r--api/src/routes/template.ts.disabled5
-rw-r--r--api/src/util/Message.ts5
-rw-r--r--api/tsconfig.json3
-rw-r--r--gateway/client.js2
-rw-r--r--gateway/src/opcodes/Identify.ts2
-rw-r--r--util/scripts/migrate_db_engine.js (renamed from util/src/migrations/migrate_db_engine.js)0
-rw-r--r--util/src/entities/Message.ts2
-rw-r--r--util/src/entities/Migration.ts18
-rw-r--r--util/src/entities/ReadState.ts5
-rw-r--r--util/src/entities/Sticker.ts32
-rw-r--r--util/src/entities/StickerPack.ts31
-rw-r--r--util/src/entities/index.ts2
-rw-r--r--util/src/interfaces/Event.ts10
-rw-r--r--util/src/migrations/1633881705509-VanityInvite.ts2
-rw-r--r--util/src/migrations/1634308884591-Stickers.ts66
-rw-r--r--util/src/util/Database.ts20
-rw-r--r--util/src/util/cdn.ts4
27 files changed, 664 insertions, 47 deletions
diff --git a/api/assets/schemas.json b/api/assets/schemas.json
index 2ceaa923..eb97112c 100644
--- a/api/assets/schemas.json
+++ b/api/assets/schemas.json
@@ -514,6 +514,12 @@
             "attachments": {
                 "type": "array",
                 "items": {}
+            },
+            "sticker_ids": {
+                "type": "array",
+                "items": {
+                    "type": "string"
+                }
             }
         },
         "definitions": {
@@ -5341,6 +5347,308 @@
         },
         "$schema": "http://json-schema.org/draft-07/schema#"
     },
+    "ModifyGuildStickerSchema": {
+        "type": "object",
+        "properties": {
+            "name": {
+                "minLength": 2,
+                "maxLength": 30,
+                "type": "string"
+            },
+            "description": {
+                "maxLength": 100,
+                "type": "string"
+            },
+            "tags": {
+                "maxLength": 200,
+                "type": "string"
+            }
+        },
+        "required": [
+            "name",
+            "tags"
+        ],
+        "definitions": {
+            "ChannelPermissionOverwriteType": {
+                "enum": [
+                    0,
+                    1
+                ],
+                "type": "number"
+            },
+            "Embed": {
+                "type": "object",
+                "properties": {
+                    "title": {
+                        "type": "string"
+                    },
+                    "type": {
+                        "enum": [
+                            "article",
+                            "gifv",
+                            "image",
+                            "link",
+                            "rich",
+                            "video"
+                        ],
+                        "type": "string"
+                    },
+                    "description": {
+                        "type": "string"
+                    },
+                    "url": {
+                        "type": "string"
+                    },
+                    "timestamp": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "color": {
+                        "type": "integer"
+                    },
+                    "footer": {
+                        "type": "object",
+                        "properties": {
+                            "text": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        },
+                        "required": [
+                            "text"
+                        ]
+                    },
+                    "image": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "thumbnail": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "video": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "provider": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            }
+                        }
+                    },
+                    "author": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        }
+                    },
+                    "fields": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "name": {
+                                    "type": "string"
+                                },
+                                "value": {
+                                    "type": "string"
+                                },
+                                "inline": {
+                                    "type": "boolean"
+                                }
+                            },
+                            "required": [
+                                "name",
+                                "value"
+                            ]
+                        }
+                    }
+                }
+            },
+            "EmbedImage": {
+                "type": "object",
+                "properties": {
+                    "url": {
+                        "type": "string"
+                    },
+                    "proxy_url": {
+                        "type": "string"
+                    },
+                    "height": {
+                        "type": "integer"
+                    },
+                    "width": {
+                        "type": "integer"
+                    }
+                }
+            },
+            "ChannelModifySchema": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "maxLength": 100,
+                        "type": "string"
+                    },
+                    "type": {
+                        "enum": [
+                            0,
+                            1,
+                            10,
+                            11,
+                            12,
+                            13,
+                            2,
+                            3,
+                            4,
+                            5,
+                            6
+                        ],
+                        "type": "number"
+                    },
+                    "topic": {
+                        "type": "string"
+                    },
+                    "icon": {
+                        "type": [
+                            "null",
+                            "string"
+                        ]
+                    },
+                    "bitrate": {
+                        "type": "integer"
+                    },
+                    "user_limit": {
+                        "type": "integer"
+                    },
+                    "rate_limit_per_user": {
+                        "type": "integer"
+                    },
+                    "position": {
+                        "type": "integer"
+                    },
+                    "permission_overwrites": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string"
+                                },
+                                "type": {
+                                    "$ref": "#/definitions/ChannelPermissionOverwriteType"
+                                },
+                                "allow": {
+                                    "type": "string"
+                                },
+                                "deny": {
+                                    "type": "string"
+                                }
+                            },
+                            "required": [
+                                "allow",
+                                "deny",
+                                "id",
+                                "type"
+                            ]
+                        }
+                    },
+                    "parent_id": {
+                        "type": "string"
+                    },
+                    "id": {
+                        "type": "string"
+                    },
+                    "nsfw": {
+                        "type": "boolean"
+                    },
+                    "rtc_region": {
+                        "type": "string"
+                    },
+                    "default_auto_archive_duration": {
+                        "type": "integer"
+                    }
+                }
+            },
+            "UserPublic": {
+                "type": "object",
+                "properties": {
+                    "username": {
+                        "type": "string"
+                    },
+                    "discriminator": {
+                        "type": "string"
+                    },
+                    "id": {
+                        "type": "string"
+                    },
+                    "public_flags": {
+                        "type": "integer"
+                    },
+                    "avatar": {
+                        "type": "string"
+                    },
+                    "accent_color": {
+                        "type": "integer"
+                    },
+                    "banner": {
+                        "type": "string"
+                    },
+                    "bio": {
+                        "type": "string"
+                    },
+                    "bot": {
+                        "type": "boolean"
+                    }
+                },
+                "required": [
+                    "bio",
+                    "bot",
+                    "discriminator",
+                    "id",
+                    "public_flags",
+                    "username"
+                ]
+            },
+            "PublicConnectedAccount": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "type": "string"
+                    },
+                    "type": {
+                        "type": "string"
+                    },
+                    "verifie": {
+                        "type": "boolean"
+                    }
+                },
+                "required": [
+                    "name",
+                    "type",
+                    "verifie"
+                ]
+            }
+        },
+        "$schema": "http://json-schema.org/draft-07/schema#"
+    },
     "TemplateCreateSchema": {
         "type": "object",
         "properties": {
diff --git a/api/client_test/index.html b/api/client_test/index.html
index 20b431b8..5a795253 100644
--- a/api/client_test/index.html
+++ b/api/client_test/index.html
@@ -37,6 +37,7 @@
 				HTML_TIMESTAMP: Date.now(),
 				ALGOLIA_KEY: "aca0d7082e4e63af5ba5917d5e96bed0"
 			};
+			GLOBAL_ENV.MEDIA_PROXY_ENDPOINT = location.protocol + "//" + GLOBAL_ENV.CDN_HOST;
 			const localStorage = window.localStorage;
 			// TODO: remote auth
 			// window.GLOBAL_ENV.REMOTE_AUTH_ENDPOINT = window.GLOBAL_ENV.GATEWAY_ENDPOINT.replace(/wss?:/, "");
@@ -105,7 +106,8 @@
 			}
 
 			const settings = JSON.parse(localStorage.getItem("UserSettingsStore"));
-			if (settings && settings.locale === "en") {
+			if (settings && settings.locale.length <= 2) {
+				// fix client locale wrong and client not loading at all
 				settings.locale = "en-US";
 				localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
 			}
diff --git a/api/src/Server.ts b/api/src/Server.ts
index 1f11a295..a6887fd4 100644
--- a/api/src/Server.ts
+++ b/api/src/Server.ts
@@ -78,7 +78,7 @@ export class FosscordServer extends Server {
 		api.use("*", (error: any, req: Request, res: Response, next: NextFunction) => {
 			if (error) return next(error);
 			res.status(404).json({
-				message: "404: Not Found",
+				message: "404 endpoint not found",
 				code: 0
 			});
 			next();
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index b5a2d334..3e26e930 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -64,6 +64,7 @@ export interface MessageCreateSchema {
 	payload_json?: string;
 	file?: any;
 	attachments?: any[]; //TODO we should create an interface for attachments
+	sticker_ids?: string[];
 }
 
 // https://discord.com/developers/docs/resources/channel#create-message
diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/api/src/routes/guilds/#guild_id/emojis.ts
index ff565cd4..85d7ac05 100644
--- a/api/src/routes/guilds/#guild_id/emojis.ts
+++ b/api/src/routes/guilds/#guild_id/emojis.ts
@@ -40,17 +40,14 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A
 	const { guild_id } = req.params;
 	const body = req.body as EmojiCreateSchema;
 
+	const id = Snowflake.generate();
 	const emoji_count = await Emoji.count({ guild_id: guild_id });
 	const { maxEmojis } = Config.get().limits.guild;
 
 	if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis);
-
-	const id = Snowflake.generate();
-
 	if (body.require_colons == null) body.require_colons = true;
 
 	const user = await User.findOneOrFail({ id: req.user_id });
-
 	body.image = (await handleFile(`/emojis/${id}`, body.image)) as string;
 
 	const emoji = await new Emoji({
diff --git a/api/src/routes/guilds/#guild_id/premium.ts b/api/src/routes/guilds/#guild_id/premium.ts
new file mode 100644
index 00000000..75361ac6
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/premium.ts
@@ -0,0 +1,10 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/subscriptions", route({}), async (req: Request, res: Response) => {
+	// TODO:
+	res.json([]);
+});
+
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/stickers.ts b/api/src/routes/guilds/#guild_id/stickers.ts
new file mode 100644
index 00000000..4ea1dce1
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/stickers.ts
@@ -0,0 +1,135 @@
+import {
+	emitEvent,
+	GuildStickersUpdateEvent,
+	handleFile,
+	Member,
+	Snowflake,
+	Sticker,
+	StickerFormatType,
+	StickerType,
+	uploadFile
+} from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import multer from "multer";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	res.json(await Sticker.find({ guild_id }));
+});
+
+const bodyParser = multer({
+	limits: {
+		fileSize: 1024 * 1024 * 100,
+		fields: 10,
+		files: 1
+	},
+	storage: multer.memoryStorage()
+}).single("file");
+
+router.post(
+	"/",
+	bodyParser,
+	route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }),
+	async (req: Request, res: Response) => {
+		if (!req.file) throw new HTTPError("missing file");
+
+		const { guild_id } = req.params;
+		const body = req.body as ModifyGuildStickerSchema;
+		const id = Snowflake.generate();
+
+		const [sticker] = await Promise.all([
+			new Sticker({
+				...body,
+				guild_id,
+				id,
+				type: StickerType.GUILD,
+				format_type: getStickerFormat(req.file.mimetype),
+				available: true
+			}).save(),
+			uploadFile(`/stickers/${id}`, req.file)
+		]);
+
+		await sendStickerUpdateEvent(guild_id);
+
+		res.json(sticker);
+	}
+);
+
+export function getStickerFormat(mime_type: string) {
+	switch (mime_type) {
+		case "image/apng":
+			return StickerFormatType.APNG;
+		case "application/json":
+			return StickerFormatType.LOTTIE;
+		case "image/png":
+			return StickerFormatType.PNG;
+		case "image/gif":
+			return StickerFormatType.GIF;
+		default:
+			throw new HTTPError("invalid sticker format: must be png, apng or lottie");
+	}
+}
+
+router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
+	const { guild_id, sticker_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	res.json(await Sticker.findOneOrFail({ guild_id, id: sticker_id }));
+});
+
+export interface ModifyGuildStickerSchema {
+	/**
+	 * @minLength 2
+	 * @maxLength 30
+	 */
+	name: string;
+	/**
+	 * @maxLength 100
+	 */
+	description?: string;
+	/**
+	 * @maxLength 200
+	 */
+	tags: string;
+}
+
+router.patch(
+	"/:sticker_id",
+	route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	async (req: Request, res: Response) => {
+		const { guild_id, sticker_id } = req.params;
+		const body = req.body as ModifyGuildStickerSchema;
+
+		const sticker = await new Sticker({ ...body, guild_id, id: sticker_id }).save();
+		await sendStickerUpdateEvent(guild_id);
+
+		return res.json(sticker);
+	}
+);
+
+async function sendStickerUpdateEvent(guild_id: string) {
+	return emitEvent({
+		event: "GUILD_STICKERS_UPDATE",
+		guild_id: guild_id,
+		data: {
+			guild_id: guild_id,
+			stickers: await Sticker.find({ guild_id: guild_id })
+		}
+	} as GuildStickersUpdateEvent);
+}
+
+router.delete("/:sticker_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => {
+	const { guild_id, sticker_id } = req.params;
+
+	await Sticker.delete({ guild_id, id: sticker_id });
+	await sendStickerUpdateEvent(guild_id);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/api/src/routes/sticker-packs/#id/index.ts b/api/src/routes/sticker-packs/#id/index.ts
deleted file mode 100644
index 7f723e97..00000000
--- a/api/src/routes/sticker-packs/#id/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Request, Response, Router } from "express";
-import { route } from "@fosscord/api";
-
-const router: Router = Router();
-
-router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	res.json({
-		id: "",
-		stickers: [],
-		name: "",
-		sku_id: "",
-		cover_sticker_id: "",
-		description: "",
-		banner_asset_id: ""
-	}).status(200);
-});
-
-export default router;
diff --git a/api/src/routes/sticker-packs/index.ts b/api/src/routes/sticker-packs/index.ts
index d671c161..e6560d12 100644
--- a/api/src/routes/sticker-packs/index.ts
+++ b/api/src/routes/sticker-packs/index.ts
@@ -1,11 +1,13 @@
 import { Request, Response, Router } from "express";
 import { route } from "@fosscord/api";
+import { StickerPack } from "@fosscord/util";
 
 const router: Router = Router();
 
 router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	res.json({ sticker_packs: [] }).status(200);
+	const sticker_packs = await StickerPack.find({ relations: ["stickers"] });
+
+	res.json({ sticker_packs });
 });
 
 export default router;
diff --git a/api/src/routes/stickers/#sticker_id/index.ts b/api/src/routes/stickers/#sticker_id/index.ts
new file mode 100644
index 00000000..293ca089
--- /dev/null
+++ b/api/src/routes/stickers/#sticker_id/index.ts
@@ -0,0 +1,12 @@
+import { Sticker } from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { sticker_id } = req.params;
+
+	res.json(await Sticker.find({ id: sticker_id }));
+});
+
+export default router;
diff --git a/api/src/routes/template.ts.disabled b/api/src/routes/template.ts.disabled
index ad785f10..fcc59ef4 100644
--- a/api/src/routes/template.ts.disabled
+++ b/api/src/routes/template.ts.disabled
@@ -1,10 +1,11 @@
 //TODO: this is a template for a generic route
 
 import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
 const router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
-	res.send({});
+router.get("/",route({}), async (req: Request, res: Response) => {
+	res.json({});
 });
 
 export default router;
diff --git a/api/src/util/Message.ts b/api/src/util/Message.ts
index 40d96b42..d14d3aa2 100644
--- a/api/src/util/Message.ts
+++ b/api/src/util/Message.ts
@@ -24,7 +24,7 @@ import fetch from "node-fetch";
 import cheerio from "cheerio";
 import { MessageCreateSchema } from "../routes/channels/#channel_id/messages";
 
-// TODO: check webhook, application, system author
+// TODO: check webhook, application, system author, stickers
 // TODO: embed gifs/videos/images
 
 const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
@@ -46,6 +46,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
 
 	const message = new Message({
 		...opts,
+		sticker_items: opts.sticker_ids?.map((x) => ({ id: x })),
 		guild_id: channel.guild_id,
 		channel_id: opts.channel_id,
 		attachments: opts.attachments || [],
@@ -82,7 +83,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
 	}
 
 	// TODO: stickers/activity
-	if (!opts.content && !opts.embeds?.length && !opts.attachments?.length) {
+	if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length) {
 		throw new HTTPError("Empty messages are not allowed", 50006);
 	}
 
diff --git a/api/tsconfig.json b/api/tsconfig.json
index 2cf4e4c1..80d7251f 100644
--- a/api/tsconfig.json
+++ b/api/tsconfig.json
@@ -67,8 +67,7 @@
 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
 		"baseUrl": ".",
 		"paths": {
-			"@fosscord/api": ["src/index"],
-			"@fosscord/api/*": ["src/*"]
+			"@fosscord/api": ["src/index"]
 		},
 		"plugins": [{ "transform": "@zerollup/ts-transform-paths" }],
 		"experimentalDecorators": true
diff --git a/gateway/client.js b/gateway/client.js
index 34a59458..c841c6a0 100644
--- a/gateway/client.js
+++ b/gateway/client.js
@@ -3,7 +3,7 @@ const WebSocket = require("ws");
 const Constants = require("./dist/util/Constants");
 
 // const ws = new WebSocket("ws://127.0.0.1:8080");
-const ws = new WebSocket("wss://gateway.discord.gg");
+const ws = new WebSocket("wss://dev.fosscord.com");
 
 ws.on("open", () => {
 	// ws.send(JSON.stringify({ req_type: "new_auth" }));
diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts
index c91ca5dd..2f9d4632 100644
--- a/gateway/src/opcodes/Identify.ts
+++ b/gateway/src/opcodes/Identify.ts
@@ -179,6 +179,8 @@ export async function onIdentify(this: WebSocket, data: Payload) {
 			x.guild_hashes = {}; // @ts-ignore
 			x.guild_scheduled_events = []; // @ts-ignore
 			x.threads = [];
+			x.premium_subscription_count = 30;
+			x.premium_tier = 3;
 			return x;
 		}),
 		guild_experiments: [], // TODO
diff --git a/util/src/migrations/migrate_db_engine.js b/util/scripts/migrate_db_engine.js
index 79e9d86f..79e9d86f 100644
--- a/util/src/migrations/migrate_db_engine.js
+++ b/util/scripts/migrate_db_engine.js
diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts
index 63cd6ad3..a4d38315 100644
--- a/util/src/entities/Message.ts
+++ b/util/src/entities/Message.ts
@@ -127,7 +127,7 @@ export class Message extends BaseClass {
 	mention_channels: Channel[];
 
 	@JoinTable({ name: "message_stickers" })
-	@ManyToMany(() => Sticker)
+	@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
 	sticker_items?: Sticker[];
 
 	@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
diff --git a/util/src/entities/Migration.ts b/util/src/entities/Migration.ts
new file mode 100644
index 00000000..09df70fb
--- /dev/null
+++ b/util/src/entities/Migration.ts
@@ -0,0 +1,18 @@
+import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
+import { BaseClassWithoutId } from ".";
+
+export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
+	? ObjectIdColumn
+	: PrimaryGeneratedColumn;
+
+@Entity("migrations")
+export class Migration extends BaseClassWithoutId {
+	@PrimaryIdAutoGenerated()
+	id: number;
+
+	@Column()
+	timestamp: number;
+
+	@Column()
+	name: string;
+}
diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts
index 89480e83..ebef89be 100644
--- a/util/src/entities/ReadState.ts
+++ b/util/src/entities/ReadState.ts
@@ -32,13 +32,8 @@ export class ReadState extends BaseClass {
 	user: User;
 
 	@Column({ nullable: true })
-	@RelationId((read_state: ReadState) => read_state.last_message)
 	last_message_id: string;
 
-	@JoinColumn({ name: "last_message_id" })
-	@ManyToOne(() => Message, { nullable: true })
-	last_message?: Message;
-
 	@Column({ nullable: true })
 	last_pin_timestamp?: Date;
 
diff --git a/util/src/entities/Sticker.ts b/util/src/entities/Sticker.ts
index 036ff2d0..37bc6fbe 100644
--- a/util/src/entities/Sticker.ts
+++ b/util/src/entities/Sticker.ts
@@ -1,4 +1,5 @@
-import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { User } from "./User";
 import { BaseClass } from "./BaseClass";
 import { Guild } from "./Guild";
 
@@ -8,6 +9,7 @@ export enum StickerType {
 }
 
 export enum StickerFormatType {
+	GIF = 0, // gif is a custom format type and not in discord spec
 	PNG = 1,
 	APNG = 2,
 	LOTTIE = 3,
@@ -21,11 +23,22 @@ export class Sticker extends BaseClass {
 	@Column({ nullable: true })
 	description?: string;
 
-	@Column()
-	tags: string;
+	@Column({ nullable: true })
+	available?: boolean;
 
-	@Column()
-	pack_id: string;
+	@Column({ nullable: true })
+	tags?: string;
+
+	@Column({ nullable: true })
+	@RelationId((sticker: Sticker) => sticker.pack)
+	pack_id?: string;
+
+	@JoinColumn({ name: "pack_id" })
+	@ManyToOne(() => require("./StickerPack").StickerPack, {
+		onDelete: "CASCADE",
+		nullable: true,
+	})
+	pack: import("./StickerPack").StickerPack;
 
 	@Column({ nullable: true })
 	guild_id?: string;
@@ -36,6 +49,15 @@ export class Sticker extends BaseClass {
 	})
 	guild?: Guild;
 
+	@Column({ nullable: true })
+	user_id?: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user?: User;
+
 	@Column({ type: "int" })
 	type: StickerType;
 
diff --git a/util/src/entities/StickerPack.ts b/util/src/entities/StickerPack.ts
new file mode 100644
index 00000000..ec8c69a2
--- /dev/null
+++ b/util/src/entities/StickerPack.ts
@@ -0,0 +1,31 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import { Sticker } from ".";
+import { BaseClass } from "./BaseClass";
+
+@Entity("sticker_packs")
+export class StickerPack extends BaseClass {
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	banner_asset_id?: string;
+
+	@OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
+	stickers: Sticker[];
+
+	// sku_id: string
+
+	@Column({ nullable: true })
+	@RelationId((pack: StickerPack) => pack.cover_sticker)
+	cover_sticker_id?: string;
+
+	@ManyToOne(() => Sticker, { nullable: true })
+	@JoinColumn()
+	cover_sticker?: Sticker;
+}
diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts
index 7b1c9750..b52841c9 100644
--- a/util/src/entities/index.ts
+++ b/util/src/entities/index.ts
@@ -11,6 +11,7 @@ export * from "./Guild";
 export * from "./Invite";
 export * from "./Member";
 export * from "./Message";
+export * from "./Migration";
 export * from "./RateLimit";
 export * from "./ReadState";
 export * from "./Recipient";
@@ -18,6 +19,7 @@ export * from "./Relationship";
 export * from "./Role";
 export * from "./Session";
 export * from "./Sticker";
+export * from "./StickerPack";
 export * from "./Team";
 export * from "./TeamMember";
 export * from "./Template";
diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts
index 3c8ab8ab..13fd4b8b 100644
--- a/util/src/interfaces/Event.ts
+++ b/util/src/interfaces/Event.ts
@@ -12,6 +12,7 @@ import { Interaction } from "./Interaction";
 import { ConnectedAccount } from "../entities/ConnectedAccount";
 import { Relationship, RelationshipType } from "../entities/Relationship";
 import { Presence } from "./Presence";
+import { Sticker } from "..";
 
 export interface Event {
 	guild_id?: string;
@@ -193,6 +194,14 @@ export interface GuildEmojisUpdateEvent extends Event {
 	};
 }
 
+export interface GuildStickersUpdateEvent extends Event {
+	event: "GUILD_STICKERS_UPDATE";
+	data: {
+		guild_id: string;
+		stickers: Sticker[];
+	};
+}
+
 export interface GuildIntegrationUpdateEvent extends Event {
 	event: "GUILD_INTEGRATIONS_UPDATE";
 	data: {
@@ -553,6 +562,7 @@ export type EVENT =
 	| "GUILD_BAN_ADD"
 	| "GUILD_BAN_REMOVE"
 	| "GUILD_EMOJIS_UPDATE"
+	| "GUILD_STICKERS_UPDATE"
 	| "GUILD_INTEGRATIONS_UPDATE"
 	| "GUILD_MEMBER_ADD"
 	| "GUILD_MEMBER_REMOVE"
diff --git a/util/src/migrations/1633881705509-VanityInvite.ts b/util/src/migrations/1633881705509-VanityInvite.ts
index af9b98ae..45485310 100644
--- a/util/src/migrations/1633881705509-VanityInvite.ts
+++ b/util/src/migrations/1633881705509-VanityInvite.ts
@@ -1,6 +1,8 @@
 import { MigrationInterface, QueryRunner } from "typeorm";
 
 export class VanityInvite1633881705509 implements MigrationInterface {
+	name = "VanityInvite1633881705509";
+
 	public async up(queryRunner: QueryRunner): Promise<void> {
 		try {
 			await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN vanity_url_code`);
diff --git a/util/src/migrations/1634308884591-Stickers.ts b/util/src/migrations/1634308884591-Stickers.ts
new file mode 100644
index 00000000..fbc4649f
--- /dev/null
+++ b/util/src/migrations/1634308884591-Stickers.ts
@@ -0,0 +1,66 @@
+import { MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey } from "typeorm";
+
+export class Stickers1634308884591 implements MigrationInterface {
+	name = "Stickers1634308884591";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.dropForeignKey("read_states", "FK_6f255d873cfbfd7a93849b7ff74");
+		await queryRunner.changeColumn(
+			"stickers",
+			"tags",
+			new TableColumn({ name: "tags", type: "varchar", isNullable: true })
+		);
+		await queryRunner.changeColumn(
+			"stickers",
+			"pack_id",
+			new TableColumn({ name: "pack_id", type: "varchar", isNullable: true })
+		);
+		await queryRunner.changeColumn("stickers", "type", new TableColumn({ name: "type", type: "integer" }));
+		await queryRunner.changeColumn(
+			"stickers",
+			"format_type",
+			new TableColumn({ name: "format_type", type: "integer" })
+		);
+		await queryRunner.changeColumn(
+			"stickers",
+			"available",
+			new TableColumn({ name: "available", type: "boolean", isNullable: true })
+		);
+		await queryRunner.changeColumn(
+			"stickers",
+			"user_id",
+			new TableColumn({ name: "user_id", type: "boolean", isNullable: true })
+		);
+		await queryRunner.createForeignKey(
+			"stickers",
+			new TableForeignKey({
+				name: "FK_8f4ee73f2bb2325ff980502e158",
+				columnNames: ["user_id"],
+				referencedColumnNames: ["id"],
+				referencedTableName: "users",
+				onDelete: "CASCADE",
+			})
+		);
+		await queryRunner.createTable(
+			new Table({
+				name: "sticker_packs",
+				columns: [
+					new TableColumn({ name: "id", type: "varchar", isPrimary: true }),
+					new TableColumn({ name: "name", type: "varchar" }),
+					new TableColumn({ name: "description", type: "varchar", isNullable: true }),
+					new TableColumn({ name: "banner_asset_id", type: "varchar", isNullable: true }),
+					new TableColumn({ name: "cover_sticker_id", type: "varchar", isNullable: true }),
+				],
+				foreignKeys: [
+					new TableForeignKey({
+						columnNames: ["cover_sticker_id"],
+						referencedColumnNames: ["id"],
+						referencedTableName: "stickers",
+					}),
+				],
+			})
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {}
+}
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
index 8bce3a6f..6124ffab 100644
--- a/util/src/util/Database.ts
+++ b/util/src/util/Database.ts
@@ -2,6 +2,7 @@ import path from "path";
 import "reflect-metadata";
 import { Connection, createConnection } from "typeorm";
 import * as Models from "../entities";
+import { Migration } from "../entities/Migration";
 import { yellow, green } from "nanocolors";
 
 // UUID extension option is only supported with postgres
@@ -33,10 +34,27 @@ export function initDatabase(): Promise<Connection> {
 		bigNumberStrings: false,
 		supportBigNumbers: true,
 		name: "default",
+		migrations: [path.join(__dirname, "..", "migrations", "*.js")],
 	});
 
-	promise.then((connection) => {
+	promise.then(async (connection: Connection) => {
 		dbConnection = connection;
+
+		// run migrations, and if it is a new fresh database, set it to the last migration
+		if (connection.migrations.length) {
+			if (!(await Migration.findOne({}))) {
+				let i = 0;
+
+				await Migration.insert(
+					connection.migrations.map((x) => ({
+						id: i++,
+						name: x.name,
+						timestamp: Date.now(),
+					}))
+				);
+			}
+		}
+		await connection.runMigrations();
 		console.log(`[Database] ${green("connected")}`);
 	});
 
diff --git a/util/src/util/cdn.ts b/util/src/util/cdn.ts
index 4dd0078a..ea950cd1 100644
--- a/util/src/util/cdn.ts
+++ b/util/src/util/cdn.ts
@@ -4,7 +4,9 @@ import fetch from "node-fetch";
 import { Config } from "./Config";
 import multer from "multer";
 
-export async function uploadFile(path: string, file: Express.Multer.File) {
+export async function uploadFile(path: string, file?: Express.Multer.File) {
+	if (!file?.buffer) throw new HTTPError("Missing file in body");
+
 	const form = new FormData();
 	form.append("file", file.buffer, {
 		contentType: file.mimetype,