1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-23 10:52:30 +01:00

Merge pull request #455 from fosscord/sticker

Stickers
This commit is contained in:
Samuel 2021-10-15 18:39:57 +02:00 committed by GitHub
commit 1bbf789a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 664 additions and 47 deletions

View File

@ -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": {

View File

@ -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));
}

View File

@ -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();

View File

@ -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

View File

@ -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({

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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" }));

View File

@ -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

View File

@ -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, {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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";

View File

@ -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"

View File

@ -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`);

View File

@ -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> {}
}

View File

@ -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")}`);
});

View File

@ -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,