import { AnimationResolvable } from "../types/animation";
import Snowflake from "./snowflake";
import UserClient from "./userClient";
import User from "./user";
import Comment from "./comment";
import Collection from "@discordjs/collection";
import { SnowflakeString } from "../types/snowflake";
import Category from "./category";
import Rating from "./rating";
import IFilters from "../types/filters";
import Filters from "./filters";
import { UserResolvable } from "../types/user";
import { RequestContext, RequestContextWithClient } from "./requestContext";
import { UserType } from "../types/basicUser";
import { CommentState } from "../types/comments";
import Language, { LanguageCodes } from "../types/language";
import { DBAnimation, DBAnimationFull, prisma } from "../../prisma";
/**
* Holds animation data without methods
*/
export class BasicAnimation {
id: Snowflake;
ctx: RequestContext;
author: User;
data: string;
category: Category;
published?: Date;
name: string;
description: string;
length: number;
language: Language;
rating?: number | null;
ratingCount?: number;
views?: number;
/**
* Creates new animation data object
* @param {object} data
*/
constructor({
id,
ctx,
author,
data,
category,
name,
description,
length,
published,
rating,
ratingCount,
views,
language = Language.EN
}: {
id: Snowflake,
ctx: RequestContext,
author: User,
data: string,
category: Category,
name: string,
description: string,
length: number,
published?: Date,
rating?: number | null,
ratingCount?: number,
views?: number,
language?: Language
}) {
this.id = id;
this.ctx = ctx;
this.author = author;
this.data = data;
this.category = category;
this.name = name;
this.description = description;
this.length = length;
this.published = published;
this.rating = rating;
this.ratingCount = ratingCount;
this.views = views;
this.language = language;
}
/**
* Returns if the param is animation or not. Used for TS.
* @param {AnimationResolvable} animation
* @return {boolean}
*/
static isAnimation(animation: AnimationResolvable):
animation is Animation | BasicAnimation {
return animation instanceof BasicAnimation;
}
/**
* Resolves params to string
* @param {UserResolvable} animation
* @return {SnowflakeString}
*/
static resolveAnimationID(animation: AnimationResolvable): SnowflakeString {
if(Animation.isAnimation(animation)) return animation.id.toString();
return animation.toString();
}
/**
* @param {DBAnimation | DBAnimationFull} data
* @param {RequestContext} ctx
* @return {Promise<BasicAnimation>}
*/
static async fromDatabase(data: DBAnimation | DBAnimationFull, ctx: RequestContext):
Promise<BasicAnimation> {
if(
!data.published &&
data.authorId !== ctx.client?.user.id.toBigInt() &&
(ctx.client && ctx.client.user.type >= UserType.MODERATOR)
) {
throw new Error("not_published");
}
if(ctx.animations.has(data.snowflake.toString())) {
return ctx.animations.get(data.snowflake.toString()) as BasicAnimation;
}
return new BasicAnimation({
id: new Snowflake(data.snowflake),
ctx,
author: "author" in data ?
await User.fromDatabase(data.author, ctx) :
await User.getUser(data.authorId, ctx),
data: data.data,
category: "category" in data ?
await Category.fromDatabase(data.category, ctx) :
await Category.getCategory(data.categoryId, ctx),
description: data.description,
length: data.length,
name: data.name,
published: data.published || undefined,
language: data.languageId,
views: data.views
});
}
/**
* Fetches animation by snowflake from DB
* @param {AnimationResolvable} id
* @param {RequestContext} ctx
* @return {Promise<BasicAnimation>}
*/
static async getAnimation(id: AnimationResolvable, ctx: RequestContext):
Promise<BasicAnimation> {
if(Animation.isAnimation(id)) return id satisfies BasicAnimation;
id = new Snowflake(id);
const animation = await prisma.animation.findUniqueOrThrow({
where: {
snowflake: id.toBigInt()
},
include: {
author: true,
category: true
}
}) satisfies DBAnimationFull;
var anim = await this.fromDatabase(animation, ctx);
anim.rating = await anim.getAverageRating();
anim.ratingCount = await anim.getRatingCount();
return anim;
}
/**
* Expands animation to full animation
* @param {AnimationResolvable} id
* @param {RequestContextWithClient} ctx
*/
static async getFullAnimation(id: AnimationResolvable, ctx: RequestContextWithClient):
Promise<Animation> {
var anim = await Animation.getAnimation(id, ctx);
return new Animation({
...anim,
ctx
});
}
/**
* Fetches all animations from DB
* @param {any} data
*/
static async newAnimation(data: {
id: Snowflake,
ctx: RequestContextWithClient,
data: string,
category: Category,
name: string,
description: string,
length: number,
language?: Language,
published?: Date
}): Promise<Animation> {
const animation = await prisma.animation.create({
data: {
snowflake: data.id.toBigInt(),
authorId: data.ctx.user!.id.toBigInt(),
data: data.data,
categoryId: data.category.id.toBigInt(),
name: data.name,
description: data.description,
published: data.published,
length: data.length,
languageId: data.language
}
}) satisfies DBAnimation;
const anim = await Animation.fromDatabase(animation, data.ctx);
return Animation.getFullAnimation(anim, data.ctx);
}
/**
* Fetches comments on the animation
* @param {number} max items per page
* @param {number} page
* @return {Promise<Collection<SnowflakeString, Comment>>}
*/
async getComments(max: number, page: number): Promise<Collection<SnowflakeString, Comment>> {
const comments = await prisma.comment.findMany({
where: {
animationId: this.id.toBigInt(),
state: this.ctx.user && this.ctx.user.type >= UserType.MODERATOR ?
undefined :
CommentState.NORMAL
},
orderBy: {
snowflake: "desc"
},
include: {
author: true
},
skip: page*max,
take: max
});
const col = new Collection<SnowflakeString, Comment>();
const reported = new Set<bigint>();
await Promise.all([
(async () => {
for(const comment of comments) {
col.set(
comment.snowflake.toString(),
await Comment.fromDatabase(comment, this.ctx)
);
}
})(), (async () => {
if(this.ctx.user && this.ctx.user.type >= UserType.MODERATOR) {
const reports = await prisma.modQueue.findMany({
where: {
resourceId: {
in: comments.map((comments) => comments.snowflake)
}
}
});
for(const report of reports) {
reported.add(report.resourceId);
}
}
})()
]);
for(const report of reported) {
const comment = col.get(report.toString());
if(comment) comment.reported = true;
}
return col;
}
/**
* Fetches comments on the animation
* @return {Promise<Cnumber>}
*/
getCommentCount(): Promise<number> {
return prisma.comment.count({
where: {
animationId: this.id.toBigInt(),
state: this.ctx.user && this.ctx.user.type >= UserType.MODERATOR ?
undefined :
CommentState.NORMAL
}
});
}
/**
* Fetches ratings on the animation
* @param {number} max items per page
* @param {number} page
* @return {Promise<Collection<SnowflakeString, Rating>>}
*/
async getRatings(max: number, page: number): Promise<Collection<SnowflakeString, Rating>> {
const ratings = await prisma.rating.findMany({
where: {
animationId: this.id.toBigInt()
},
include: {
author: true
},
skip: page*max,
take: max
});
var col = new Collection<SnowflakeString, Rating>();
for(const rating of ratings) {
col.set(rating.snowflake.toString(), await Rating.fromDatabase(rating, this.ctx));
}
return col;
}
/**
* @return {Promise<number|null>}
*/
async getAverageRating(): Promise<number | null> {
const result = await prisma.rating.aggregate({
where: {
animationId: this.id.toBigInt()
},
_avg: {
stars: true
}
});
return result._avg.stars;
}
/**
* @return {Promise<number>}
*/
getRatingCount(): Promise<number> {
return prisma.rating.count({
where: {
animationId: this.id.toBigInt()
}
});
}
/**
* @return {Promise<void>}
*/
async registerView(): Promise<void> {
await prisma.animation.update({
where: {
snowflake: this.id.toBigInt()
},
data: {
views: {
increment: 1
}
}
});
}
/**
* Returns a rating from given user, if any
* @param {UserResolvable} user
* @return {Promise<Rating | null>}
*/
getRatingFrom(user: UserResolvable): Promise<Rating | null> {
return Rating.getUserRating(user, this, this.ctx);
}
toMinimal(includeAuthor: false): {
id: string;
created: Date;
category: any;
rating: number | undefined;
ratingCount: number | undefined;
name: string;
published: Date | undefined;
length: number;
language: typeof LanguageCodes[keyof typeof LanguageCodes];
views: number | undefined;
}
toMinimal(includeAuthor: true): {
id: string;
created: Date;
category: any;
rating: number | undefined;
ratingCount: number | undefined;
author: {
id: string;
avatar: string | undefined;
registered: Date;
name: string;
};
name: string;
published: Date | undefined;
length: number;
language: typeof LanguageCodes[keyof typeof LanguageCodes];
views: number | undefined;
}
/**
* Returns minimum animation representation
* @param {boolean} includeAuthor
* @return {object}
*/
toMinimal(includeAuthor: boolean): {
id: string;
created: Date;
category: any;
rating: number | undefined | null;
ratingCount: number | undefined;
author?: {
id: string;
avatar: string | undefined;
registered: Date;
name: string;
};
name: string;
published: Date | undefined;
length: number;
language: typeof LanguageCodes[keyof typeof LanguageCodes];
views: number | undefined;
} {
if(includeAuthor === true) {
return {
id: this.id.getSnowflake(),
created: this.id.time,
rating: this.rating,
ratingCount: this.ratingCount,
category: this.category.toJSON(),
author: this.author.toJSON(),
name: this.name,
length: this.length,
published: this.published,
language: LanguageCodes[this.language],
views: this.views
};
}
return {
id: this.id.getSnowflake(),
created: this.id.time,
rating: this.rating,
ratingCount: this.ratingCount,
category: this.category.toJSON(),
name: this.name,
length: this.length,
published: this.published,
language: LanguageCodes[this.language],
views: this.views
};
}
/**
* Converts animations to JSON-compatible object for saving resources
* @return {Object}
*/
toJSON() {
return {
id: this.id.getSnowflake(),
created: this.id.time,
rating: this.rating,
ratingCount: this.ratingCount,
author: this.author.toJSON(),
data: this.data,
category: this.category.toJSON(),
name: this.name,
description: this.description,
published: this.published,
length: this.length,
language: this.language,
views: this.views
};
}
/**
* Counts animations matching given filters
* @param {RequestContext} ctx
* @param {IFilters} ifilters
* @return {Promise<number>}
*/
static countAnimations(ctx: RequestContext, ifilters: IFilters) : Promise<number> {
var filters: Filters = ifilters instanceof Filters ? ifilters : new Filters(ifilters, ctx);
return new Promise((resolve, reject) => {
var query = filters.buildCountQuery();
ctx.conn.query(query.query, query.variables, async (err, results) => {
if(err) return reject(err);
resolve(results[0].count);
});
});
}
/**
* Gets animations that fit into the specified filters
* @param {RequestContext} ctx
* @param {IFilters} ifilters
* @return {Promise<Collection<SnowflakeString, BasicAnimation>>}
*/
static getAnimations(ctx: RequestContext, ifilters: IFilters):
Promise<Collection<SnowflakeString, BasicAnimation>> {
var filters: Filters = ifilters instanceof Filters ? ifilters : new Filters(ifilters, ctx);
return new Promise((resolve, reject) => {
var query = filters.buildQuery();
ctx.conn.query(query.query, query.variables, async (err, results) => {
if(err) return reject(err);
var col = new Collection<SnowflakeString, BasicAnimation>();
for(var result of results) {
col.set(result.snowflake, new BasicAnimation({
id: new Snowflake(BigInt(result.snowflake)),
ctx,
author: await User.getUser(result.author, ctx),
data: result.data,
category: await Category.getCategory(result.category, ctx),
name: result.name,
description: result.description,
length: result.length,
published: result.published
}));
}
resolve(col);
});
});
}
}
/**
* Holds animation data
* @extends {BasicAnimation}
*/
class Animation extends BasicAnimation {
client: UserClient;
/**
* Creates new animation data object
* @param {object} data
*/
constructor({
id,
ctx,
author,
data,
category,
name,
description,
length
}: {
id: Snowflake,
ctx: RequestContextWithClient,
author: User,
data: string,
category: Category,
name: string,
description: string,
length: number
}) {
super({
id,
ctx,
author,
data,
category,
name,
length,
description
});
this.client = ctx.client;
}
/**
* rates the animation
* @param {Rating | number} rateResolvable
* @return {Promise<Rating>}
*/
async rate(rateResolvable: Rating | number): Promise<Rating> {
var rate: Rating;
if(rateResolvable instanceof Rating) rate = rateResolvable;
else if(typeof rateResolvable === "number") {
rate = new Rating({
id: Snowflake.newSnowflake(),
animation: this,
ctx: this.ctx,
stars: rateResolvable,
user: this.client.user
});
} else throw new TypeError("rateResolvable must be Rating or number");
if(rate.animation.id !== this.id) {
throw new Error("rateResolvable must be for this animation");
}
const res = await prisma.rating.upsert({
where: {
authorId_animationId: {
authorId: rate.user.id.toBigInt(),
animationId: this.id.toBigInt()
}
},
create: {
snowflake: rate.id.toBigInt(),
stars: rate.stars,
animation: {
connect: {
snowflake: this.id.toBigInt()
}
},
author: {
connect: {
snowflake: rate.user.id.toBigInt()
}
}
},
update: {
stars: rate.stars
}
});
rate.id = new Snowflake(res.snowflake);
return rate;
}
/**
* comments on the animation
* @param {Comment | string} commentResolvable
* @return {Promise<Comment>}
*/
async comment(commentResolvable: Comment | string): Promise<Comment> {
var comment: Comment;
if(commentResolvable instanceof Comment) comment = commentResolvable;
else if(typeof commentResolvable === "string") {
comment = new Comment({
id: Snowflake.newSnowflake(),
animation: this,
ctx: this.ctx,
content: commentResolvable,
user: this.client.user,
state: CommentState.NORMAL
});
} else throw new Error("Invalid comment resolvable");
await prisma.comment.create({
data: {
snowflake: comment.id.toBigInt(),
animation: {
connect: {
snowflake: this.id.toBigInt()
}
},
author: {
connect: {
snowflake: this.client.user.id.toBigInt()
}
},
content: comment.content,
state: comment.state
}
});
return comment;
}
/**
* Updates the animation data
* @return {Promise<void>}
*/
async update(): Promise<void> {
if(this.author.id.getSnowflake() !== this.client.user.id.getSnowflake()) {
throw new Error("Access rights");
}
await prisma.animation.update({
where: {
snowflake: this.id.toBigInt()
},
data: {
data: typeof this.data === "string" ? this.data : JSON.stringify(this.data),
name: this.name,
description: this.description,
category: {
connect: {
snowflake: this.category.id.toBigInt()
}
},
length: this.length,
published: this.published,
language: {
connect: {
id: this.language
}
}
}
});
}
/**
* Checks animation data
* @param {any} data
* @return {boolean}
*/
static checkData(data: any) {
if(
typeof data !== "object" ||
!Array.isArray(data.elements) ||
data.elements.findIndex(
(element: any) =>
typeof element.start !== "number" ||
typeof element.end !== "number" ||
typeof element.x !== "number" ||
typeof element.y !== "number" ||
typeof element.z !== "number" ||
typeof element.media !== "object" ||
typeof element.media.type !== "string" ||
typeof element.media.width !== "number" ||
typeof element.media.height !== "number" ||
!Array.isArray(element.keyframes) ||
element.keyframes.findIndex(
(keyframe: any) =>
typeof keyframe !== "object" ||
!["number", "undefined"].includes(typeof keyframe.t) ||
!["number", "undefined"].includes(typeof keyframe.x) ||
!["number", "undefined"].includes(typeof keyframe.y) ||
!["number", "undefined"].includes(typeof keyframe.w) ||
!["number", "undefined"].includes(typeof keyframe.h) ||
!["number", "undefined"].includes(typeof keyframe.r) ||
!["number", "undefined"].includes(typeof keyframe.rt) ||
!["string", "undefined"].includes(typeof keyframe.fill) ||
!["number", "undefined"].includes(typeof keyframe.o)
) !== -1
) !== -1
) {
return false;
}
return true;
}
}
export default Animation;
Source