Rename getLovers and add base send-search-notifications.ts

This commit is contained in:
MartinBraquet
2025-09-15 18:48:34 +02:00
parent 2cd102ef0b
commit 02a1cbd467
9 changed files with 127 additions and 61 deletions

View File

@@ -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. Its 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<k> } = {
@@ -128,7 +148,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'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<k> } = {
'get-channel-messages': getChannelMessages,
'get-channel-seen-time': getLastSeenChannelTime,
'set-channel-seen-time': setChannelLastSeenTime,
'send-search-notifications': sendSearchNotifications,
}
Object.entries(handlers).forEach(([path, handler]) => {

View File

@@ -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,

View File

@@ -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}
}

View File

@@ -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(
`

View File

@@ -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

View File

@@ -132,7 +132,7 @@ export function getScoredAnswerCompatibility(
)
}
export const getLoversCompatibilityFactor = (
export const getProfilesCompatibilityFactor = (
lover1: LoverRow,
lover2: LoverRow
) => {

5
scripts/curl.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
curl -X POST http://localhost:8088/v0/send-search-notifications

View File

@@ -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(),

View File

@@ -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
})
}