mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
## Description <!-- Briefly describe the purpose and scope of your changes --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for managing multiple Steam profiles per user, including a new profiles page with avatar selection and profile management. - Introduced a streamlined Steam authentication flow using a popup window, replacing the previous QR code and team-based login. - Added utilities for Steam image handling and metadata, including avatar preloading and static Steam metadata mappings. - Enhanced OpenID verification for Steam login. - Added new image-related events and expanded event handling for Steam account updates and image processing. - **Improvements** - Refactored the account structure from teams to profiles, updating related UI, context, and storage. - Updated API headers and authentication logic to use Steam IDs instead of team IDs. - Expanded game metadata with new fields for categories, franchises, and social links. - Improved library and category schemas for richer game and profile data. - Simplified and improved Steam API client methods for fetching user info, friends, and game libraries using Steam Web API. - Updated queue processing to handle individual game updates and publish image events. - Adjusted permissions and queue configurations for better message handling and dead-letter queue support. - Improved slug creation and rating estimation utilities. - **Bug Fixes** - Fixed avatar image loading to display higher quality images after initial load. - **Removals** - Removed all team, member, and credential management functionality and related database schemas. - Eliminated the QR code-based login and related UI components. - Deleted legacy team and member database tables and related code. - Removed encryption utilities and deprecated secret keys in favor of new secret management. - **Chores** - Updated dependencies and internal configuration for new features and schema changes. - Cleaned up unused code and updated database migrations for new data structures. - Adjusted import orders and removed unused imports across multiple modules. - Added new resource declarations and updated service link configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import { z } from "zod";
|
|
import { fn } from "../utils";
|
|
import { User } from "../user";
|
|
import { Steam } from "../steam";
|
|
import { Actor } from "../actor";
|
|
import { Examples } from "../examples";
|
|
import { friendTable } from "./friend.sql";
|
|
import { userTable } from "../user/user.sql";
|
|
import { steamTable } from "../steam/steam.sql";
|
|
import { createSelectSchema } from "drizzle-zod";
|
|
import { and, eq, isNull, sql } from "drizzle-orm";
|
|
import { groupBy, map, pipe, values } from "remeda";
|
|
import { ErrorCodes, VisibleError } from "../error";
|
|
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
|
|
|
export namespace Friend {
|
|
export const Info = Steam.Info
|
|
.extend({
|
|
user: User.Info.nullable().openapi({
|
|
description: "The user account that owns this Steam account",
|
|
example: Examples.User
|
|
})
|
|
})
|
|
.openapi({
|
|
ref: "Friend",
|
|
description: "Represents a friend's information stored on Nestri",
|
|
example: Examples.Friend,
|
|
});
|
|
|
|
|
|
export const InputInfo = createSelectSchema(friendTable)
|
|
.omit({ timeCreated: true, timeDeleted: true, timeUpdated: true })
|
|
|
|
export type Info = z.infer<typeof Info>;
|
|
export type InputInfo = z.infer<typeof InputInfo>;
|
|
|
|
export const add = fn(
|
|
InputInfo.partial({ steamID: true }),
|
|
async (input) =>
|
|
createTransaction(async (tx) => {
|
|
const steamID = input.steamID ?? Actor.steamID()
|
|
if (steamID === input.friendSteamID) {
|
|
throw new VisibleError(
|
|
"forbidden",
|
|
ErrorCodes.Validation.INVALID_PARAMETER,
|
|
"Cannot add yourself as a friend"
|
|
);
|
|
}
|
|
|
|
const results =
|
|
await tx
|
|
.select()
|
|
.from(friendTable)
|
|
.where(
|
|
and(
|
|
eq(friendTable.steamID, steamID),
|
|
eq(friendTable.friendSteamID, input.friendSteamID),
|
|
isNull(friendTable.timeDeleted)
|
|
)
|
|
)
|
|
.execute()
|
|
|
|
if (results.length > 0) return null
|
|
|
|
await tx
|
|
.insert(friendTable)
|
|
.values({
|
|
steamID,
|
|
friendSteamID: input.friendSteamID
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [friendTable.steamID, friendTable.friendSteamID],
|
|
set: { timeDeleted: null }
|
|
})
|
|
|
|
return steamID
|
|
}),
|
|
)
|
|
|
|
export const end = fn(
|
|
InputInfo,
|
|
(input) =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.update(friendTable)
|
|
.set({ timeDeleted: sql`now()` })
|
|
.where(
|
|
and(
|
|
eq(friendTable.steamID, input.steamID),
|
|
eq(friendTable.friendSteamID, input.friendSteamID),
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
export const list = () =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.select({
|
|
steam: steamTable,
|
|
user: userTable,
|
|
})
|
|
.from(friendTable)
|
|
.innerJoin(
|
|
steamTable,
|
|
eq(friendTable.friendSteamID, steamTable.id)
|
|
)
|
|
.leftJoin(
|
|
userTable,
|
|
eq(steamTable.userID, userTable.id)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(friendTable.steamID, Actor.steamID()),
|
|
isNull(friendTable.timeDeleted)
|
|
)
|
|
)
|
|
.limit(100)
|
|
.execute()
|
|
.then(rows => serialize(rows))
|
|
)
|
|
|
|
export const fromFriendID = fn(
|
|
InputInfo.shape.friendSteamID,
|
|
(friendSteamID) =>
|
|
useTransaction(async (tx) =>
|
|
tx
|
|
.select({
|
|
steam: steamTable,
|
|
user: userTable,
|
|
})
|
|
.from(friendTable)
|
|
.innerJoin(
|
|
steamTable,
|
|
eq(friendTable.friendSteamID, steamTable.id)
|
|
)
|
|
.leftJoin(
|
|
userTable,
|
|
eq(steamTable.userID, userTable.id)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(friendTable.steamID, Actor.steamID()),
|
|
eq(friendTable.friendSteamID, friendSteamID),
|
|
isNull(friendTable.timeDeleted)
|
|
)
|
|
)
|
|
.limit(1)
|
|
.execute()
|
|
.then(rows => serialize(rows).at(0))
|
|
)
|
|
)
|
|
|
|
|
|
export const areFriends = fn(
|
|
InputInfo.shape.friendSteamID,
|
|
(friendSteamID) =>
|
|
useTransaction(async (tx) => {
|
|
const result = await tx
|
|
.select()
|
|
.from(friendTable)
|
|
.where(
|
|
and(
|
|
eq(friendTable.steamID, Actor.steamID()),
|
|
eq(friendTable.friendSteamID, friendSteamID),
|
|
isNull(friendTable.timeDeleted)
|
|
)
|
|
)
|
|
.limit(1)
|
|
.execute()
|
|
|
|
return result.length > 0
|
|
})
|
|
)
|
|
|
|
export function serialize(
|
|
input: { user: typeof userTable.$inferSelect | null; steam: typeof steamTable.$inferSelect }[],
|
|
): z.infer<typeof Info>[] {
|
|
return pipe(
|
|
input,
|
|
groupBy((row) => row.steam.id),
|
|
values(),
|
|
map((group) => ({
|
|
...Steam.serialize(group[0].steam),
|
|
user: group[0].user ? User.serialize(group[0].user!) : null
|
|
}))
|
|
)
|
|
}
|
|
|
|
} |