mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-24 10:56:21 -05:00
* Test * Add pretty formatting * Fix Tests * Fix Tests * Fix Tests * Fix * Add pretty formatting fix * Fix * Test * Fix tests * Clean typeckech * Add prettier check * Fix api tsconfig * Fix api tsconfig * Fix tsconfig * Fix * Fix * Prettier
1009 lines
27 KiB
TypeScript
1009 lines
27 KiB
TypeScript
import {
|
|
arraybeSchema,
|
|
baseProfilesSchema,
|
|
combinedProfileSchema,
|
|
contentSchema,
|
|
zBoolean,
|
|
} from 'common/api/zod-types'
|
|
import {PrivateChatMessage} from 'common/chat-message'
|
|
import {Notification} from 'common/notifications'
|
|
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
|
import {MAX_COMPATIBILITY_QUESTION_LENGTH, OPTION_TABLES} from 'common/profiles/constants'
|
|
import {Profile, ProfileRow} from 'common/profiles/profile'
|
|
import {PrivateMessageChannel} from 'common/supabase/private-messages'
|
|
import {Row} from 'common/supabase/utils'
|
|
import {PrivateUser, User} from 'common/user'
|
|
import {notification_preference} from 'common/user-notification-preferences'
|
|
import {arrify} from 'common/util/array'
|
|
import {z} from 'zod'
|
|
|
|
import {LikeData, ShipData} from './profile-types'
|
|
import {FullUser} from './user-types'
|
|
|
|
// mqp: very unscientific, just balancing our willingness to accept load
|
|
// with user willingness to put up with stale data
|
|
export const DEFAULT_CACHE_STRATEGY = 'public, max-age=5, stale-while-revalidate=10'
|
|
|
|
type APIGenericSchema = {
|
|
// GET is for retrieval, POST is to mutate something, PUT is idempotent mutation (can be repeated safely)
|
|
method: 'GET' | 'POST' | 'PUT'
|
|
// whether the endpoint requires authentication
|
|
authed: boolean
|
|
// whether the endpoint requires authentication
|
|
rateLimited?: boolean
|
|
// zod schema for the request body (or for params for GET requests)
|
|
props: z.ZodType
|
|
// note this has to be JSON serializable
|
|
returns?: Record<string, any>
|
|
// Cache-Control header. like, 'max-age=60'
|
|
cache?: string
|
|
// Description of the endpoint
|
|
summary?: string
|
|
// Tag for grouping endpoints in documentation
|
|
tag?: string
|
|
}
|
|
|
|
let _apiTypeCheck: {[x: string]: APIGenericSchema}
|
|
|
|
export const API = (_apiTypeCheck = {
|
|
health: {
|
|
method: 'GET',
|
|
authed: false,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
returns: {} as {
|
|
message: 'Server is working.'
|
|
uid?: string
|
|
version?: string
|
|
git?: {
|
|
revision?: string
|
|
commitDate?: string
|
|
author?: string
|
|
message?: string
|
|
}
|
|
},
|
|
summary: 'Check whether the API server is running',
|
|
tag: 'General',
|
|
},
|
|
'get-supabase-token': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
returns: {} as {jwt: string},
|
|
summary: 'Return a Supabase JWT for authenticated clients',
|
|
tag: 'Tokens',
|
|
},
|
|
'mark-all-notifs-read': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
summary: 'Mark all user notifications as read',
|
|
tag: 'Notifications',
|
|
},
|
|
// 'user/:username': {
|
|
// method: 'GET',
|
|
// authed: false,
|
|
// rateLimited: false,
|
|
// cache: DEFAULT_CACHE_STRATEGY,
|
|
// returns: {} as FullUser,
|
|
// props: z.object({username: z.string()}).strict(),
|
|
// summary: 'Get full public profile by username',
|
|
// },
|
|
// 'user/:username/lite': {
|
|
// method: 'GET',
|
|
// authed: false,
|
|
// rateLimited: false,
|
|
// cache: DEFAULT_CACHE_STRATEGY,
|
|
// returns: {} as DisplayUser,
|
|
// props: z.object({username: z.string()}).strict(),
|
|
// summary: 'Get lightweight public profile by username',
|
|
// },
|
|
'user/by-id/:id': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
cache: DEFAULT_CACHE_STRATEGY,
|
|
returns: {} as FullUser,
|
|
props: z.object({id: z.string()}).strict(),
|
|
summary: 'Get full profile by user ID',
|
|
tag: 'Users',
|
|
},
|
|
// 'user/by-id/:id/lite': {
|
|
// method: 'GET',
|
|
// authed: false,
|
|
// rateLimited: false,
|
|
// cache: DEFAULT_CACHE_STRATEGY,
|
|
// returns: {} as DisplayUser,
|
|
// props: z.object({id: z.string()}).strict(),
|
|
// summary: 'Get lightweight profile by user ID',
|
|
// },
|
|
'user/by-id/:id/block': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({id: z.string()}).strict(),
|
|
summary: 'Block a user by their ID',
|
|
tag: 'Users',
|
|
},
|
|
'user/by-id/:id/unblock': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({id: z.string()}).strict(),
|
|
summary: 'Unblock a user by their ID',
|
|
tag: 'Users',
|
|
},
|
|
'ban-user': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z
|
|
.object({
|
|
userId: z.string(),
|
|
unban: z.boolean().optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Ban or unban a user',
|
|
tag: 'Admin',
|
|
},
|
|
'create-user': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as {user: User; privateUser: PrivateUser},
|
|
props: z
|
|
.object({
|
|
deviceToken: z.string().optional(),
|
|
adminToken: z.string().optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Create a new user (admin or onboarding flow)',
|
|
tag: 'Users',
|
|
},
|
|
'create-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as Row<'profiles'>,
|
|
props: baseProfilesSchema,
|
|
summary: 'Create a new profile for the authenticated user',
|
|
tag: 'Profiles',
|
|
},
|
|
report: {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z
|
|
.object({
|
|
contentOwnerId: z.string(),
|
|
contentType: z.enum(['user', 'comment', 'contract']),
|
|
contentId: z.string(),
|
|
description: z.string().optional(),
|
|
parentId: z.string().optional(),
|
|
parentType: z.enum(['contract', 'post', 'user']).optional(),
|
|
})
|
|
.strict(),
|
|
returns: {} as any,
|
|
summary: 'Submit a report for content or a user',
|
|
tag: 'Moderation',
|
|
},
|
|
me: {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
cache: DEFAULT_CACHE_STRATEGY,
|
|
props: z.object({}),
|
|
returns: {} as FullUser,
|
|
summary: 'Get the authenticated user full data',
|
|
tag: 'Users',
|
|
},
|
|
'me/data': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({}),
|
|
// Full JSON export of the user's data, including
|
|
// profile, private user, answers, messages, endorsements, bookmarks, etc.
|
|
// We deliberately keep this loosely typed as it's meant for export/inspection.
|
|
returns: {} as Record<string, any>,
|
|
summary: 'Download all data for the authenticated user as JSON',
|
|
tag: 'Users',
|
|
},
|
|
'me/update': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
name: z.string().trim().min(1).optional(),
|
|
username: z.string().trim().min(1).optional(),
|
|
avatarUrl: z.string().optional(),
|
|
link: z.record(z.string().nullable()).optional(),
|
|
}),
|
|
returns: {} as FullUser,
|
|
summary: 'Update authenticated user profile and settings',
|
|
tag: 'Users',
|
|
},
|
|
'update-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: combinedProfileSchema.partial(),
|
|
returns: {} as ProfileRow,
|
|
summary: 'Update profile fields for the authenticated user',
|
|
tag: 'Profiles',
|
|
},
|
|
'update-notif-settings': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
type: z.string() as z.ZodType<notification_preference>,
|
|
medium: z.enum(['email', 'browser', 'mobile']),
|
|
enabled: z.boolean(),
|
|
}),
|
|
summary: 'Update a notification preference for the user',
|
|
tag: 'Notifications',
|
|
},
|
|
'me/delete': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({}),
|
|
summary: 'Delete the authenticated user account',
|
|
tag: 'Users',
|
|
},
|
|
'me/private': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
returns: {} as PrivateUser,
|
|
summary: 'Get private user data for the authenticated user',
|
|
tag: 'Users',
|
|
},
|
|
'search-users': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
cache: DEFAULT_CACHE_STRATEGY,
|
|
returns: [] as FullUser[],
|
|
props: z
|
|
.object({
|
|
term: z.string(),
|
|
limit: z.coerce.number().gte(0).lte(20).default(500),
|
|
page: z.coerce.number().gte(0).default(0),
|
|
})
|
|
.strict(),
|
|
summary: 'Search users by term with pagination',
|
|
tag: 'Users',
|
|
},
|
|
'compatible-profiles': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({userId: z.string()}),
|
|
returns: {} as {
|
|
// profile: Profile
|
|
// compatibleProfiles: Profile[]
|
|
profileCompatibilityScores: {
|
|
[userId: string]: CompatibilityScore
|
|
}
|
|
},
|
|
summary: 'Find profiles compatible with a given user',
|
|
tag: 'Profiles',
|
|
},
|
|
'remove-pinned-photo': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {success: true},
|
|
props: z
|
|
.object({
|
|
userId: z.string(),
|
|
})
|
|
.strict(),
|
|
summary: 'Remove the pinned photo from a profile',
|
|
tag: 'Profiles',
|
|
},
|
|
'create-compatibility-question': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
|
|
options: z.record(z.string(), z.number()),
|
|
}),
|
|
summary: 'Create a new compatibility question with options',
|
|
tag: 'Compatibility',
|
|
},
|
|
'set-compatibility-answer': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as Row<'compatibility_answers'>,
|
|
props: z
|
|
.object({
|
|
questionId: z.number(),
|
|
multipleChoice: z.number(),
|
|
prefChoices: z.array(z.number()),
|
|
importance: z.number(),
|
|
explanation: z.string().nullable().optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Submit or update a compatibility answer',
|
|
tag: 'Compatibility',
|
|
},
|
|
'get-profile-answers': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({userId: z.string()}).strict(),
|
|
returns: {} as {
|
|
status: 'success'
|
|
answers: Row<'compatibility_answers'>[]
|
|
},
|
|
summary: 'Get compatibility answers for a profile',
|
|
tag: 'Compatibility',
|
|
},
|
|
'get-compatibility-questions': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
locale: z.string().optional(),
|
|
keyword: z.string().optional(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
questions: (Row<'compatibility_prompts'> & {
|
|
answer_count: number
|
|
score: number
|
|
})[]
|
|
},
|
|
summary: 'Retrieve compatibility questions and stats',
|
|
tag: 'Compatibility',
|
|
},
|
|
'delete-compatibility-answer': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
id: z.number(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Delete a compatibility answer',
|
|
tag: 'Compatibility',
|
|
},
|
|
'like-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
targetUserId: z.string(),
|
|
remove: z.boolean().optional(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Like or unlike a profile',
|
|
tag: 'Profiles',
|
|
},
|
|
'ship-profiles': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
targetUserId1: z.string(),
|
|
targetUserId2: z.string(),
|
|
remove: z.boolean().optional(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Create or remove a ship between two profiles',
|
|
tag: 'Profiles',
|
|
},
|
|
'get-likes-and-ships': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z
|
|
.object({
|
|
userId: z.string(),
|
|
})
|
|
.strict(),
|
|
returns: {} as {
|
|
status: 'success'
|
|
likesReceived: LikeData[]
|
|
likesGiven: LikeData[]
|
|
ships: ShipData[]
|
|
},
|
|
summary: 'Fetch likes and ships for a user',
|
|
tag: 'Profiles',
|
|
},
|
|
'has-free-like': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({}).strict(),
|
|
returns: {} as {
|
|
status: 'success'
|
|
hasFreeLike: boolean
|
|
},
|
|
summary: 'Check whether the user has a free like available',
|
|
tag: 'Profiles',
|
|
},
|
|
'star-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
targetUserId: z.string(),
|
|
remove: z.boolean().optional(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Star or unstar a profile',
|
|
tag: 'Profiles',
|
|
},
|
|
'hide-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
hiddenUserId: z.string(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Hide a profile for the current user',
|
|
tag: 'Profiles',
|
|
},
|
|
'unhide-profile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
hiddenUserId: z.string(),
|
|
}),
|
|
returns: {} as {
|
|
status: 'success'
|
|
},
|
|
summary: 'Unhide a previously hidden profile for the current user',
|
|
tag: 'Profiles',
|
|
},
|
|
'get-hidden-profiles': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z
|
|
.object({
|
|
limit: z.coerce.number().min(1).max(200).optional(),
|
|
offset: z.coerce.number().min(0).optional(),
|
|
})
|
|
.strict(),
|
|
returns: {} as {
|
|
status: 'success'
|
|
hidden: {
|
|
id: string
|
|
name: string
|
|
username: string
|
|
avatarUrl?: string | null
|
|
createdTime?: string
|
|
}[]
|
|
count: number
|
|
},
|
|
summary: 'Get the list of profiles the current user has hidden',
|
|
tag: 'Profiles',
|
|
},
|
|
'get-profiles': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z
|
|
.object({
|
|
limit: z.coerce.number().gt(0).lte(20).optional().default(20),
|
|
after: z.string().optional(),
|
|
// Search and filter parameters
|
|
name: z.string().optional(),
|
|
genders: arraybeSchema.optional(),
|
|
education_levels: arraybeSchema.optional(),
|
|
pref_gender: arraybeSchema.optional(),
|
|
pref_age_min: z.coerce.number().optional(),
|
|
pref_age_max: z.coerce.number().optional(),
|
|
drinks_min: z.coerce.number().optional(),
|
|
drinks_max: z.coerce.number().optional(),
|
|
big5_openness_min: z.coerce.number().optional(),
|
|
big5_openness_max: z.coerce.number().optional(),
|
|
big5_conscientiousness_min: z.coerce.number().optional(),
|
|
big5_conscientiousness_max: z.coerce.number().optional(),
|
|
big5_extraversion_min: z.coerce.number().optional(),
|
|
big5_extraversion_max: z.coerce.number().optional(),
|
|
big5_agreeableness_min: z.coerce.number().optional(),
|
|
big5_agreeableness_max: z.coerce.number().optional(),
|
|
big5_neuroticism_min: z.coerce.number().optional(),
|
|
big5_neuroticism_max: z.coerce.number().optional(),
|
|
religion: arraybeSchema.optional(),
|
|
pref_relation_styles: arraybeSchema.optional(),
|
|
pref_romantic_styles: arraybeSchema.optional(),
|
|
diet: arraybeSchema.optional(),
|
|
political_beliefs: arraybeSchema.optional(),
|
|
mbti: arraybeSchema.optional(),
|
|
interests: arraybeSchema.optional(),
|
|
causes: arraybeSchema.optional(),
|
|
work: arraybeSchema.optional(),
|
|
relationship_status: arraybeSchema.optional(),
|
|
languages: arraybeSchema.optional(),
|
|
wants_kids_strength: z.coerce.number().optional(),
|
|
has_kids: z.coerce.number().optional(),
|
|
is_smoker: zBoolean.optional().optional(),
|
|
shortBio: zBoolean.optional().optional(),
|
|
geodbCityIds: arraybeSchema.optional(),
|
|
lat: z.coerce.number().optional(),
|
|
lon: z.coerce.number().optional(),
|
|
radius: z.coerce.number().optional(),
|
|
compatibleWithUserId: z.string().optional(),
|
|
skipId: z.string().optional(),
|
|
locale: z.string().optional(),
|
|
orderBy: z
|
|
.enum(['last_online_time', 'created_time', 'compatibility_score'])
|
|
.optional()
|
|
.default('last_online_time'),
|
|
})
|
|
.strict(),
|
|
returns: {} as {
|
|
status: 'success' | 'fail'
|
|
profiles: Profile[]
|
|
count: number
|
|
},
|
|
summary: 'List profiles with filters, pagination and ordering',
|
|
tag: 'Profiles',
|
|
},
|
|
'get-options': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {},
|
|
props: z
|
|
.object({
|
|
table: z.enum(OPTION_TABLES),
|
|
locale: z.string().optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Get profile options like interests',
|
|
tag: 'Profiles',
|
|
},
|
|
'update-options': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {},
|
|
props: z
|
|
.object({
|
|
table: z.enum(OPTION_TABLES),
|
|
values: arraybeSchema.optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Update profile options like interests',
|
|
tag: 'Profiles',
|
|
},
|
|
'create-comment': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
userId: z.string(),
|
|
content: contentSchema,
|
|
replyToCommentId: z.string().optional(),
|
|
}),
|
|
returns: {} as any,
|
|
summary: 'Create a comment or reply',
|
|
tag: 'Profiles',
|
|
},
|
|
'hide-comment': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
props: z.object({
|
|
commentId: z.string(),
|
|
hide: z.boolean(),
|
|
}),
|
|
returns: {} as any,
|
|
summary: 'Hide or unhide a comment',
|
|
tag: 'Profiles',
|
|
},
|
|
'get-channel-memberships': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
channelId: z.coerce.number().optional(),
|
|
createdTime: z.string().optional(),
|
|
lastUpdatedTime: z.string().optional(),
|
|
limit: z.coerce.number(),
|
|
}),
|
|
returns: {
|
|
channels: [] as PrivateMessageChannel[],
|
|
memberIdsByChannelId: {} as {[channelId: string]: string[]},
|
|
},
|
|
summary: 'List private message channel memberships',
|
|
tag: 'Messages',
|
|
},
|
|
'get-channel-messages': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
channelId: z.coerce.number(),
|
|
limit: z.coerce.number(),
|
|
id: z.coerce.number().optional(),
|
|
}),
|
|
returns: [] as PrivateChatMessage[],
|
|
summary: 'Retrieve messages for a private channel',
|
|
tag: 'Messages',
|
|
},
|
|
'get-channel-seen-time': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
channelIds: z.array(z.coerce.number()).or(z.coerce.number()).transform(arrify),
|
|
}),
|
|
returns: [] as [number, string][],
|
|
summary: 'Get last seen times for one or more channels',
|
|
tag: 'Messages',
|
|
},
|
|
'set-channel-seen-time': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({
|
|
channelId: z.coerce.number(),
|
|
}),
|
|
summary: 'Set last seen time for a channel',
|
|
tag: 'Messages',
|
|
},
|
|
'set-last-online-time': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
summary: 'Update the user last online timestamp',
|
|
tag: 'Users',
|
|
},
|
|
'get-notifications': {
|
|
method: 'GET',
|
|
authed: true,
|
|
rateLimited: false,
|
|
returns: [] as Notification[],
|
|
props: z
|
|
.object({
|
|
after: z.coerce.number().optional(),
|
|
limit: z.coerce.number().gte(0).lte(1000).default(100),
|
|
})
|
|
.strict(),
|
|
summary: 'Fetch notifications for the authenticated user',
|
|
tag: 'Notifications',
|
|
},
|
|
'create-private-user-message': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
content: contentSchema,
|
|
channelId: z.number(),
|
|
}),
|
|
summary: 'Send a message in a private channel',
|
|
tag: 'Messages',
|
|
},
|
|
'create-private-user-message-channel': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
userIds: z.array(z.string()),
|
|
}),
|
|
summary: 'Create a new private message channel between users',
|
|
tag: 'Messages',
|
|
},
|
|
'update-private-user-message-channel': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
channelId: z.number(),
|
|
notifyAfterTime: z.number(),
|
|
}),
|
|
summary: 'Update settings for a private message channel',
|
|
tag: 'Messages',
|
|
},
|
|
'leave-private-user-message-channel': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
channelId: z.number(),
|
|
}),
|
|
summary: 'Leave a private message channel',
|
|
tag: 'Messages',
|
|
},
|
|
'edit-message': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
messageId: z.number(),
|
|
content: contentSchema,
|
|
}),
|
|
summary: 'Edit a private message',
|
|
tag: 'Messages',
|
|
},
|
|
'delete-message': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
messageId: z.number(),
|
|
}),
|
|
summary: 'Delete a private message',
|
|
tag: 'Messages',
|
|
},
|
|
'react-to-message': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
messageId: z.number(),
|
|
reaction: z.string(),
|
|
toDelete: z.boolean().optional(),
|
|
}),
|
|
summary: 'Add or remove a reaction to a message',
|
|
tag: 'Messages',
|
|
},
|
|
// 'get-message-reactions': {
|
|
// method: 'GET',
|
|
// authed: true,
|
|
// rateLimited: false,
|
|
// returns: {} as {
|
|
// reactions: Record<string, number>
|
|
// },
|
|
// props: z.object({
|
|
// messageId: z.string(),
|
|
// }),
|
|
// summary: 'Get reactions for a message',
|
|
// tag: 'Messages',
|
|
// },
|
|
'create-vote': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
title: z.string().min(1),
|
|
isAnonymous: z.boolean(),
|
|
description: contentSchema,
|
|
}),
|
|
summary: 'Create a new vote/poll',
|
|
tag: 'Votes',
|
|
},
|
|
vote: {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
voteId: z.number(),
|
|
priority: z.number(),
|
|
choice: z.enum(['for', 'abstain', 'against']),
|
|
}),
|
|
summary: 'Cast a vote on an existing poll',
|
|
tag: 'Votes',
|
|
},
|
|
'search-location': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
term: z.string(),
|
|
limit: z.number().optional(),
|
|
}),
|
|
summary: 'Search for a location by text',
|
|
tag: 'Locations',
|
|
},
|
|
'search-near-city': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
cityId: z.string(),
|
|
radius: z.number().min(1).max(500),
|
|
}),
|
|
summary: 'Find places near a GeoDB city ID within a radius',
|
|
tag: 'Locations',
|
|
},
|
|
contact: {
|
|
method: 'POST',
|
|
authed: false,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
content: contentSchema,
|
|
userId: z.string().optional(),
|
|
}),
|
|
summary: 'Send a contact/support message',
|
|
tag: 'Contact',
|
|
},
|
|
'get-messages-count': {
|
|
method: 'GET',
|
|
authed: false,
|
|
rateLimited: false,
|
|
props: z.object({}),
|
|
returns: {} as {count: number},
|
|
summary: 'Get the total number of messages (public endpoint)',
|
|
tag: 'Messages',
|
|
},
|
|
'save-subscription': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
subscription: z.record(z.any()),
|
|
}),
|
|
summary: 'Save a push/browser subscription for the user',
|
|
tag: 'Notifications',
|
|
},
|
|
'save-subscription-mobile': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
token: z.string(),
|
|
}),
|
|
summary: 'Save a mobile push subscription for the user',
|
|
tag: 'Notifications',
|
|
},
|
|
'create-bookmarked-search': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as Row<'bookmarked_searches'>,
|
|
props: z.object({
|
|
search_filters: z.any().optional(),
|
|
location: z.any().optional(),
|
|
search_name: z.string().nullable().optional(),
|
|
}),
|
|
summary: 'Create a bookmarked search for quick reuse',
|
|
tag: 'Searches',
|
|
},
|
|
'delete-bookmarked-search': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
id: z.number(),
|
|
}),
|
|
summary: 'Delete a bookmarked search by ID',
|
|
tag: 'Searches',
|
|
},
|
|
'cancel-event': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as {success: boolean},
|
|
props: z.object({
|
|
eventId: z.string(),
|
|
}),
|
|
summary: 'Cancel an event (creator only)',
|
|
tag: 'Events',
|
|
},
|
|
'rsvp-event': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as {success: boolean},
|
|
props: z.object({
|
|
eventId: z.string(),
|
|
status: z.enum(['going', 'maybe', 'not_going']),
|
|
}),
|
|
summary: 'RSVP to an event',
|
|
tag: 'Events',
|
|
},
|
|
'cancel-rsvp': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as {success: boolean},
|
|
props: z.object({
|
|
eventId: z.string(),
|
|
}),
|
|
summary: 'Cancel RSVP to an event',
|
|
tag: 'Events',
|
|
},
|
|
'create-event': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: true,
|
|
returns: {} as any,
|
|
props: z.object({
|
|
title: z.string().min(1).max(200),
|
|
description: z.string().max(2000).optional(),
|
|
locationType: z.enum(['in_person', 'online']),
|
|
locationAddress: z.string().max(500).optional(),
|
|
locationUrl: z.string().url().max(500).optional(),
|
|
eventStartTime: z.string().datetime(),
|
|
eventEndTime: z.string().datetime().optional(),
|
|
maxParticipants: z.number().int().min(1).optional(),
|
|
}),
|
|
summary: 'Create a new event',
|
|
tag: 'Events',
|
|
},
|
|
'get-events': {
|
|
method: 'GET',
|
|
authed: false,
|
|
rateLimited: false,
|
|
returns: {} as {
|
|
upcoming: any[]
|
|
past: any[]
|
|
},
|
|
props: z.object({}),
|
|
summary: 'Get all public events split into upcoming and past',
|
|
tag: 'Events',
|
|
},
|
|
'update-event': {
|
|
method: 'POST',
|
|
authed: true,
|
|
rateLimited: false,
|
|
returns: {} as {success: boolean},
|
|
props: z
|
|
.object({
|
|
eventId: z.string(),
|
|
title: z.string().min(1).max(200),
|
|
description: z.string().max(2000).optional(),
|
|
locationType: z.enum(['in_person', 'online']),
|
|
locationAddress: z.string().max(500).optional(),
|
|
locationUrl: z.string().url().max(500).optional(),
|
|
eventStartTime: z.string(),
|
|
eventEndTime: z.string().optional(),
|
|
maxParticipants: z.number().min(1).max(1000).optional(),
|
|
})
|
|
.strict(),
|
|
summary: 'Update an existing event',
|
|
tag: 'Events',
|
|
},
|
|
} as const)
|
|
|
|
export type APIPath = keyof typeof API
|
|
export type APISchema<N extends APIPath> = (typeof API)[N]
|
|
|
|
export type APIParams<N extends APIPath> = z.input<APISchema<N>['props']>
|
|
export type ValidatedAPIParams<N extends APIPath> = z.output<APISchema<N>['props']>
|
|
|
|
export type APIResponse<N extends APIPath> =
|
|
APISchema<N> extends {
|
|
returns: Record<string, any>
|
|
}
|
|
? APISchema<N>['returns']
|
|
: void
|
|
|
|
export type APIResponseOptionalContinue<N extends APIPath> =
|
|
| {continue: () => Promise<void>; result: APIResponse<N>}
|
|
| APIResponse<N>
|