Files
netris-nestri/packages/core/src:old/profile/index.ts
Wanjohi 457aac2258 feat(infra): Update infra and add support for teams to SST (#186)
## Description
- [x] Adds support for AWS SSO, which makes us (the team) able to use
SST and update the components independently
- [x] Splits the webpage into the landing page (Qwik), and Astro (the
console) in charge of playing. This allows us to pass in Environment
Variables to the console
- ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This
allows us to use Fumadocs _citation needed_ that's much more beautiful,
and supports OpenApi~
- Cloudflare pages with github integration is not working on our new CF
account. So we will have to push the pages deployment manually with
Github actions
- [x] Moves the current set up from my personal CF and AWS accounts to
dedicated Nestri accounts -

## Related Issues
<!-- List any related issues (e.g., "Closes #123", "Fixes #456") -->

## Type of Change

- [ ] Bug fix (non-breaking change)
- [x] New feature (non-breaking change)
- [ ] Breaking change (fix or feature that changes existing
functionality)
- [x] Documentation update
- [ ] Other (please describe):

## Checklist

- [x] I have updated relevant documentation
- [x] My code follows the project's coding style
- [x] My changes generate no new warnings/errors

## Notes for Reviewers
<!-- Point out areas you'd like reviewers to focus on, questions you
have, or decisions that need discussion -->
Please approve my PR 🥹


## Screenshots/Demo
<!-- If applicable, add screenshots or a GIF demo of your changes
(especially for UI changes) -->

## Additional Context
<!-- Add any other context about the pull request here -->
2025-02-27 18:52:05 +03:00

412 lines
12 KiB
TypeScript

import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID, } from "@instantdb/admin";
import { useCurrentUser } from "../actor";
export const userStatus = z.enum([
"active", //online and playing a game
"idle", //online and not playing
"offline",
]);
export module Profiles {
const MAX_ATTEMPTS = 50;
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
username: z.string().openapi({
description: "The user's unique username",
example: Examples.Profile.username,
}),
avatarUrl: z.string().or(z.undefined()).openapi({
description: "The url to the profile picture.",
example: Examples.Profile.username,
}),
status: userStatus.openapi({
description: "Whether the user is active, idle or offline",
example: Examples.Profile.status
}),
discriminator: z.string().or(z.number()).openapi({
description: "The number discriminator for each username",
example: Examples.Profile.discriminator,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time when this profile was first created",
example: Examples.Profile.createdAt,
}),
updatedAt: z.string().or(z.number()).openapi({
description: "The time when this profile was last edited",
example: Examples.Profile.updatedAt,
})
})
.openapi({
ref: "Profile",
description: "Represents a profile of a user on Nestri",
example: Examples.Profile,
});
export type Info = z.infer<typeof Info>;
export type userStatus = z.infer<typeof userStatus>;
export const sanitizeUsername = (username: string): string => {
// Remove spaces and numbers
return username.replace(/[\s0-9]/g, '');
};
export const generateDiscriminator = (): string => {
return Math.floor(Math.random() * 100).toString().padStart(2, '0');
};
export const isValidDiscriminator = (discriminator: string): boolean => {
return /^\d{2}$/.test(discriminator);
};
export const fromUsername = fn(z.string(), async (input) => {
const sanitizedUsername = sanitizeUsername(input);
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username: sanitizedUsername,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length == 0) {
return null
}
return pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
discriminator: group[0].discriminator,
updatedAt: group[0].updatedAt,
status: group[0].status as userStatus
}))
)
})
export const findAvailableDiscriminator = fn(z.string(), async (input) => {
const db = databaseClient()
const username = sanitizeUsername(input);
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const discriminator = generateDiscriminator();
const query = {
profiles: {
$: {
where: {
username,
discriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length === 0) {
return discriminator;
}
}
return null; // No available discriminators
})
export const create = fn(z.object({ username: z.string(), customDiscriminator: z.string().optional(), avatarUrl: z.string().optional(), owner: z.string() }), async (input) => {
const username = sanitizeUsername(input.username);
const db = databaseClient()
const id = createID()
const now = new Date().toISOString()
let discriminator: string | null;
if (input.customDiscriminator) {
if (!isValidDiscriminator(input.customDiscriminator)) {
console.error('Invalid discriminator format')
return null
// throw new Error('Invalid discriminator format');
}
const query = {
profiles: {
$: {
where: {
username,
discriminator: input.customDiscriminator
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (profiles.length != 0) {
console.error("Username and discriminator combination already taken ")
return null
// throw new Error('Username and discriminator combination already taken');
}
discriminator = input.customDiscriminator
} else {
// Generate a random available discriminator
discriminator = await findAvailableDiscriminator(username);
if (!discriminator) {
console.error("No available discriminators for this username ")
return null
// throw new Error('No available discriminators for this username');
}
}
return await db.transact(
db.tx.profiles[id]!.update({
username,
avatarUrl: input.avatarUrl,
createdAt: now,
updatedAt: now,
discriminator,
status: "idle"
}).link({ owner: input.owner })
)
})
export const getFullUsername = async (username: string) => {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
username,
}
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
console.error('User not found')
return null
// throw new Error('User not found');
}
return `${profiles[0]?.username}#${profiles[0]?.discriminator}`;
}
export const fromOwnerID = async (ownerID: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
owner: ownerID
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromOwnerID", error)
return null
}
}
export const fromID = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const fromIDToOwner = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles as any
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
return profiles[0]!.owner as string
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await fromOwnerID(user.id);
return currentProfile
}
export const setStatus = fn(userStatus, async (status) => {
try {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(
db.tx.profiles[user.id]!.update({
status,
updatedAt: now
})
)
} catch (error) {
console.log("user setStatus error", error)
return null
}
})
export const list = async () => {
try {
const db = databaseClient()
// const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString()
const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
const query = {
profiles: {
$: {
limit: 10,
where: {
updatedAt: { $gt: ago },
},
order: {
updatedAt: "desc" as const,
},
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const result = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return result
} catch (error) {
console.log("user list error", error)
return null
}
}
}