diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 19f843a8..736e47ac 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -1,56 +1,58 @@ -import { API, type APIPath } from 'common/api/schema' -import { APIError, pathWithPrefix } from 'common/api/utils' +import {API, type APIPath} from 'common/api/schema' +import {APIError, pathWithPrefix} from 'common/api/utils' import cors from 'cors' import * as crypto from 'crypto' -import express from 'express' -import { type ErrorRequestHandler, type RequestHandler } from 'express' -import { hrtime } from 'node:process' -import { withMonitoringContext } from 'shared/monitoring/context' -import { log } from 'shared/monitoring/log' -import { metrics } from 'shared/monitoring/metrics' -import { banUser } from './ban-user' -import { blockUser, unblockUser } from './block-user' -import { getCompatibleLoversHandler } from './compatible-lovers' -import { createComment } from './create-comment' -import { createCompatibilityQuestion } from './create-compatibility-question' -import { createLover } from './create-lover' -import { createUser } from './create-user' -import { getCompatibilityQuestions } from './get-compatibililty-questions' -import { getLikesAndShips } from './get-likes-and-ships' -import { getLoverAnswers } from './get-lover-answers' -import { getLovers } from './get-lovers' -import { getSupabaseToken } from './get-supabase-token' -import { getDisplayUser, getUser } from './get-user' -import { getMe } from './get-me' -import { hasFreeLike } from './has-free-like' -import { health } from './health' -import { typedEndpoint, type APIHandler } from './helpers/endpoint' -import { hideComment } from './hide-comment' -import { likeLover } from './like-lover' -import { markAllNotifsRead } from './mark-all-notifications-read' -import { removePinnedPhoto } from './remove-pinned-photo' -import { report } from './report' -import { searchLocation } from './search-location' -import { searchNearCity } from './search-near-city' -import { shipLovers } from './ship-lovers' -import { starLover } from './star-lover' -import { updateLover } from './update-lover' -import { updateMe } from './update-me' -import { deleteMe } from './delete-me' -import { getCurrentPrivateUser } from './get-current-private-user' -import { createPrivateUserMessage } from './create-private-user-message' +import express, {type ErrorRequestHandler, type RequestHandler} from 'express' +import {hrtime} from 'node:process' +import {withMonitoringContext} from 'shared/monitoring/context' +import {log} from 'shared/monitoring/log' +import {metrics} from 'shared/monitoring/metrics' +import {banUser} from './ban-user' +import {blockUser, unblockUser} from './block-user' +import {getCompatibleLoversHandler} from './compatible-lovers' +import {createComment} from './create-comment' +import {createCompatibilityQuestion} from './create-compatibility-question' +import {createLover} from './create-lover' +import {createUser} from './create-user' +import {getCompatibilityQuestions} from './get-compatibililty-questions' +import {getLikesAndShips} from './get-likes-and-ships' +import {getLoverAnswers} from './get-lover-answers' +import {getProfiles} from './get-profiles' +import {getSupabaseToken} from './get-supabase-token' +import {getDisplayUser, getUser} from './get-user' +import {getMe} from './get-me' +import {hasFreeLike} from './has-free-like' +import {health} from './health' +import {sendSearchNotifications} from './send-search-notifications' +import {type APIHandler, typedEndpoint} from './helpers/endpoint' +import {hideComment} from './hide-comment' +import {likeLover} from './like-lover' +import {markAllNotifsRead} from './mark-all-notifications-read' +import {removePinnedPhoto} from './remove-pinned-photo' +import {report} from './report' +import {searchLocation} from './search-location' +import {searchNearCity} from './search-near-city' +import {shipLovers} from './ship-lovers' +import {starLover} from './star-lover' +import {updateLover} from './update-lover' +import {updateMe} from './update-me' +import {deleteMe} from './delete-me' +import {getCurrentPrivateUser} from './get-current-private-user' +import {createPrivateUserMessage} from './create-private-user-message' import { getChannelMemberships, getChannelMessages, getLastSeenChannelTime, setChannelLastSeenTime, } from 'api/get-private-messages' -import { searchUsers } from './search-users' -import { createPrivateUserMessageChannel } from './create-private-user-message-channel' -import { leavePrivateUserMessageChannel } from './leave-private-user-message-channel' -import { updatePrivateUserMessageChannel } from './update-private-user-message-channel' -import { getNotifications } from './get-notifications' -import { updateNotifSettings } from './update-notif-setting' +import {searchUsers} from './search-users' +import {createPrivateUserMessageChannel} from './create-private-user-message-channel' +import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel' +import {updatePrivateUserMessageChannel} from './update-private-user-message-channel' +import {getNotifications} from './get-notifications' +import {updateNotifSettings} from './update-notif-setting' +import swaggerUi from "swagger-ui-express" +import * as fs from "fs" const allowCorsUnrestricted: RequestHandler = cors({}) @@ -66,15 +68,15 @@ const requestMonitoring: RequestHandler = (req, _res, next) => { const traceId = traceContext ? traceContext.split('/')[0] : crypto.randomUUID() - const context = { endpoint: req.path, traceId } + const context = {endpoint: req.path, traceId} withMonitoringContext(context, () => { const startTs = hrtime.bigint() log(`${req.method} ${req.url}`) - metrics.inc('http/request_count', { endpoint: req.path }) + metrics.inc('http/request_count', {endpoint: req.path}) next() const endTs = hrtime.bigint() const latencyMs = Number(endTs - startTs) / 1e6 - metrics.push('http/request_latency', latencyMs, { endpoint: req.path }) + metrics.push('http/request_latency', latencyMs, {endpoint: req.path}) }) } @@ -82,7 +84,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => { if (error instanceof APIError) { log.info(error) if (!res.headersSent) { - const output: { [k: string]: unknown } = { message: error.message } + const output: { [k: string]: unknown } = {message: error.message} if (error.details != null) { output.details = error.details } @@ -91,7 +93,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => { } else { log.error(error) if (!res.headersSent) { - res.status(500).json({ message: error.stack, error }) + res.status(500).json({message: error.stack, error}) } } } @@ -99,6 +101,24 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => { export const app = express() app.use(requestMonitoring) +const swaggerDocument = JSON.parse(fs.readFileSync("./openapi.json", "utf-8")) +swaggerDocument.info = { + ...swaggerDocument.info, + description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.", + version: "1.0.0", + contact: { + name: "Compass", + email: "compass.meet.info@gmail.com", + url: "https://compassmeet.com" + } +}; + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)) + +app.listen(process.env.PORT ?? 8088, () => { + console.log(`API UI available at /docs`) +}) + app.options('*', allowCorsUnrestricted) const handlers: { [k in APIPath]: APIHandler } = { @@ -128,7 +148,7 @@ const handlers: { [k in APIPath]: APIHandler } = { 'get-likes-and-ships': getLikesAndShips, 'has-free-like': hasFreeLike, 'star-lover': starLover, - 'get-lovers': getLovers, + 'get-profiles': getProfiles, 'get-lover-answers': getLoverAnswers, 'get-compatibility-questions': getCompatibilityQuestions, 'remove-pinned-photo': removePinnedPhoto, @@ -146,6 +166,7 @@ const handlers: { [k in APIPath]: APIHandler } = { 'get-channel-messages': getChannelMessages, 'get-channel-seen-time': getLastSeenChannelTime, 'set-channel-seen-time': setChannelLastSeenTime, + 'send-search-notifications': sendSearchNotifications, } Object.entries(handlers).forEach(([path, handler]) => { diff --git a/backend/api/src/get-lovers.ts b/backend/api/src/get-profiles.ts similarity index 98% rename from backend/api/src/get-lovers.ts rename to backend/api/src/get-profiles.ts index 6e038700..d63ed8ec 100644 --- a/backend/api/src/get-lovers.ts +++ b/backend/api/src/get-profiles.ts @@ -6,7 +6,7 @@ import {getCompatibleLovers} from 'api/compatible-lovers' import {intersection} from 'lodash' import {MAX_INT, MIN_INT} from "common/constants"; -export const getLovers: APIHandler<'get-lovers'> = async (props, _auth) => { +export const getProfiles: APIHandler<'get-profiles'> = async (props, _auth) => { const pg = createSupabaseDirectClient() const { limit: limitParam, diff --git a/backend/api/src/send-search-notifications.ts b/backend/api/src/send-search-notifications.ts new file mode 100644 index 00000000..af7e0012 --- /dev/null +++ b/backend/api/src/send-search-notifications.ts @@ -0,0 +1,33 @@ +import {APIHandler} from './helpers/endpoint' +import {createSupabaseDirectClient} from "shared/supabase/init"; +import {convertRow} from "shared/love/supabase"; +import {from, join, renderSql, select, where} from "shared/supabase/sql-builder"; + +export function convertSearchRow(row: any): any { + return row +} + +export const sendSearchNotifications: APIHandler<'send-search-notifications'> = async (_, auth) => { + const pg = createSupabaseDirectClient() + + const search_query = renderSql( + select('bookmarked_searches.*'), + from('bookmarked_searches'), + ) + const searches = pg.map(search_query, [], convertSearchRow) + + const query = renderSql( + select('lovers.*, name, username, users.data as user'), + from('lovers'), + join('users on users.id = lovers.user_id'), + where('looking_for_matches = true'), + where( + `(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)` + ), + where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`), + ) + + const profiles = await pg.map(query, [], convertRow) + + return {status: 'success', lovers: profiles} +} \ No newline at end of file diff --git a/backend/shared/src/love/supabase.ts b/backend/shared/src/love/supabase.ts index ab00cd63..a0795ff4 100644 --- a/backend/shared/src/love/supabase.ts +++ b/backend/shared/src/love/supabase.ts @@ -40,7 +40,7 @@ export const getLover = async (userId: string) => { ) } -export const getLovers = async (userIds: string[]) => { +export const getProfiles = async (userIds: string[]) => { const pg = createSupabaseDirectClient() return await pg.map( ` diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index d0374b03..d2fa613f 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -51,6 +51,15 @@ export const API = (_apiTypeCheck = { props: z.object({}), returns: {} as { jwt: string }, }, + 'send-search-notifications': { + method: 'POST', + authed: false, + props: z.object({}), + returns: {} as { + status: 'success' | 'fail' + lovers: Lover[] + }, + }, 'mark-all-notifs-read': { method: 'POST', authed: true, @@ -304,7 +313,7 @@ export const API = (_apiTypeCheck = { status: 'success' }, }, - 'get-lovers': { + 'get-profiles': { method: 'GET', authed: false, props: z diff --git a/common/src/love/compatibility-score.ts b/common/src/love/compatibility-score.ts index 8b330b09..257bed3d 100644 --- a/common/src/love/compatibility-score.ts +++ b/common/src/love/compatibility-score.ts @@ -132,7 +132,7 @@ export function getScoredAnswerCompatibility( ) } -export const getLoversCompatibilityFactor = ( +export const getProfilesCompatibilityFactor = ( lover1: LoverRow, lover2: LoverRow ) => { diff --git a/scripts/curl.sh b/scripts/curl.sh new file mode 100755 index 00000000..d9e7d35c --- /dev/null +++ b/scripts/curl.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +curl -X POST http://localhost:8088/v0/send-search-notifications + + diff --git a/web/components/profiles/profiles-home.tsx b/web/components/profiles/profiles-home.tsx index 90191be8..0aa9a9e0 100644 --- a/web/components/profiles/profiles-home.tsx +++ b/web/components/profiles/profiles-home.tsx @@ -15,8 +15,6 @@ import {useGetter} from 'web/hooks/use-getter' import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state' import {useUser} from 'web/hooks/use-user' import {api} from 'web/lib/api' -import {debounce, omit} from 'lodash' -import {PREF_AGE_MAX, PREF_AGE_MIN,} from 'web/components/filters/location-filter' import {useBookmarkedSearches} from "web/hooks/use-bookmarked-searches"; export function ProfilesHome() { @@ -55,7 +53,7 @@ export function ProfilesHome() { if (!user) return; setIsReloading(true); const current = ++id.current; - api('get-lovers', removeNullOrUndefinedProps({ + api('get-profiles', removeNullOrUndefinedProps({ limit: 20, compatibleWithUserId: user?.id, ...filters @@ -77,7 +75,7 @@ export function ProfilesHome() { try { setIsLoadingMore(true); const lastLover = lovers[lovers.length - 1]; - const result = await api('get-lovers', removeNullOrUndefinedProps({ + const result = await api('get-profiles', removeNullOrUndefinedProps({ limit: 20, compatibleWithUserId: user?.id, after: lastLover?.id.toString(), diff --git a/web/hooks/use-lovers.ts b/web/hooks/use-lovers.ts index ac5ff3ba..71d59d4b 100644 --- a/web/hooks/use-lovers.ts +++ b/web/hooks/use-lovers.ts @@ -4,7 +4,7 @@ import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-s import { api } from 'web/lib/api' import { APIResponse } from 'common/api/schema' import { useLoverByUserId } from './use-lover' -import { getLoversCompatibilityFactor } from 'common/love/compatibility-score' +import { getProfilesCompatibilityFactor } from 'common/love/compatibility-score' export const useCompatibleLovers = ( userId: string | null | undefined, @@ -32,7 +32,7 @@ export const useCompatibleLovers = ( if (data && lover && options?.sortWithModifiers) { data.compatibleLovers = sortBy(data.compatibleLovers, (l) => { - const modifier = !lover ? 1 : getLoversCompatibilityFactor(lover, l) + const modifier = !lover ? 1 : getProfilesCompatibilityFactor(lover, l) return -1 * modifier * data.loverCompatibilityScores[l.user.id].score }) }