initial commit: fork
This commit is contained in:
18
packages/api/index.ts
Normal file
18
packages/api/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
|
||||
|
||||
import type { AppRouter } from './src/root';
|
||||
|
||||
export { appRouter, type AppRouter } from './src/root';
|
||||
export { createTRPCContext } from './src/trpc';
|
||||
|
||||
/**
|
||||
* Inference helpers for input types
|
||||
* @example type HelloInput = RouterInputs['example']['hello']
|
||||
**/
|
||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
|
||||
/**
|
||||
* Inference helpers for output types
|
||||
* @example type HelloOutput = RouterOutputs['example']['hello']
|
||||
**/
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
36
packages/api/package.json
Normal file
36
packages/api/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@master-bot/api",
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@master-bot/auth": "^0.1.0",
|
||||
"@master-bot/db": "^0.1.0",
|
||||
"@t3-oss/env-core": "^0.6.0",
|
||||
"@trpc/client": "^10.37.1",
|
||||
"@trpc/server": "^10.37.1",
|
||||
"axios": "^1.4.0",
|
||||
"discord-api-types": "^0.37.51",
|
||||
"superjson": "1.13.1",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@master-bot/eslint-config": "^0.2.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.46.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@master-bot/eslint-config/base"
|
||||
]
|
||||
}
|
||||
}
|
34
packages/api/src/env.mjs
Normal file
34
packages/api/src/env.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import { createEnv } from '@t3-oss/env-core';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
clientPrefix: '',
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
||||
* built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
DATABASE_URL: z.string(),
|
||||
DISCORD_TOKEN: z.string(),
|
||||
DISCORD_CLIENT_ID: z.string(),
|
||||
DISCORD_CLIENT_SECRET: z.string()
|
||||
},
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
},
|
||||
/**
|
||||
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
DISCORD_TOKEN: process.env.DISCORD_TOKEN,
|
||||
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
},
|
||||
skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION
|
||||
});
|
27
packages/api/src/root.ts
Normal file
27
packages/api/src/root.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { channelRouter } from './routers/channel';
|
||||
import { commandRouter } from './routers/command';
|
||||
import { guildRouter } from './routers/guild';
|
||||
import { hubRouter } from './routers/hub';
|
||||
import { playlistRouter } from './routers/playlist';
|
||||
import { reminderRouter } from './routers/reminder';
|
||||
import { songRouter } from './routers/song';
|
||||
import { twitchRouter } from './routers/twitch';
|
||||
import { userRouter } from './routers/user';
|
||||
import { welcomeRouter } from './routers/welcome';
|
||||
import { createTRPCRouter } from './trpc';
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
user: userRouter,
|
||||
guild: guildRouter,
|
||||
playlist: playlistRouter,
|
||||
song: songRouter,
|
||||
twitch: twitchRouter,
|
||||
channel: channelRouter,
|
||||
welcome: welcomeRouter,
|
||||
command: commandRouter,
|
||||
hub: hubRouter,
|
||||
reminder: reminderRouter
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
42
packages/api/src/routers/channel.ts
Normal file
42
packages/api/src/routers/channel.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import type {
|
||||
APIGuildChannel,
|
||||
APIGuildTextChannel
|
||||
} from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export const channelRouter = createTRPCRouter({
|
||||
getAll: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const token = env.DISCORD_TOKEN;
|
||||
|
||||
// call the discord api with the token and the guildId and get all the guild's text channels
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const responseChannels =
|
||||
(await response.json()) as APIGuildChannel<any>[];
|
||||
|
||||
const channels: APIGuildTextChannel<0>[] = responseChannels.filter(
|
||||
channel => channel.type === 0
|
||||
);
|
||||
return { channels };
|
||||
})
|
||||
});
|
361
packages/api/src/routers/command.ts
Normal file
361
packages/api/src/routers/command.ts
Normal file
@ -0,0 +1,361 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type {
|
||||
APIApplicationCommandPermission,
|
||||
APIGuildChannel,
|
||||
APIRole,
|
||||
ChannelType
|
||||
} from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
import { discordApi } from '../utils/axiosWithRefresh';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export interface CommandType {
|
||||
code: number;
|
||||
id: string;
|
||||
applicationId: string;
|
||||
version: string;
|
||||
default_permission: string;
|
||||
default_member_permissions: null | string[];
|
||||
type: number;
|
||||
name: string;
|
||||
description: string;
|
||||
dm_permission: boolean;
|
||||
options: any[];
|
||||
}
|
||||
|
||||
export interface CommandPermissionsResponseOkay {
|
||||
id: string;
|
||||
application_id: string;
|
||||
guild_id: string;
|
||||
permissions: APIApplicationCommandPermission[];
|
||||
}
|
||||
|
||||
export interface CommandPermissionsResponseNotOkay {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export const commandRouter = createTRPCRouter({
|
||||
getDisabledCommands: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
disabledCommands: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
return { disabledCommands: guild.disabledCommands };
|
||||
}),
|
||||
getCommands: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async () => {
|
||||
try {
|
||||
const token = env.DISCORD_TOKEN;
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/applications/${env.DISCORD_CLIENT_ID}/commands`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const commands = (await response.json()) as CommandType[];
|
||||
|
||||
return { commands };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getCommandAndGuildChannels: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const token = env.DISCORD_TOKEN;
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId } = input;
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const [
|
||||
guildChannelsResponse,
|
||||
guildRolesResponse,
|
||||
commandResponse,
|
||||
permissionsResponse
|
||||
] = await Promise.all([
|
||||
fetch(`https://discord.com/api/guilds/${guildId}/channels`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}).then((res: any) => res.json()) as Promise<unknown>,
|
||||
fetch(`https://discord.com/api/guilds/${guildId}/roles`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}).then((res: any) => res.json()) as Promise<unknown>,
|
||||
fetch(
|
||||
`https://discord.com/api/applications/${clientID}/commands/${commandId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
).then((res: any) => res.json()) as Promise<unknown>,
|
||||
discordApi
|
||||
.get(
|
||||
`https://discord.com/api/v10/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res: any) => res.data)
|
||||
]);
|
||||
|
||||
const channels =
|
||||
guildChannelsResponse as APIGuildChannel<ChannelType>[];
|
||||
const roles = guildRolesResponse as APIRole[];
|
||||
const command = commandResponse as CommandType;
|
||||
const permissions = permissionsResponse;
|
||||
|
||||
return { channels, roles, command, permissions };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getCommandPermissions: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId } = input;
|
||||
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const command = await response.json();
|
||||
if (!command) throw new Error();
|
||||
|
||||
return { command };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
editCommandPermissions: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string(),
|
||||
permissions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.number(),
|
||||
permission: z.boolean()
|
||||
})
|
||||
),
|
||||
type: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const clientID = env.DISCORD_CLIENT_ID;
|
||||
const { guildId, commandId, permissions, type } = input;
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
},
|
||||
select: {
|
||||
access_token: true,
|
||||
providerAccountId: true,
|
||||
user: {
|
||||
select: {
|
||||
discordId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const everyone = {
|
||||
id: guildId,
|
||||
type: 1,
|
||||
permission: type === 'allow' ? true : false
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/applications/${clientID}/guilds/${guildId}/commands/${commandId}/permissions`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${account?.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ permissions: [everyone, ...permissions] })
|
||||
}
|
||||
);
|
||||
const command = await response.json();
|
||||
if (!command) throw new Error();
|
||||
|
||||
return { command };
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds'
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
toggleCommand: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
commandId: z.string(),
|
||||
status: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, commandId, status } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
disabledCommands: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
let updatedGuild;
|
||||
|
||||
if (status) {
|
||||
updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
disabledCommands: {
|
||||
set: [...guild.disabledCommands, commandId]
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
disabledCommands: {
|
||||
set: guild?.disabledCommands.filter(cid => cid !== commandId)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { updatedGuild };
|
||||
})
|
||||
});
|
178
packages/api/src/routers/guild.ts
Normal file
178
packages/api/src/routers/guild.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import type { APIGuild, APIRole } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
import { discordApi } from '../utils/axiosWithRefresh';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
function getUserGuilds(
|
||||
access_token: string,
|
||||
refresh_token: string,
|
||||
user_id: string
|
||||
) {
|
||||
return discordApi.get('https://discord.com/api/v10/users/@me/guilds', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
// set user agent
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
|
||||
'X-User-Id': user_id,
|
||||
'X-Refresh-Token': refresh_token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const guildRouter = createTRPCRouter({
|
||||
getGuild: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
ownerId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, ownerId, name } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.upsert({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: id,
|
||||
ownerId: ownerId,
|
||||
volume: 100,
|
||||
name: name
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.delete({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
updateVolume: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
volume: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, volume } = input;
|
||||
|
||||
await ctx.prisma.guild.update({
|
||||
where: { id: guildId },
|
||||
data: { volume }
|
||||
});
|
||||
}),
|
||||
getRoles: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
message: 'Not Authenticated',
|
||||
code: 'UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://discord.com/api/guilds/${guildId}/roles`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const roles = (await response.json()) as APIRole[];
|
||||
|
||||
return { roles };
|
||||
}),
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const account = await ctx.prisma.account.findFirst({
|
||||
where: {
|
||||
userId: ctx.session?.user?.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!account?.access_token || !account?.refresh_token) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Account not found'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const dbGuilds = await ctx.prisma.guild.findMany({
|
||||
where: {
|
||||
ownerId: account.providerAccountId
|
||||
}
|
||||
});
|
||||
|
||||
const response = await getUserGuilds(
|
||||
account.access_token,
|
||||
account.refresh_token,
|
||||
account.userId
|
||||
);
|
||||
|
||||
// get the guilds from response data
|
||||
const apiGuilds = response.data as APIGuild[];
|
||||
|
||||
const apiGuildsOwns = apiGuilds.filter(guild => guild.owner);
|
||||
|
||||
return {
|
||||
apiGuilds: apiGuildsOwns,
|
||||
dbGuilds,
|
||||
apiGuildsIds: apiGuildsOwns.map(guild => guild.id),
|
||||
dbGuildsIds: dbGuilds.map(guild => guild.id)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Something went wrong when trying to fetch guilds from DB'
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
203
packages/api/src/routers/hub.ts
Normal file
203
packages/api/src/routers/hub.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { getFetch } from '@trpc/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
const fetch = getFetch();
|
||||
|
||||
export const hubRouter = createTRPCRouter({
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, name } = input;
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
let parent;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
type: 4
|
||||
})
|
||||
}
|
||||
);
|
||||
parent = (await response.json()) as any;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new TRPCError({
|
||||
message: 'Could not create channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
let hubChannel;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://discordapp.com/api/guilds/${guildId}/channels`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'Join To Create',
|
||||
type: 2,
|
||||
parent_id: parent.id
|
||||
})
|
||||
}
|
||||
);
|
||||
hubChannel = (await response.json()) as any;
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
message: 'Could not create channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGuild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
hub: parent.id,
|
||||
hubChannel: hubChannel.id
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
guild: updatedGuild
|
||||
};
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
hub: true,
|
||||
hubChannel: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!guild) {
|
||||
throw new TRPCError({
|
||||
message: 'Guild not found',
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
Promise.all([
|
||||
fetch(`https://discordapp.com/api/channels/${guild.hubChannel}`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
},
|
||||
method: 'DELETE'
|
||||
}),
|
||||
fetch(`https://discordapp.com/api/channels/${guild.hub}`, {
|
||||
headers: {
|
||||
Authorization: `Bot ${token}`
|
||||
},
|
||||
method: 'DELETE'
|
||||
})
|
||||
]).then(async () => {
|
||||
await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
hub: null,
|
||||
hubChannel: null
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new TRPCError({
|
||||
message: 'Could not delete channel',
|
||||
code: 'INTERNAL_SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
}),
|
||||
getTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
ownerId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId, ownerId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.findFirst({
|
||||
where: {
|
||||
guildId,
|
||||
ownerId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
}),
|
||||
createTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
ownerId: z.string(),
|
||||
channelId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, ownerId, channelId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.create({
|
||||
data: {
|
||||
guildId,
|
||||
ownerId,
|
||||
id: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
}),
|
||||
deleteTempChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { channelId } = input;
|
||||
|
||||
const tempChannel = await ctx.prisma.tempChannel.delete({
|
||||
where: {
|
||||
id: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { tempChannel };
|
||||
})
|
||||
});
|
33
packages/api/src/routers/index.ts
Normal file
33
packages/api/src/routers/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createTRPCRouter } from '../trpc';
|
||||
import { channelRouter } from './channel';
|
||||
import { commandRouter } from './command';
|
||||
import { guildRouter } from './guild';
|
||||
import { hubRouter } from './hub';
|
||||
import { playlistRouter } from './playlist';
|
||||
import { reminderRouter } from './reminder';
|
||||
import { songRouter } from './song';
|
||||
import { twitchRouter } from './twitch';
|
||||
import { userRouter } from './user';
|
||||
import { welcomeRouter } from './welcome';
|
||||
|
||||
/**
|
||||
* Create your application's root router
|
||||
* If you want to use SSG, you need export this
|
||||
* @link https://trpc.io/docs/ssg
|
||||
* @link https://trpc.io/docs/router
|
||||
*/
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
user: userRouter,
|
||||
guild: guildRouter,
|
||||
playlist: playlistRouter,
|
||||
song: songRouter,
|
||||
twitch: twitchRouter,
|
||||
channel: channelRouter,
|
||||
welcome: welcomeRouter,
|
||||
command: commandRouter,
|
||||
hub: hubRouter,
|
||||
reminder: reminderRouter
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
93
packages/api/src/routers/playlist.ts
Normal file
93
packages/api/src/routers/playlist.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const playlistRouter = createTRPCRouter({
|
||||
getPlaylist: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
name
|
||||
},
|
||||
include: {
|
||||
songs: true
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
}),
|
||||
getAll: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const playlists = await ctx.prisma.playlist.findMany({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
include: {
|
||||
songs: true
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return { playlists };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.create({
|
||||
data: {
|
||||
name,
|
||||
user: {
|
||||
connect: {
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, name } = input;
|
||||
|
||||
const playlist = await ctx.prisma.playlist.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
name
|
||||
}
|
||||
});
|
||||
|
||||
return { playlist };
|
||||
})
|
||||
});
|
103
packages/api/src/routers/reminder.ts
Normal file
103
packages/api/src/routers/reminder.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const reminderRouter = createTRPCRouter({
|
||||
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||
const reminders = await ctx.prisma.reminder.findMany();
|
||||
|
||||
return { reminders };
|
||||
}),
|
||||
getReminder: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event } = input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
event
|
||||
},
|
||||
include: { user: true }
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
}),
|
||||
getByUserId: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const reminders = await ctx.prisma.reminder.findMany({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
select: {
|
||||
event: true,
|
||||
dateTime: true,
|
||||
description: true
|
||||
},
|
||||
orderBy: {
|
||||
id: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
return { reminders };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string(),
|
||||
description: z.nullable(z.string()),
|
||||
dateTime: z.string(),
|
||||
repeat: z.nullable(z.string()),
|
||||
timeOffset: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event, description, dateTime, repeat, timeOffset } =
|
||||
input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.create({
|
||||
data: {
|
||||
event,
|
||||
description,
|
||||
dateTime,
|
||||
repeat,
|
||||
timeOffset,
|
||||
user: { connect: { discordId: userId } }
|
||||
}
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
event: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, event } = input;
|
||||
|
||||
const reminder = await ctx.prisma.reminder.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
event
|
||||
}
|
||||
});
|
||||
|
||||
return { reminder };
|
||||
})
|
||||
});
|
38
packages/api/src/routers/song.ts
Normal file
38
packages/api/src/routers/song.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const songRouter = createTRPCRouter({
|
||||
createMany: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
songs: z.array(z.any())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { songs } = input;
|
||||
|
||||
const songsCreated = await ctx.prisma.song.createMany({
|
||||
data: songs
|
||||
});
|
||||
|
||||
return { songsCreated };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const song = await ctx.prisma.song.delete({
|
||||
where: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
|
||||
return { song };
|
||||
})
|
||||
});
|
106
packages/api/src/routers/twitch.ts
Normal file
106
packages/api/src/routers/twitch.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const twitchRouter = createTRPCRouter({
|
||||
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||
const notifications = await ctx.prisma.twitchNotify.findMany();
|
||||
|
||||
return { notifications };
|
||||
}),
|
||||
findUserById: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.findFirst({
|
||||
where: {
|
||||
twitchId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
userImage: z.string(),
|
||||
channelId: z.string(),
|
||||
sendTo: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, userImage, channelId, sendTo } = input;
|
||||
await ctx.prisma.twitchNotify.upsert({
|
||||
create: {
|
||||
twitchId: userId,
|
||||
channelIds: [channelId],
|
||||
logo: userImage,
|
||||
sent: false
|
||||
},
|
||||
update: { channelIds: sendTo },
|
||||
where: { twitchId: userId }
|
||||
});
|
||||
}),
|
||||
updateNotification: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
channelIds: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId, channelIds } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.update({
|
||||
where: {
|
||||
twitchId: userId
|
||||
},
|
||||
data: {
|
||||
channelIds
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { userId } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.delete({
|
||||
where: {
|
||||
twitchId: userId
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}),
|
||||
updateNotificationStatus: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
live: z.boolean(),
|
||||
sent: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { live, sent, userId } = input;
|
||||
|
||||
const notification = await ctx.prisma.twitchNotify.update({
|
||||
where: { twitchId: userId },
|
||||
data: { live, sent }
|
||||
});
|
||||
|
||||
return { notification };
|
||||
})
|
||||
});
|
80
packages/api/src/routers/user.ts
Normal file
80
packages/api/src/routers/user.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
getUserById: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
discordId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { user };
|
||||
}),
|
||||
create: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, name } = input;
|
||||
const user = await ctx.prisma.user.upsert({
|
||||
where: {
|
||||
discordId: id
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
discordId: id,
|
||||
name
|
||||
}
|
||||
});
|
||||
return { user };
|
||||
}),
|
||||
delete: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id } = input;
|
||||
|
||||
const user = await ctx.prisma.user.delete({
|
||||
where: {
|
||||
discordId: id
|
||||
}
|
||||
});
|
||||
|
||||
return { user };
|
||||
}),
|
||||
updateTimeOffset: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
timeOffset: z.number()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, timeOffset } = input;
|
||||
const userTime = await ctx.prisma.user.update({
|
||||
where: {
|
||||
discordId: id
|
||||
},
|
||||
data: { timeOffset: timeOffset },
|
||||
select: { timeOffset: true }
|
||||
});
|
||||
|
||||
return { userTime };
|
||||
})
|
||||
});
|
128
packages/api/src/routers/welcome.ts
Normal file
128
packages/api/src/routers/welcome.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const welcomeRouter = createTRPCRouter({
|
||||
getMessage: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: guild?.welcomeMessage
|
||||
};
|
||||
}),
|
||||
setMessage: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
message: z.string().min(4).max(100),
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { message, guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessage: message
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
setChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { channelId, guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessageChannel: channelId
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
getChannel: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
welcomeMessageChannel: true
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
getStatus: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { guildId } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.findUnique({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
select: {
|
||||
welcomeMessageEnabled: true
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
}),
|
||||
toggle: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
guildId: z.string(),
|
||||
status: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { guildId, status } = input;
|
||||
|
||||
const guild = await ctx.prisma.guild.update({
|
||||
where: {
|
||||
id: guildId
|
||||
},
|
||||
data: {
|
||||
welcomeMessageEnabled: status
|
||||
}
|
||||
});
|
||||
|
||||
return { guild };
|
||||
})
|
||||
});
|
131
packages/api/src/trpc.ts
Normal file
131
packages/api/src/trpc.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||
* 1. You want to modify request context (see Part 1)
|
||||
* 2. You want to create a new middleware or type of procedure (see Part 3)
|
||||
*
|
||||
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
|
||||
* The pieces you will need to use are documented accordingly near the end
|
||||
*/
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { auth } from '@master-bot/auth';
|
||||
import type { Session } from '@master-bot/auth';
|
||||
import { prisma } from '@master-bot/db';
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
*
|
||||
* This section defines the "contexts" that are available in the backend API
|
||||
*
|
||||
* These allow you to access things like the database, the session, etc, when
|
||||
* processing a request
|
||||
*
|
||||
*/
|
||||
interface CreateContextOptions {
|
||||
session: Session | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use
|
||||
* it, you can export it from here
|
||||
*
|
||||
* Examples of things you may need it for:
|
||||
* - testing, so we dont have to mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||
*/
|
||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
return {
|
||||
session: opts.session,
|
||||
prisma
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you'll use in your router. It will be used to
|
||||
* process every request that goes through your tRPC endpoint
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: {
|
||||
req?: Request;
|
||||
auth?: Session;
|
||||
}) => {
|
||||
const session = opts.auth ?? (await auth());
|
||||
// const source = opts.req?.headers.get('x-trpc-source') ?? 'unknown';
|
||||
|
||||
// console.log('>>> tRPC Request from', source, 'by', session?.user);
|
||||
|
||||
return createInnerTRPCContext({
|
||||
session
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
* This is where the trpc api is initialized, connecting the context and
|
||||
* transformer
|
||||
*/
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
*
|
||||
* These are the pieces you use to build your tRPC API. You should import these
|
||||
* a lot in the /src/server/api/routers folder
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is how you create new routers and subrouters in your tRPC API
|
||||
* @see https://trpc.io/docs/router
|
||||
*/
|
||||
export const createTRPCRouter = t.router;
|
||||
|
||||
/**
|
||||
* Public (unauthed) procedure
|
||||
*
|
||||
* This is the base piece you use to build new queries and mutations on your
|
||||
* tRPC API. It does not guarantee that a user querying is authorized, but you
|
||||
* can still access user session data if they are logged in
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/**
|
||||
* Reusable middleware that enforces users are logged in before running the
|
||||
* procedure
|
||||
*/
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Protected (authed) procedure
|
||||
*
|
||||
* If you want a query or mutation to ONLY be accessible to logged in users, use
|
||||
* this. It verifies the session is valid and guarantees ctx.session.user is not
|
||||
* null
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
|
137
packages/api/src/utils/axiosWithRefresh.ts
Normal file
137
packages/api/src/utils/axiosWithRefresh.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import axios, { type AxiosError } from 'axios';
|
||||
|
||||
import { prisma } from '@master-bot/db';
|
||||
|
||||
import { env } from '../env.mjs';
|
||||
|
||||
// const baseURL = 'https://discord.com/api/v10'; // Update to the appropriate Discord API version
|
||||
|
||||
const discordApi = axios.create();
|
||||
|
||||
async function refreshAccessToken(refreshToken: string, userId: string) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
client_id: env.DISCORD_CLIENT_ID,
|
||||
client_secret: env.DISCORD_CLIENT_SECRET,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await discordApi.post(
|
||||
'https://discord.com/api/v10/oauth2/token',
|
||||
params,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('error in refreshing token', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const {
|
||||
access_token,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_in
|
||||
} = response.data;
|
||||
|
||||
// Update the access and refresh tokens in the database
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
data: {
|
||||
access_token,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_at: expires_in
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: access_token,
|
||||
refreshToken: newRefreshToken,
|
||||
expiresIn: expires_in
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error refreshing access token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUserTokens(
|
||||
newTokens: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
},
|
||||
userId: string
|
||||
) {
|
||||
try {
|
||||
const updatedAccount = await prisma.account.update({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
data: {
|
||||
access_token: newTokens.accessToken,
|
||||
refresh_token: newTokens.refreshToken,
|
||||
expires_at: newTokens.expiresIn
|
||||
}
|
||||
});
|
||||
|
||||
return updatedAccount;
|
||||
} catch (error) {
|
||||
console.error('Error updating user tokens:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
discordApi.interceptors.response.use(
|
||||
response => {
|
||||
// if response is ok return it
|
||||
return response;
|
||||
},
|
||||
async (error: Error | AxiosError) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.code === 'ERR_BAD_REQUEST') {
|
||||
const { 'X-User-Id': userId, 'X-Refresh-Token': refreshToken } =
|
||||
originalRequest!.headers;
|
||||
|
||||
if (typeof userId !== 'string' || typeof refreshToken !== 'string') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const newTokens = await refreshAccessToken(refreshToken, userId);
|
||||
|
||||
if (!newTokens?.accessToken) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Save the new access token and refresh token to the DB
|
||||
try {
|
||||
await updateUserTokens(newTokens, userId);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Set the new access token in the header and retry the original request
|
||||
originalRequest!.headers[
|
||||
'Authorization'
|
||||
] = `Bearer ${newTokens.accessToken}`;
|
||||
|
||||
return discordApi(originalRequest!);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { discordApi };
|
4
packages/api/tsconfig.json
Normal file
4
packages/api/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "*.ts"]
|
||||
}
|
28
packages/auth/env.mjs
Normal file
28
packages/auth/env.mjs
Normal file
@ -0,0 +1,28 @@
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
DISCORD_CLIENT_ID: z.string().min(1),
|
||||
DISCORD_CLIENT_SECRET: z.string().min(1),
|
||||
NEXTAUTH_SECRET:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? z.string().min(1)
|
||||
: z.string().min(1).optional(),
|
||||
NEXTAUTH_URL: z.preprocess(
|
||||
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
|
||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
||||
str => process.env.VERCEL_URL ?? str,
|
||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
||||
process.env.VERCEL ? z.string() : z.string().url()
|
||||
)
|
||||
},
|
||||
client: {},
|
||||
runtimeEnv: {
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
|
||||
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET
|
||||
},
|
||||
skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION
|
||||
});
|
134
packages/auth/index.ts
Normal file
134
packages/auth/index.ts
Normal file
@ -0,0 +1,134 @@
|
||||
// @ts-nocheck
|
||||
import Discord, { type DiscordProfile } from '@auth/core/providers/discord';
|
||||
import type { DefaultSession as DefaultSessionType } from '@auth/core/types';
|
||||
import { PrismaAdapter } from '@auth/prisma-adapter';
|
||||
import { prisma } from '@master-bot/db';
|
||||
import NextAuth from 'next-auth';
|
||||
|
||||
import { env } from './env.mjs';
|
||||
|
||||
export type { Session } from 'next-auth';
|
||||
|
||||
// Update this whenever adding new providers so that the client can
|
||||
export const providers = ['discord'] as const;
|
||||
export type OAuthProviders = (typeof providers)[number];
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
discordId: string;
|
||||
} & DefaultSessionType['user'];
|
||||
}
|
||||
}
|
||||
|
||||
const scope = ['identify', 'guilds', 'email'].join(' ');
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
CSRF_experimental
|
||||
} = NextAuth({
|
||||
adapter: {
|
||||
...PrismaAdapter(prisma),
|
||||
createUser: async data => {
|
||||
return await prisma.user.upsert({
|
||||
where: { discordId: data.discordId },
|
||||
update: data,
|
||||
create: data
|
||||
});
|
||||
}
|
||||
},
|
||||
providers: [
|
||||
Discord({
|
||||
clientId: env.DISCORD_CLIENT_ID,
|
||||
clientSecret: env.DISCORD_CLIENT_SECRET,
|
||||
authorization: {
|
||||
params: {
|
||||
scope
|
||||
}
|
||||
},
|
||||
profile(profile: DiscordProfile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
email: profile.email,
|
||||
image: profile.avatar,
|
||||
discordId: profile.id
|
||||
};
|
||||
}
|
||||
})
|
||||
],
|
||||
callbacks: {
|
||||
session: async ({ session, user }) => {
|
||||
const account = await prisma.account.findUnique({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (account?.expires_at * 1000 < Date.now()) {
|
||||
// refresh token
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://discord.com/api/v10/oauth2/token',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: env.DISCORD_CLIENT_ID,
|
||||
client_secret: env.DISCORD_CLIENT_SECRET,
|
||||
refresh_token: account.refresh_token
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh token');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
await prisma.account.update({
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
data: {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
expires_at: data.expires_in
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
discordId: user.discordId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @TODO - if you wanna have auth on the edge
|
||||
// jwt: ({ token, profile }) => {
|
||||
// if (profile?.id) {
|
||||
// token.id = profile.id;
|
||||
// token.image = profile.picture;
|
||||
// }
|
||||
// return token;
|
||||
// },
|
||||
|
||||
// @TODO
|
||||
// authorized({ request, auth }) {
|
||||
// return !!auth?.user
|
||||
// }
|
||||
}
|
||||
});
|
35
packages/auth/package.json
Normal file
35
packages/auth/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@master-bot/auth",
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.10.0",
|
||||
"@auth/prisma-adapter": "^1.0.1",
|
||||
"@master-bot/db": "^0.1.0",
|
||||
"@t3-oss/env-nextjs": "^0.6.0",
|
||||
"next": "^13.4.12",
|
||||
"next-auth": "^0.0.0-manual.b53ca00b",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@master-bot/eslint-config": "^0.2.0",
|
||||
"eslint": "^8.46.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@master-bot/eslint-config/base"
|
||||
]
|
||||
}
|
||||
}
|
4
packages/auth/tsconfig.json
Normal file
4
packages/auth/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "*.ts", "env.mjs"]
|
||||
}
|
53
packages/config/eslint/base.js
Normal file
53
packages/config/eslint/base.js
Normal file
@ -0,0 +1,53 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
const config = {
|
||||
extends: [
|
||||
'turbo',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:@typescript-eslint/stylistic-type-checked',
|
||||
'prettier'
|
||||
],
|
||||
env: {
|
||||
es2022: true,
|
||||
node: true
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: true
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' }
|
||||
],
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
2,
|
||||
{ checksVoidReturn: { attributes: false } }
|
||||
],
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off'
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/.eslintrc.cjs',
|
||||
'**/*.config.js',
|
||||
'**/*.config.cjs',
|
||||
'packages/config/**',
|
||||
'.next',
|
||||
'dist',
|
||||
'pnpm-lock.yaml'
|
||||
],
|
||||
reportUnusedDisableDirectives: true
|
||||
};
|
||||
|
||||
module.exports = config;
|
9
packages/config/eslint/nextjs.js
Normal file
9
packages/config/eslint/nextjs.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
extends: ['plugin:@next/next/recommended'],
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
26
packages/config/eslint/package.json
Normal file
26
packages/config/eslint/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@master-bot/eslint-config",
|
||||
"version": "0.2.0",
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"./base.js",
|
||||
"./nextjs.js",
|
||||
"./react.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "^13.4.12",
|
||||
"@types/eslint": "^8.44.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-config-turbo": "^1.10.12",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.46.0",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
24
packages/config/eslint/react.js
vendored
Normal file
24
packages/config/eslint/react.js
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended'
|
||||
],
|
||||
rules: {
|
||||
'react/prop-types': 'off'
|
||||
},
|
||||
globals: {
|
||||
React: 'writable'
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
env: {
|
||||
browser: true
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
9
packages/config/tailwind/index.ts
Normal file
9
packages/config/tailwind/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: [''],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
} satisfies Config;
|
15
packages/config/tailwind/package.json
Normal file
15
packages/config/tailwind/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@master-bot/tailwind-config",
|
||||
"version": "0.1.0",
|
||||
"main": "index.ts",
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"index.ts",
|
||||
"postcss.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.3.3"
|
||||
}
|
||||
}
|
6
packages/config/tailwind/postcss.js
Normal file
6
packages/config/tailwind/postcss.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
16
packages/db/index.ts
Normal file
16
packages/db/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export * from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as { prisma?: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient({
|
||||
log:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? ['query', 'error', 'warn']
|
||||
: ['error']
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
23
packages/db/package.json
Normal file
23
packages/db/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@master-bot/db",
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"db:generate": "pnpm with-env prisma generate",
|
||||
"db:push": "pnpm with-env prisma db push --skip-generate",
|
||||
"db:reset": "pnpm with-env prisma db push --force-reset",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.6",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
133
packages/db/prisma/schema.prisma
Normal file
133
packages/db/prisma/schema.prisma
Normal file
@ -0,0 +1,133 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DB_URL")
|
||||
}
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
discordId String @unique
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
account Account?
|
||||
sessions Session[]
|
||||
playlists Playlist[]
|
||||
guilds Guild[]
|
||||
reminders Reminder[]
|
||||
timeOffset Int?
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model Song {
|
||||
id Int @id @default(autoincrement())
|
||||
length Int
|
||||
track String
|
||||
identifier String
|
||||
author String
|
||||
isStream Boolean
|
||||
position Int
|
||||
title String
|
||||
uri String
|
||||
isSeekable Boolean
|
||||
sourceName String
|
||||
thumbnail String
|
||||
added Int
|
||||
playlistId Int
|
||||
playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Playlist {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
name String
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
songs Song[]
|
||||
}
|
||||
|
||||
model Guild {
|
||||
id String @id
|
||||
name String
|
||||
added DateTime @default(now())
|
||||
volume Int @default(100)
|
||||
notifyList String[]
|
||||
ownerId String
|
||||
owner User @relation(fields: [ownerId], references: [discordId])
|
||||
// Settings
|
||||
disabledCommands String[] @map("disabled_commands")
|
||||
logChannel String? @map("log_channel")
|
||||
welcomeMessageChannel String? @map("welcome_message_channel")
|
||||
welcomeMessage String? @map("welcome_message")
|
||||
welcomeMessageEnabled Boolean @default(false) @map("welcome_message_enabled")
|
||||
// Temp Channels
|
||||
hub String?
|
||||
hubChannel String? @map("hub_channel") // The channel that users enter to get redirected
|
||||
tempChannels TempChannel[]
|
||||
}
|
||||
|
||||
model TempChannel {
|
||||
id String @id
|
||||
guildId String
|
||||
guild Guild @relation(fields: [guildId], references: [id])
|
||||
ownerId String @unique
|
||||
}
|
||||
|
||||
model TwitchNotify {
|
||||
twitchId String @id
|
||||
logo String
|
||||
live Boolean @default(false)
|
||||
channelIds String[]
|
||||
sent Boolean
|
||||
}
|
||||
|
||||
model Reminder {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
repeat String?
|
||||
event String
|
||||
description String?
|
||||
dateTime String
|
||||
userId String
|
||||
user User? @relation(fields: [userId], references: [discordId])
|
||||
timeOffset Int
|
||||
}
|
4
packages/db/tsconfig.json
Normal file
4
packages/db/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["index.ts"]
|
||||
}
|
Reference in New Issue
Block a user