initial commit: fork

This commit is contained in:
2023-10-27 10:31:20 +02:00
commit a52dbbc103
195 changed files with 17484 additions and 0 deletions

18
packages/api/index.ts Normal file
View 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
View 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
View 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
View 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;

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

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

View 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'
});
}
})
});

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

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

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

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

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

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

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

View 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
View 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);

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

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "*.ts"]
}