Browse Source

Use the Database package in request service

dev
David Ludwig 4 years ago
parent
commit
eca50dd905
22 changed files with 40 additions and 487 deletions
  1. +1
    -1
      services/request/src/server/Application.ts
  2. +0
    -22
      services/request/src/server/database/entities/DiscordAccount.ts
  3. +0
    -17
      services/request/src/server/database/entities/DiscordChannel.ts
  4. +0
    -31
      services/request/src/server/database/entities/DiscordLinkRequest.ts
  5. +0
    -26
      services/request/src/server/database/entities/DiscordRequest.ts
  6. +0
    -23
      services/request/src/server/database/entities/MovieInfo.ts
  7. +0
    -11
      services/request/src/server/database/entities/MovieQuota.ts
  8. +0
    -93
      services/request/src/server/database/entities/MovieTicket.ts
  9. +0
    -18
      services/request/src/server/database/entities/MovieTorrent.ts
  10. +0
    -50
      services/request/src/server/database/entities/PlexMovie.ts
  11. +0
    -32
      services/request/src/server/database/entities/RegisterToken.ts
  12. +0
    -113
      services/request/src/server/database/entities/User.ts
  13. +0
    -11
      services/request/src/server/database/entities/index.ts
  14. +0
    -22
      services/request/src/server/database/index.ts
  15. +9
    -3
      services/request/src/server/services/Database.ts
  16. +4
    -5
      services/request/src/server/services/DiscordBot.ts
  17. +1
    -1
      services/request/src/server/services/MovieSearch.ts
  18. +1
    -1
      services/request/src/server/services/PlexLibrary.ts
  19. +1
    -1
      services/request/src/server/services/WebServer/middleware/auth.ts
  20. +21
    -4
      services/request/src/server/services/WebServer/routes/api.ts
  21. +1
    -1
      services/request/src/server/services/WebServer/routes/auth.ts
  22. +1
    -1
      services/request/src/server/services/WebServer/validators.ts

+ 1
- 1
services/request/src/server/Application.ts View File

@ -1,6 +1,6 @@
import services from "./services";
import Service from "./services/Service";
import { User, RegisterToken } from "./database/entities";
import { User, RegisterToken } from "@autoplex/database";
import assert from "assert";
interface ServiceMap {


+ 0
- 22
services/request/src/server/database/entities/DiscordAccount.ts View File

@ -1,22 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { DiscordRequest } from "./DiscordRequest";
import { User } from "./User";
@Entity()
export class DiscordAccount extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ length: 20 })
discordId!: string;
@ManyToOne(() => User, user => user.discordAccounts)
user!: User;
@OneToMany(() => DiscordRequest, request => request.account)
requests!: DiscordRequest[];
@CreateDateColumn()
createdAt!: Date;
}

+ 0
- 17
services/request/src/server/database/entities/DiscordChannel.ts View File

@ -1,17 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class DiscordChannel extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ length: 20 })
channelId!: string;
@Column({ length: 20 })
messageId!: string;
@CreateDateColumn()
createdAt!: Date;
}

+ 0
- 31
services/request/src/server/database/entities/DiscordLinkRequest.ts View File

@ -1,31 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
import { DiscordAccount } from "./DiscordAccount";
import { User } from "./User";
@Entity()
export class DiscordLinkRequest extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
discordId!: string;
@Column()
token!: string;
@CreateDateColumn()
createdAt!: Date;
/**
* Fulfill a link request for the given user
*/
public async linkUser(user: User) {
let account = new DiscordAccount();
account.discordId = this.discordId;
account.user = user;
let awaitSave = account.save();
await DiscordLinkRequest.delete({ id: this.id });
return await awaitSave;
}
}

+ 0
- 26
services/request/src/server/database/entities/DiscordRequest.ts View File

@ -1,26 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { DiscordAccount } from "./DiscordAccount";
import { MovieTicket } from "./MovieTicket";
@Entity()
export class DiscordRequest extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ length: 20 })
channelId!: string;
@Column({ length: 20 })
messageId!: string;
@CreateDateColumn()
createdAt!: Date;
@ManyToOne(() => DiscordAccount, account => account.requests)
account!: DiscordAccount;
@OneToOne(() => MovieTicket)
@JoinColumn()
ticket!: MovieTicket;
}

+ 0
- 23
services/request/src/server/database/entities/MovieInfo.ts View File

@ -1,23 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
@Entity()
export class MovieInfo extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "text", nullable: true })
overview!: string | null;
@Column({ type: "int", nullable: true })
runtime!: number | null;
@Column({ type: "char", length: 10, nullable: true })
releaseDate!: string | null;
@Column({ type: "varchar", length: 32, nullable: true })
backdropPath!: string | null;
@Column({ type: "varchar", length: 32, nullable: true })
posterPath!: string | null;
}

+ 0
- 11
services/request/src/server/database/entities/MovieQuota.ts View File

@ -1,11 +0,0 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class MovieQuota extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ default: 5 })
moviesPerWeek!: number;
}

+ 0
- 93
services/request/src/server/database/entities/MovieTicket.ts View File

@ -1,93 +0,0 @@
import { IApiMovieDetails } from "@common/api_schema";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn, Not, IsNull } from "typeorm";
import { MovieInfo } from "./MovieInfo";
import { MovieTorrent } from "./MovieTorrent";
import { User } from "./User";
@Entity()
export class MovieTicket extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: "int", nullable: true })
tmdbId!: number | null;
@Column({ type: "varchar", length: 27, nullable: true })
imdbId!: string | null;
@Column({ type: "varchar" })
title!: string;
@Column({ type: "year", nullable: true })
year!: number | null;
@CreateDateColumn()
createdAt!: Date;
@Column({ default: false })
isFulfilled!: boolean;
@Column({ default: false })
isCanceled!: boolean;
@Column({ default: false })
isStale!: boolean;
@ManyToOne(() => User, user => user.movieTickets)
user!: User;
@OneToMany(() => MovieTorrent, torrent => torrent.movieTicket)
torrents!: MovieTorrent[];
@OneToOne(() => MovieInfo, { nullable: true })
@JoinColumn()
info!: MovieInfo | null;
/**
* @TODO This needs to check for fulfilled tickets too, but not there yet
* Fetch all active ticket ID's
*/
public static async activeTicketMap() {
let tickets = await MovieTicket.find({ where: { tmdbId: Not(IsNull()), isCanceled: false } });
let result: {[tmdbId: number]: number} = {};
for (let ticket of tickets) {
result[<number>ticket.tmdbId] = ticket.id;
}
return result;
}
/**
* Insert a request via IMDb movie details
*/
public static async requestImdb(user: User, imdbId: string, title: string, year: number) {
let ticket = new MovieTicket();
ticket.imdbId = imdbId;
ticket.title = title;
ticket.year = year;
ticket.user = user;
return await ticket.save();
}
/**
* Insert a rquest via TMDb movie details
*/
public static async requestTmdb(user: User, movie: IApiMovieDetails) {
let info = new MovieInfo();
info.overview = movie.overview;
info.posterPath = movie.posterPath;
info.backdropPath = movie.backdropPath;
info.releaseDate = movie.releaseDate;
info.runtime = movie.runtime;
await info.save();
let ticket = new MovieTicket();
ticket.tmdbId = movie.tmdbId;
ticket.imdbId = movie.imdbId;
ticket.title = movie.title;
ticket.year = movie.releaseDate ? parseInt(movie.releaseDate.slice(0, 4)) : null;
ticket.user = user;
ticket.info = info;
return await ticket.save();
}
}

+ 0
- 18
services/request/src/server/database/entities/MovieTorrent.ts View File

@ -1,18 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm";
import { MovieTicket } from "./MovieTicket";
@Entity()
export class MovieTorrent extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
infoHash!: string;
@Column()
diskName!: string;
@ManyToOne(() => MovieTicket, ticket => ticket.torrents)
movieTicket!: MovieTicket;
}

+ 0
- 50
services/request/src/server/database/entities/PlexMovie.ts View File

@ -1,50 +0,0 @@
import Plex from "@lib/plex";
import { BaseEntity, Column, Entity, In, PrimaryColumn } from "typeorm";
@Entity()
export class PlexMovie extends BaseEntity
{
@PrimaryColumn({ length: 27 })
imdbId!: string;
@Column({ type: "int", nullable: true, unique: true })
tmdbId!: number|null;
@Column()
plexKey!: number;
/**
* Check if a movie is on Plex given its TMDb ID
*/
public static async findPlexKey(tmdbId: number|string) {
return (await PlexMovie.findOne({ where: { tmdbId } }))?.plexKey ?? null;
}
/**
* Get the set of IMDb IDs stored in the library
*/
public static async imdbSet() {
let rows = await PlexMovie.createQueryBuilder("plex_movie")
.select("imdbId")
.getRawMany();
return new Set(rows.map(row => <string>row.imdbId));
}
/**
* Insert a set of IMDb IDs into the database
*/
public static async insertMovies(movies: { imdbId: string, plexKey: number }[]) {
await PlexMovie.createQueryBuilder()
.insert()
.into("plex_movie")
.values([...movies].map(({imdbId, plexKey}) => ({ imdbId, tmdbId: null, plexKey })))
.execute();
}
/**
* Remove the given set of IMDb IDs from the library
*/
public static async removeImdbSet(imdbIds: Set<string>) {
await PlexMovie.delete({ imdbId: In([...imdbIds]) });
}
}

+ 0
- 32
services/request/src/server/database/entities/RegisterToken.ts View File

@ -1,32 +0,0 @@
import { generateToken } from "@autoplex/utils";
import { randomBytes } from "crypto";
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
@Entity()
export class RegisterToken extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
token!: string
/**
* Check if the provided token is valid
*/
public static async isValid(token: string) {
if (typeof token !== "string") {
return false;
}
return Boolean(token) && await RegisterToken.count({token}) > 0;
}
/**
* Create a new registration token and insert it into the database
*/
public static async generate() {
let token = new RegisterToken();
token.token = await generateToken();
return await token.save();
}
}

+ 0
- 113
services/request/src/server/database/entities/User.ts View File

@ -1,113 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany, OneToOne, JoinColumn, CreateDateColumn, MoreThanOrEqual } from "typeorm";
import bcrypt from "bcrypt";
import { MovieTicket } from "./MovieTicket";
import { MovieQuota } from "./MovieQuota";
import { IApiMovie } from "@common/api_schema";
import { DiscordAccount } from "./DiscordAccount";
@Entity()
export class User extends BaseEntity
{
@PrimaryGeneratedColumn()
id!: number;
@Column()
isAdmin!: boolean;
@Column({ length: 50 })
name!: string;
@Column({ length: 255 })
email!: string;
@Column({ type: "char", length: 60 })
password!: string;
@CreateDateColumn()
createdAt!: Date;
@OneToOne(() => MovieQuota, { nullable: true })
@JoinColumn()
quota!: MovieQuota | null;
@OneToMany(() => User, user => user.movieTickets)
movieTickets!: MovieTicket[];
@OneToMany(() => DiscordAccount, account => account.user)
discordAccounts!: DiscordAccount[];
/**
* Authenticate a user and return an auth token upon success
*/
public static async authenticate(email: string, password: string) {
let user = <User>await User.findOne({ email });
if (user === undefined || !(await bcrypt.compare(password, user.password))) {
return null;
}
return user;
}
/**
* Create a new user
*/
public static async createUser(name: string, email: string, password: string, quota: number|null = 5) {
let user = new User();
user.isAdmin = false;
user.name = name;
user.email = email;
user.password = await bcrypt.hash(password, 8);
// Create a quota if necessary
if (quota !== null) {
user.quota = new MovieQuota;
user.quota.moviesPerWeek = quota;
await user.quota.save();
}
return await user.save();
}
/**
* Determine the user's available quota
*/
public async availableQuota() {
let quota = await this.fetchQuota();
if (quota === null) {
return null;
}
let oneWeekAgo = new Date(Date.now() - 1000*60*60*24*7);
let numTicketsThisWeek = await MovieTicket.count({
user: this,
createdAt: MoreThanOrEqual(oneWeekAgo),
isCanceled: false
});
return quota.moviesPerWeek - numTicketsThisWeek;
}
/**
* Get the user's quota, fetching it if undefined
*/
public async fetchQuota() {
if (this.quota !== undefined) {
return this.quota;
}
let user = <User>await User.findOne(this.id, { relations: ["quota"] });
return user.quota;
}
/**
* Fetch active movie tickets for this user
*/
public async activeMovieTickets() {
let tickets = await MovieTicket.find({
where: { user: this, isCanceled: false, isFulfilled: false },
relations: ["info"]
});
return tickets.map(ticket => <IApiMovie>({
plexLink : null,
posterPath : ticket.info?.posterPath,
releaseDate: ticket.info?.releaseDate,
ticketId : ticket.id,
title : ticket.title,
tmdbId : ticket.tmdbId
}));
}
}

+ 0
- 11
services/request/src/server/database/entities/index.ts View File

@ -1,11 +0,0 @@
export * from "./DiscordAccount";
export * from "./DiscordChannel";
export * from "./DiscordLinkRequest";
export * from "./DiscordRequest";
export * from "./MovieInfo";
export * from "./MovieQuota";
export * from "./MovieTicket";
export * from "./MovieTorrent";
export * from "./PlexMovie";
export * from "./RegisterToken";
export * from "./User";

+ 0
- 22
services/request/src/server/database/index.ts View File

@ -1,22 +0,0 @@
import * as entities from "./entities";
import { readFile } from "fs/promises";
import { createConnection } from "typeorm";
export default async function connectToDatabase() {
// Fetch the database password from the secret file
let password = (await readFile(<string>process.env["DB_PASSWORD_FILE"])).toString().trim();
// Create the database connection
await createConnection({
type : <"mysql" | "mariadb">process.env["DB_TYPE"],
host : process.env["DB_HOST"],
port : parseInt(<string>process.env["DB_PORT"]),
username : process.env["DB_USER"],
password : password,
database : process.env["DB_DATABASE"],
// synchronize: process.env["NODE_ENV"] != "production",
synchronize: true, // Seems stable enough for my liking
entities : Object.values(entities),
migrations : ["src/migrations/*.ts"]
});
}

+ 9
- 3
services/request/src/server/services/Database.ts View File

@ -1,7 +1,8 @@
import { Connection, createConnection } from "typeorm";
import { Connection } from "typeorm";
import Service from "./Service";
import Application from "../Application";
import connectToDatabase from "@server/database";
import connectToDatabase from "@autoplex/database";
import { env, secret } from "@autoplex/utils";
export default class Database extends Service
{
@ -21,7 +22,12 @@ export default class Database extends Service
* Boot the database service
*/
public async boot() {
await connectToDatabase();
let host = env("DB_HOST");
let port = parseInt(env("DB_PORT"));
let username = env("DB_USER");
let password = await secret(env("DB_PASSWORD_FILE"));
let database = env("DB_DATABASE");
this.connection = await connectToDatabase(host, port, username, password, database);
}
/**


+ 4
- 5
services/request/src/server/services/DiscordBot.ts View File

@ -2,10 +2,8 @@ import Application from "../Application";
import Service from "./Service";
import { Client, Collection, Message, TextChannel, User as DiscordUser } from "discord.js";
import { env, formatImdbId, generateToken, secret } from "@autoplex/utils";
import { DiscordChannel } from "@server/database/entities/DiscordChannel";
import { DiscordAccount, MovieTicket } from "@server/database/entities";
import { DiscordAccount, DiscordChannel, DiscordLinkRequest, MovieTicket } from "@autoplex/database";
import MovieSearch from "./MovieSearch";
import { DiscordLinkRequest } from "@server/database/entities/DiscordLinkRequest";
/**
* The required role to perfrom administrative commands on the bot
@ -349,6 +347,7 @@ export default class DiscordBot extends Service
}
/**
* @TODO movie requests have been disabled while the project is reorganized
* Request a movie
*/
protected async cmdRequestMovie(account: DiscordAccount, message: Message) {
@ -390,7 +389,7 @@ export default class DiscordBot extends Service
return;
}
let info = await this.app.service<MovieSearch>("Movie Search").details(movie.id);
await MovieTicket.requestTmdb(account.user, info);
this.sendDm(message.author, `*${info.title}* has been requested successfully!`);
// await MovieTicket.requestTmdb(account.user, info);
this.sendDm(message.author, `*${info.title}* has been requested successfully! (TEMP DISABLE)`);
}
}

+ 1
- 1
services/request/src/server/services/MovieSearch.ts View File

@ -5,7 +5,7 @@ import { request } from "https";
import Service from "./Service";
import TvDb from "./TvDb";
import { IApiMovie, IApiMovieDetails, IApiPaginatedResponse } from "@common/api_schema";
import { MovieTicket, PlexMovie } from "@server/database/entities";
import { MovieTicket, PlexMovie } from "@autoplex/database";
import { IMovieSearchResult } from "@lib/tmdb/schema";
const CACHE_CLEAR_INTERVAL = 1000*60; // 60 seconds


+ 1
- 1
services/request/src/server/services/PlexLibrary.ts View File

@ -1,6 +1,6 @@
import Plex from "@lib/plex";
import Application from "@server/Application";
import { PlexMovie } from "@server/database/entities";
import { PlexMovie } from "@autoplex/database";
import { env, secret, sleep } from "@autoplex/utils";
import MovieSearch from "./MovieSearch";
import Service from "./Service";


+ 1
- 1
services/request/src/server/services/WebServer/middleware/auth.ts View File

@ -3,7 +3,7 @@ import Application from "@server/Application";
import jwt from "jsonwebtoken";
import { IteratorNext, MiddlewareRequest } from ".";
import { ITokenSchema } from "@common/api_schema";
import { User } from "@server/database/entities";
import { User } from "@autoplex/database";
/**
* Attempt to authenticate a client's JWT token


+ 21
- 4
services/request/src/server/services/WebServer/routes/api.ts View File

@ -4,7 +4,7 @@ import MovieSearch from "@server/services/MovieSearch";
import { auth } from "../middleware/auth";
import RouteRegisterFactory from "./RouteRegisterFactory";
import handle from "../requests";
import { MovieTicket } from "@server/database/entities";
import { MovieInfo, MovieTicket } from "@autoplex/database";
import RequestTmdbMovieRequest from "../requests/RequestTmdbMovieRequest";
/**
@ -68,15 +68,32 @@ export default function register(factory: RouteRegisterFactory, app: Application
return;
}
// Verify that the IMDb ID exists
let movieDetails = await app.service<MovieSearch>("Movie Search").details(tmdbId);
if (!movieDetails) { // @TODO This isn't correct I don't think
let movie = await app.service<MovieSearch>("Movie Search").details(tmdbId);
if (!movie) { // @TODO This isn't correct I don't think
reply.status(404);
reply.send({ satus: "Not found" });
return;
}
// Create the movie request ticket
let user = request.middlewareParams.auth.user;
let ticket = await MovieTicket.requestTmdb(user, movieDetails);
let info = new MovieInfo();
info.overview = movie.overview;
info.posterPath = movie.posterPath;
info.backdropPath = movie.backdropPath;
info.releaseDate = movie.releaseDate;
info.runtime = movie.runtime;
await info.save();
let ticket = new MovieTicket();
ticket.tmdbId = movie.tmdbId;
ticket.imdbId = movie.imdbId;
ticket.title = movie.title;
ticket.year = movie.releaseDate ? parseInt(movie.releaseDate.slice(0, 4)) : null;
ticket.user = user;
ticket.info = info;
await ticket.save();
app.service<SeekerIpc>("Seeker IPC").notifyMovieRequested(ticket.id);
return reply.send({ status: "Success", data: { ticket_id: ticket.id }});
}));


+ 1
- 1
services/request/src/server/services/WebServer/routes/auth.ts View File

@ -1,6 +1,6 @@
import jwt from "jsonwebtoken"
import Application from "@server/Application";
import { DiscordLinkRequest, RegisterToken, User } from "@server/database/entities";
import { DiscordLinkRequest, RegisterToken, User } from "@autoplex/database";
import LoginRequest, { ILoginFormBody } from "../requests/LoginRequest";
import RegisterRequest, { IRegisterFormBody } from "../requests/RegisterRequest";
import handle from "../requests";


+ 1
- 1
services/request/src/server/services/WebServer/validators.ts View File

@ -1,4 +1,4 @@
import { RegisterToken } from "@server/database/entities";
import { RegisterToken } from "@autoplex/database";
import validate from "validate.js";
interface IValidTokenOptions {


Loading…
Cancel
Save