TwitchClone / src / server / api / routers / lambda.ts
lambda.ts
Raw
// /src/server/api/routers/lambda.ts
import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { v4 } from "uuid";
// import { pusher } from "../pusher";
import Pusher from "pusher";

export const pusher = new Pusher({
  appId: process.env.PUSHER_APP_ID as string,
  key: process.env.PUSHER_APP_KEY as string,
  secret: process.env.PUSHER_APP_SECRET as string,
  cluster: process.env.PUSHER_APP_CLUSTER as string,
  useTLS: process.env.PUSHER_APP_TLS as unknown as boolean,
});

const CreateUserInputSchema = z.object({
  name: z.string().min(1).max(280),
  email: z.string().email(),
  password: z.string().min(1).max(280),
  userName: z.string().min(1).max(45),
  image: z.string(),
  createdAt: z.string(),
  roomId: z.string(),
});

const SeedUserVideosInputSchema = z.object({
  userId: z.string(),
  videos: z.array(
    z.object({
      id: z.string(),
      playbackId: z.string(),
      streamKey: z.string(),
      createdAt: z.string(),
      updatedAt: z.string(),
      userId: z.string(),
    })
  ),
});

const AddDemoUsersInputSchema = z.array(
  z.object({
    name: z.string().min(1).max(280),
    email: z.string(),
    image: z.string().optional(),
    id: z.string(),
  })
);

const UpdateUserSettingsInputSchema = z.object({
  userId: z.string(),
  themeColor: z.string(),
});

const SendMessageInputSchema = z.object({
  content: z.string().min(1).max(280),
  roomId: z.string(),
  reply: z.boolean().optional(),
  replyToId: z.string().optional(),
});

const CreateUserVideoInputSchema = z.object({
  playbackId: z.string().optional(),
  streamKey: z.string(),
  userId: z.string(),
  roomId: z.string(),
  assetId: z.string(),
  uuid: z.string(),
});

const CreateVideoInputSchema = z.object({
  playbackId: z.string(),
  streamKey: z.string(),
  assetId: z.string(),
});

const VideoUpdateSchema = z.object({
  id: z.string(), //of video
  playbackId: z.string(), //of video
  title: z.string(), //roomId will be title
  creatorId: z.string(), //of video
});

export const lambdaRouter = createTRPCRouter({
  updateFollowing: publicProcedure
    .input(
      z.object({
        email: z.string(),
        userId: z.string(),
        action: z.enum(["add", "remove", "seed"]),
        following: z.array(
          z.object({
            id: z.string(),
            followerId: z.string(),
            followingId: z.string(),
            createdAt: z.date(),
            // createdAt: z.string(),
          })
        ),
      })
    )
    .mutation(async ({ input, ctx }) => {
      const { session } = ctx;
      const { email, following } = input;
      console.log("input", input);
      if (
        !session ||
        !session.user ||
        !input ||
        !ctx ||
        !ctx.session ||
        !ctx.session.user
      ) {
        throw new Error("UNAUTHORIZED");
      }
      try {
        const currentUser = await ctx.prisma.user.findUnique({
          where: { email },
          include: { following: true },
        });

        if (!currentUser) {
          throw new Error("User not found");
        }
        console.log("currentUser", currentUser);
        const updateFollowingActions = {
          seed: async () => {
            // Disconnect all existing following relations
            const disconnectPromises = currentUser.following.map((following) =>
              ctx.prisma.userFollows.delete({
                where: { id: following.id },
              })
            );
            await Promise.all(disconnectPromises);
            // Connect existing following relations
            const newFollowingPromises = following.map(async (follow) => {
              const existingFollow = await ctx.prisma.userFollows.findFirst({
                where: {
                  followerId: currentUser.id,
                  followingId: follow.followingId,
                },
              });

              if (!existingFollow) {
                return ctx.prisma.userFollows.upsert({
                  where: {
                    followerId_followingId: {
                      followerId: currentUser.id,
                      followingId: follow.followingId,
                    },
                  },
                  update: {
                    createdAt: follow.createdAt,
                  },
                  create: {
                    followerId: currentUser.id,
                    followingId: follow.followingId,
                    createdAt: follow.createdAt,
                  },
                });
              } else {
                console.log("Skipped duplicate follow relationship");
              }
            });
            await Promise.all(newFollowingPromises);
          },
          remove: async () => {
            console.log("removing", following);
            await ctx.prisma.userFollows.deleteMany({
              where: {
                id: { in: following.map((follow) => follow.id) },
                followerId: currentUser.id,
              },
            });
          },
          add: async () => {
            console.log(following, "following");
            // Remove the specified follower
            if (ctx.session == null || !ctx.session.user) {
              throw new Error("UNAUTHORIZED");
            }
            const addFollowerPromises = following.map((following) =>
              ctx.prisma.userFollows.upsert({
                where: {
                  followerId_followingId: {
                    followerId: following.followerId,
                    followingId: following.followingId,
                  },
                },
                update: {
                  createdAt: following.createdAt,
                },
                create: {
                  followerId: following.followerId,
                  followingId: following.followingId,
                  createdAt: following.createdAt,
                },
              })
            );
            await Promise.all(addFollowerPromises);
          },
        };
        console.log("input.action", input.action);
        await updateFollowingActions[input.action]();

        const updatedUser = await ctx.prisma.user.findUnique({
          where: { email },
          include: { following: true },
        });
        console.log("updatedUser", updatedUser);

        return updatedUser;
      } catch (e) {
        console.error("Error updating following in DB:", e);
        throw e;
      }
    }),

  seedUserVideos: protectedProcedure
    .input(SeedUserVideosInputSchema)
    .mutation(async ({ input, ctx }) => {
      const { videos } = input;
      const seededVideos = await Promise.all(
        videos.map((video) =>
          ctx.prisma.content.create({
            data: {
              userId: ctx.session.user.id,
              playbackId: video.playbackId,
              streamKey: video.streamKey,
              roomId: v4(),
            },
          })
        )
      );
      return seededVideos;
    }),

  fetchLiveStream: publicProcedure
    .input(z.object({ playbackId: z.string() }))
    .query(async ({ input, ctx }) => {
      const { playbackId } = input;
      const { prisma } = ctx;
      // Query the video entry from the database using the roomId
      const video = await prisma.content.findUnique({
        where: {
          roomId: playbackId,
        },
      });

      if (!video) {
        // throw new Error("Video not found");
        console.log("Video not found");
        return null;
      }
      // Return the playbackId and streamKey
      return {
        playbackId: video.playbackId,
        streamKey: video.streamKey,
        assetId: video.id,
      };
    }),

  fetchAllContent: publicProcedure.query(async ({ ctx }) => {
    const { prisma } = ctx;
    const allContent = await prisma.content.findMany({
      include: {
        user: true,
      },
    });
    return allContent;
  }),

  getUserInfo: publicProcedure
    .input(z.object({ email: z.string() }))
    .query(async ({ input, ctx }) => {
      console.log("ey");
      const userEmail = input.email;
      const user = await ctx.prisma.user.findUnique({
        where: {
          email: userEmail,
        },
        include: {
          following: true,
          followers: true,
          videos: true,
        },
      });
      return user;
    }),

  getUserById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      console.log("ey");
      const userId = input.id;
      const user = await ctx.prisma.user.findUnique({
        where: {
          id: userId,
        },
        include: {
          following: true,
          followers: true,
          videos: true,
        },
      });
      return user;
    }),

  hookContentUpdate: publicProcedure
    .input(
      z.object({
        uuid: z.string(),
        playbackId: z.string(),
        assetId: z.string(),
      })
    )
    .mutation(async ({ input, ctx }) => {
      console.log("hookContentUpdate", input);
      const { uuid, playbackId, assetId } = input;
      const { prisma } = ctx;
      console.log("uuid", uuid);
      console.log("playbackId", playbackId);
      console.log("assetId", assetId);
      const content = await prisma.content.update({
        where: {
          roomId: uuid,
        },
        data: {
          playbackId: playbackId,
          id: assetId,
        },
      });
      console.log("content", content);
      return content;
    }),

  createUserVideo: protectedProcedure
    .input(CreateUserVideoInputSchema)
    .mutation(async ({ ctx, input }) => {
      const { session, prisma } = ctx;
      console.log("firing");
      if (!session || !session.user) {
        throw new Error("UNAUTHORIZED");
      }
      const { userId, uuid } = input;
      console.log("input", input);
      const video = await prisma.content.create({
        data: {
          userId: userId,
          playbackId: input.roomId,
          streamKey: input.streamKey || "static content",
          roomId: uuid || "none",
          id: input.assetId,
        },
      });
      const allContent = await prisma.content.findMany({
        include: {
          user: true,
        },
      });
      // return allContent;
      console.log(video, "video");
      return { video, allContent };
    }),

  createVideo: protectedProcedure
    .input(CreateVideoInputSchema)
    .mutation(async ({ ctx, input }) => {
      const { session, prisma } = ctx;
      if (!session || !session.user) {
        throw new Error("UNAUTHORIZED");
      }
      const { playbackId, streamKey } = input;
      const roomId = v4();

      const video = await prisma.content.create({
        data: {
          userId: session.user.id,
          playbackId: playbackId,
          streamKey: streamKey,
          roomId: roomId,
          id: input.assetId,
        },
      });

      const allContent = await prisma.content.findMany({
        include: {
          user: true,
        },
      });

      return { video, allContent };
    }),

  updateVideo: protectedProcedure
    .input(VideoUpdateSchema)
    .mutation(async ({ ctx, input }) => {
      const { session, prisma } = ctx;
      if (!session || !session.user) {
        throw new Error("UNAUTHORIZED");
      }
      const { playbackId, id, title, creatorId } = input;
      const roomId = v4();
      const video = await prisma.content.update({
        where: {
          id: id,
        },
        data: {
          roomId: title,
        },
      });

      await pusher.trigger(`chat-${creatorId}`, "video-title-update", {
        video: {
          roomId: title,
        },
      });
      return video;
    }),

  deleteVideo: protectedProcedure
    .input(z.object({ videoId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const { session, prisma } = ctx;
      if (!session || !session.user) {
        throw new Error("UNAUTHORIZED");
      }

      const { videoId } = input;

      // Delete the video entry from the database using the videoId
      const deletedVideo = await prisma.content.delete({
        where: {
          id: videoId,
        },
      });

      // Fetch all remaining content
      const allContent = await prisma.content.findMany({
        include: {
          user: true,
        },
      });

      return { deletedVideo, allContent };
    }),

  fetchAllUsers: publicProcedure.query(async ({ ctx }) => {
    const { prisma } = ctx;
    const users = await prisma.user.findMany({
      include: {
        following: true,
        videos: true,
      },
    });
    console.log("users", users);
    return users;
  }),

  createUser: publicProcedure
    .input(CreateUserInputSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx;
      const { email, name } = input;
      const user = await prisma.user.create({
        data: {
          email,
          name,
        },
      });
      return user;
    }),

  updateUserSettings: publicProcedure
    .input(UpdateUserSettingsInputSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx;
      const { userId, themeColor } = input;

      // Update the user's theme color in the database
      const updatedUser = await prisma.user.update({
        where: { id: userId },
        data: { chatColor: themeColor },
      });

      // Return the updated user
      return updatedUser;
    }),

  addDemoUsers: publicProcedure
    .input(AddDemoUsersInputSchema)
    .mutation(async ({ input, ctx }) => {
      const { prisma } = ctx;
      const demoUsers = input;

      // Add the demo users to the database
      const addedDemoUsers = await Promise.all(
        demoUsers.map((user) =>
          prisma.user.upsert({
            where: { id: user.id },
            update: {
              name: user.name,
              email: user.email,
              image: user.image,
            },
            create: {
              id: user.id,
              name: user.name,
              email: user.email,
              image: user.image,
            },
          })
        )
      );
      addedDemoUsers;
      // Fetch all users from the database
      const allUsers = await prisma.user.findMany();

      // Return the list of all users including the new demo users
      return allUsers;
    }),

  sendMessage: protectedProcedure
    .input(SendMessageInputSchema)
    .mutation(async ({ input, ctx }) => {
      const { session, prisma } = ctx;
      if (!session || !session.user) {
        throw new Error("UNAUTHORIZED");
      }
      const { roomId, reply, replyToId } = input;
      console.log("testId", roomId);
      console.log("reply", reply);
      console.log("input", input);
      const user = await prisma.user.findUnique({
        where: { id: session.user.id },
      });
      let userColor = user?.chatColor;
      if (!userColor) {
        userColor = "#fffffff";
        await prisma.user.update({
          where: { id: session.user.id },
          data: { chatColor: userColor },
        });
      }
      console.log("userColor", userColor);
      // If the message is not a reply, create a new message
      if (!reply) {
        console.log("no reply", roomId);
        const message = await prisma.message.create({
          data: {
            content: input.content,
            roomId: roomId,
            authorId: session.user.id,
            createdAt: new Date().toISOString(),
          },
        });
        // Trigger an event to Pusher after the message is created
        console.log("roomId", roomId);
        console.log("message", message);
        await pusher.trigger(`chat-${roomId}`, "new-message", {
          message: {
            id: message.id,
            content: message.content,
            createdAt: message.createdAt,
            author: { id: ctx.session.user.id, color: userColor },
          },
        });
        // Return the created message
        return message;
      }
      if (replyToId) {
        const message = await prisma.message.create({
          data: {
            content: input.content,
            roomId: roomId,
            authorId: session.user.id,
            createdAt: new Date().toISOString(),
            replyTo: replyToId,
          },
        });
        console.log("replyToId", replyToId);
        const originalMessage = await prisma.message.findUnique({
          where: { id: replyToId },
          include: { author: true },
        });
        console.log(message, "message reply");
        console.log(originalMessage, "originalMessage");
        // Trigger an event to Pusher after the message is created
        await pusher.trigger(`chat-${roomId}`, "new-message", {
          message: {
            id: message.id,
            content: message.content,
            createdAt: message.createdAt,
            author: { id: ctx.session.user.id },
            replyTo: { originalMessage },
          },
          originalMessage: originalMessage,
        });
        // Return the created message
        return { message, originalMessage };
      }
    }),
});