diff --git a/apps/api/src/schema/dropbox-accounts.ts b/apps/api/src/schema/dropbox-accounts.ts new file mode 100644 index 00000000..1a7f830b --- /dev/null +++ b/apps/api/src/schema/dropbox-accounts.ts @@ -0,0 +1,20 @@ +import { InferInsertModel, InferSelectModel } from "drizzle-orm"; +import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import users from "./users"; + +const dropboxAccounts = pgTable("dropbox_accounts", { + id: text("xata_id").primaryKey(), + email: text("email").unique().notNull(), + isBetaUser: boolean("is_beta_user").default(false).notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id), + xataVersion: text("xata_version").notNull(), + createdAt: timestamp("xata_createdat").defaultNow().notNull(), + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), +}); + +export type SelectDropboxAccounts = InferSelectModel; +export type InsertDropboxAccounts = InferInsertModel; + +export default dropboxAccounts; diff --git a/apps/api/src/schema/google-drive-accounts.ts b/apps/api/src/schema/google-drive-accounts.ts new file mode 100644 index 00000000..f5de199d --- /dev/null +++ b/apps/api/src/schema/google-drive-accounts.ts @@ -0,0 +1,24 @@ +import { InferInsertModel, InferSelectModel } from "drizzle-orm"; +import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import users from "./users"; + +const googleDriveAccounts = pgTable("google_drive_accounts", { + id: text("xata_id").primaryKey(), + email: text("email").unique().notNull(), + isBetaUser: boolean("is_beta_user").default(false).notNull(), + userId: text("user_id") + .notNull() + .references(() => users.id), + xataVersion: text("xata_version").notNull(), + createdAt: timestamp("xata_createdat").defaultNow().notNull(), + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), +}); + +export type SelectGoogleDriveAccounts = InferSelectModel< + typeof googleDriveAccounts +>; +export type InsertGoogleDriveAccounts = InferInsertModel< + typeof googleDriveAccounts +>; + +export default googleDriveAccounts; diff --git a/apps/api/src/schema/index.ts b/apps/api/src/schema/index.ts index 94e3c0aa..b05023ff 100644 --- a/apps/api/src/schema/index.ts +++ b/apps/api/src/schema/index.ts @@ -4,6 +4,8 @@ import apiKeys from "./api-keys"; import artistAlbums from "./artist-albums"; import artistTracks from "./artist-tracks"; import artists from "./artists"; +import dropboxAccounts from "./dropbox-accounts"; +import googleDriveAccounts from "./google-drive-accounts"; import lovedTracks from "./loved-tracks"; import playlistTracks from "./playlist-tracks"; import playlists from "./playlists"; @@ -46,4 +48,6 @@ export default { spotifyTokens, artistTracks, artistAlbums, + dropboxAccounts, + googleDriveAccounts, }; diff --git a/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts b/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts index 92ae8da4..9af61a9c 100644 --- a/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts +++ b/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts @@ -10,6 +10,10 @@ import { QueryParams } from "lexicon/types/app/rocksky/actor/getProfile"; import { createAgent } from "lib/agent"; import _ from "lodash"; import tables from "schema"; +import { SelectDropboxAccounts } from "schema/dropbox-accounts"; +import { SelectGoogleDriveAccounts } from "schema/google-drive-accounts"; +import { SelectSpotifyAccount } from "schema/spotify-accounts"; +import { SelectSpotifyToken } from "schema/spotify-tokens"; import { SelectUser } from "schema/users"; export default function (server: Server, ctx: Context) { @@ -161,7 +165,17 @@ const retrieveProfile = ({ did, agent, user, -}: WithUser): Effect.Effect<[Profile, string], Error> => { +}: WithUser): Effect.Effect< + [ + Profile, + string, + SelectSpotifyAccount, + SelectSpotifyToken, + SelectGoogleDriveAccounts, + SelectDropboxAccounts, + ], + Error +> => { return Effect.tryPromise({ try: async () => { return Promise.all([ @@ -178,25 +192,97 @@ const retrieveProfile = ({ user, })), ctx.resolver.resolveDidToHandle(did), + ctx.db + .select() + .from(tables.spotifyAccounts) + .leftJoin( + tables.users, + eq(tables.spotifyAccounts.userId, tables.users.id) + ) + .where(eq(tables.users.did, did)) + .execute() + .then(([result]) => result.spotify_accounts), + ctx.db + .select() + .from(tables.spotifyTokens) + .leftJoin( + tables.users, + eq(tables.spotifyTokens.userId, tables.users.id) + ) + .where(eq(tables.users.did, did)) + .execute() + .then(([result]) => result?.spotify_tokens), + ctx.db + .select() + .from(tables.googleDriveAccounts) + .leftJoin( + tables.users, + eq(tables.googleDriveAccounts.userId, tables.users.id) + ) + .where(eq(tables.users.did, did)) + .execute() + .then(([result]) => result?.google_drive_accounts), + ctx.db + .select() + .from(tables.dropboxAccounts) + .leftJoin( + tables.users, + eq(tables.dropboxAccounts.userId, tables.users.id) + ) + .where(eq(tables.users.did, did)) + .execute() + .then(([result]) => result?.dropbox_accounts), ]); }, catch: (error) => new Error(`Failed to retrieve profile: ${error}`), }); }; -const refreshProfile = ([profile, handle]: [Profile, string]) => { +const refreshProfile = ([ + profile, + handle, + selectSpotifyAccount, + selectSpotifyToken, + selectGoogleDriveAccounts, + selectDropboxAccounts, +]: [ + Profile, + string, + SelectSpotifyAccount, + SelectSpotifyToken, + SelectGoogleDriveAccounts, + SelectDropboxAccounts, +]) => { return Effect.tryPromise({ try: async () => { - return [profile, handle]; + return [ + profile, + handle, + selectSpotifyAccount, + selectSpotifyToken, + selectGoogleDriveAccounts, + selectDropboxAccounts, + ]; }, catch: (error) => new Error(`Failed to refresh profile: ${error}`), }); }; -const presentation = ([profile, handle]: [Profile, string]): Effect.Effect< - ProfileViewDetailed, - never -> => { +const presentation = ([ + profile, + handle, + spotifyUser, + spotifyToken, + googledrive, + dropbox, +]: [ + Profile, + string, + SelectSpotifyAccount, + SelectSpotifyToken, + SelectGoogleDriveAccounts, + SelectDropboxAccounts, +]): Effect.Effect => { return Effect.sync(() => ({ id: profile.user?.id, did: profile.did, @@ -205,6 +291,11 @@ const presentation = ([profile, handle]: [Profile, string]): Effect.Effect< avatar: `https://cdn.bsky.app/img/avatar/plain/${profile.did}/${_.get(profile, "profileRecord.value.avatar.ref", "").toString()}@jpeg`, createdAt: profile.user?.createdAt.toISOString(), updatedAt: profile.user?.updatedAt.toISOString(), + spotifyUser, + spotifyToken, + spotifyConnected: !!spotifyToken, + googledrive, + dropbox, })); }; diff --git a/apps/web/src/api/profile.ts b/apps/web/src/api/profile.ts index 84428cec..ba25a4d6 100644 --- a/apps/web/src/api/profile.ts +++ b/apps/web/src/api/profile.ts @@ -1,28 +1,30 @@ -import axios from "axios"; -import { API_URL } from "../consts"; +import { client } from "."; import { Scrobble } from "../types/scrobble"; export const getProfileByDid = async (did: string) => { - const response = await axios.get(`${API_URL}/users/${did}`); + const response = await client.get("/xrpc/app.rocksky.actor.getProfile", { + params: { did }, + }); return response.data; }; export const getProfileStatsByDid = async (did: string) => { - const response = await axios.get( - `${API_URL}/xrpc/app.rocksky.stats.getStats`, - { params: { did } } - ); + const response = await client.get("/xrpc/app.rocksky.stats.getStats", { + params: { did }, + }); return response.data; }; export const getRecentTracksByDid = async ( did: string, offset = 0, - size = 10 + limit = 10 ): Promise => { - const response = await axios.get( - `${API_URL}/users/${did}/scrobbles`, - { params: { size, offset } } + const response = await client.get<{ scrobbles: Scrobble[] }>( + "/xrpc/app.rocksky.actor.getActorScrobbles", + { + params: { did, offset, limit }, + } ); - return response.data; + return response.data.scrobbles || []; }; diff --git a/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx b/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx index 2e9bf45e..afaf2f6d 100644 --- a/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx +++ b/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx @@ -96,15 +96,15 @@ function RecentTracks(props: RecentTracksProps) { title: item.title, artist: item.artist, album: item.album, - albumArt: item.album_art, - albumArtist: item.album_artist, + albumArt: item.albumArt, + albumArtist: item.albumArtist, uri: item.uri, - date: item.created_at.endsWith("Z") - ? item.created_at - : `${item.created_at}Z`, + date: item.createdAt.endsWith("Z") + ? item.createdAt + : `${item.createdAt}Z`, scrobbleUri: item.uri, - albumUri: item.album_uri, - artistUri: item.artist_uri, + albumUri: item.albumUri, + artistUri: item.artistUri, })) ); diff --git a/apps/web/src/types/scrobble.ts b/apps/web/src/types/scrobble.ts index 72db8b33..31d2633a 100644 --- a/apps/web/src/types/scrobble.ts +++ b/apps/web/src/types/scrobble.ts @@ -1,15 +1,15 @@ export type Scrobble = { id: string; - track_id: string; + trackId: string; title: string; artist: string; album: string; - album_art?: string; - album_artist: string; + albumArt?: string; + albumArtist: string; handle: string; - track_uri: string; - album_uri: string; - artist_uri: string; + trackUri: string; + albumUri: string; + artistUri: string; uri: string; - created_at: string; + createdAt: string; };