-
-
Notifications
You must be signed in to change notification settings - Fork 175
feat: package likes #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: package likes #712
Changes from all commits
4830f1d
5637945
e708ea5
551e01f
85b73d8
26411a9
cd6207c
784bcc7
b3d3995
a282987
53b3268
a69048d
f3d422e
4cbfb05
5787fc3
a8f9a02
202de5b
8c6b799
934fd6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { FetchError } from 'ofetch' | ||
| import type { LocationQueryRaw } from 'vue-router' | ||
|
|
||
| /** | ||
| * Redirect user to ATProto authentication | ||
| */ | ||
| export async function authRedirect(identifier: string, create: boolean = false) { | ||
| let query: LocationQueryRaw = { handle: identifier } | ||
| if (create) { | ||
| query = { ...query, create: 'true' } | ||
| } | ||
| await navigateTo( | ||
| { | ||
| path: '/api/auth/atproto', | ||
| query, | ||
| }, | ||
| { external: true }, | ||
| ) | ||
| } | ||
|
|
||
| export async function handleAuthError( | ||
| fetchError: FetchError, | ||
| userHandle?: string | null, | ||
| ): Promise<never> { | ||
| const errorMessage = fetchError?.data?.message | ||
| if (errorMessage === ERROR_NEED_REAUTH && userHandle) { | ||
| await authRedirect(userHandle) | ||
| } | ||
| throw fetchError | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { FetchError } from 'ofetch' | ||
| import { handleAuthError } from '~/utils/atproto/helpers' | ||
| import type { PackageLikes } from '#shared/types/social' | ||
|
|
||
| export type LikeResult = { success: true; data: PackageLikes } | { success: false; error: Error } | ||
|
|
||
| /** | ||
| * Like a package via the API | ||
| */ | ||
| export async function likePackage( | ||
| packageName: string, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| try { | ||
| const result = await $fetch<PackageLikes>('/api/social/like', { | ||
| method: 'POST', | ||
| body: { packageName }, | ||
| }) | ||
| return { success: true, data: result } | ||
| } catch (e) { | ||
| if (e instanceof FetchError) { | ||
| await handleAuthError(e, userHandle) | ||
| } | ||
| return { success: false, error: e as Error } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Unlike a package via the API | ||
| */ | ||
| export async function unlikePackage( | ||
| packageName: string, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| try { | ||
| const result = await $fetch<PackageLikes>('/api/social/like', { | ||
| method: 'DELETE', | ||
| body: { packageName }, | ||
| }) | ||
| return { success: true, data: result } | ||
| } catch (e) { | ||
| if (e instanceof FetchError) { | ||
| await handleAuthError(e, userHandle) | ||
| } | ||
| return { success: false, error: e as Error } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Toggle like status for a package | ||
| */ | ||
| export async function togglePackageLike( | ||
| packageName: string, | ||
| currentlyLiked: boolean, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| return currentlyLiked | ||
| ? unlikePackage(packageName, userHandle) | ||
| : likePackage(packageName, userHandle) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -168,6 +168,10 @@ export default defineNuxtConfig({ | |
| driver: 'fsLite', | ||
| base: './.cache/atproto-oauth/session', | ||
| }, | ||
| 'generic-cache': { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we probably also need to update modules/cache.ts to configure production redis
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May have to leave that one to you 😅. Do you have something to read up on that? The local generic-cache defined there is used just locally for dev and then uses upstash redis like you did for the oauth session lock when in production if that makes a difference. |
||
| driver: 'fsLite', | ||
| base: './.cache/generic', | ||
| }, | ||
| }, | ||
| typescript: { | ||
| tsConfig: { | ||
|
|
@@ -270,6 +274,11 @@ export default defineNuxtConfig({ | |
| 'virtua/vue', | ||
| 'semver', | ||
| 'validate-npm-package-name', | ||
| '@atproto/lex', | ||
| '@atproto/lex-data', | ||
| '@atproto/lex-json', | ||
| '@atproto/lex-schema', | ||
| '@atproto/lex-client', | ||
| ], | ||
| }, | ||
| }, | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about making xrpc routes (sth like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now we're just using the the server side oauth client so if we made this endpoint an XRPC it wouldn't be quite correct since it uses the cookie for authentication. But there is a plan for some XRPC endpoints for other things just need to make a middleware for it to authenticate the service auth jwt
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think all the they're not auth related, they just rely on a session.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import * as v from 'valibot' | ||
| import { Client } from '@atproto/lex' | ||
| import * as dev from '#shared/types/lexicons/dev' | ||
| import { PackageLikeBodySchema } from '#shared/schemas/social' | ||
| import { throwOnMissingOAuthScope } from '#server/utils/atproto/oauth' | ||
|
|
||
| export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { | ||
| const loggedInUsersDid = oAuthSession?.did.toString() | ||
|
|
||
| if (!oAuthSession || !loggedInUsersDid) { | ||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) | ||
| } | ||
|
|
||
| //Checks if the user has a scope to like packages | ||
| await throwOnMissingOAuthScope(oAuthSession, LIKES_SCOPE) | ||
|
|
||
| const body = v.parse(PackageLikeBodySchema, await readBody(event)) | ||
|
|
||
| const likesUtil = new PackageLikesUtils() | ||
|
|
||
| const getTheUsersLikedRecord = await likesUtil.getTheUsersLikedRecord( | ||
| body.packageName, | ||
| loggedInUsersDid, | ||
| ) | ||
|
|
||
| if (getTheUsersLikedRecord) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if it isn't truthy? somehow feels like something would have to be very wrong for that to happen, but we should log an error if so
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. I think some of this like logic is kind of "let's see what all comes of it stage". Do we have something for logging or is it just
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think just a regular console.error for now They should show up in the vercel logs |
||
| const client = new Client(oAuthSession) | ||
|
|
||
| await client.delete(dev.npmx.feed.like, { | ||
| rkey: getTheUsersLikedRecord.rkey, | ||
| }) | ||
| var result = await likesUtil.unlikeAPackageAndReturnLikes(body.packageName, loggedInUsersDid) | ||
| return result | ||
| } | ||
|
|
||
| console.warn( | ||
| `User ${loggedInUsersDid} tried to unlike a package ${body.packageName} but it was not liked by them.`, | ||
| ) | ||
|
|
||
| return await likesUtil.getLikes(body.packageName, loggedInUsersDid) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import * as v from 'valibot' | ||
| import { Client } from '@atproto/lex' | ||
| import * as dev from '#shared/types/lexicons/dev' | ||
| import type { UriString } from '@atproto/lex' | ||
| import { LIKES_SCOPE } from '#shared/utils/constants' | ||
| import { PackageLikeBodySchema } from '#shared/schemas/social' | ||
| import { throwOnMissingOAuthScope } from '~~/server/utils/atproto/oauth' | ||
|
|
||
| export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { | ||
| const loggedInUsersDid = oAuthSession?.did.toString() | ||
|
|
||
| if (!oAuthSession || !loggedInUsersDid) { | ||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) | ||
| } | ||
|
|
||
| //Checks if the user has a scope to like packages | ||
| await throwOnMissingOAuthScope(oAuthSession, LIKES_SCOPE) | ||
|
|
||
| const body = v.parse(PackageLikeBodySchema, await readBody(event)) | ||
|
|
||
| const likesUtil = new PackageLikesUtils() | ||
|
|
||
| // Checks to see if the user has liked the package already | ||
| const likesResult = await likesUtil.getLikes(body.packageName, loggedInUsersDid) | ||
| if (likesResult.userHasLiked) { | ||
| return likesResult | ||
| } | ||
|
|
||
| const subjectRef = PACKAGE_SUBJECT_REF(body.packageName) | ||
| const client = new Client(oAuthSession) | ||
|
|
||
| const like = dev.npmx.feed.like.$build({ | ||
| createdAt: new Date().toISOString(), | ||
| subjectRef: subjectRef as UriString, | ||
| }) | ||
|
|
||
| const result = await client.create(dev.npmx.feed.like, like) | ||
| if (!result) { | ||
| throw createError({ | ||
| status: 500, | ||
| message: 'Failed to create a like', | ||
| }) | ||
| } | ||
|
|
||
| return await likesUtil.likeAPackageAndReturnLikes(body.packageName, loggedInUsersDid, result.uri) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| export default eventHandlerWithOAuthSession(async (event, oAuthSession, _) => { | ||
| const packageName = getRouterParam(event, 'pkg') | ||
| if (!packageName) { | ||
| throw createError({ | ||
| status: 400, | ||
| message: 'package name not provided', | ||
| }) | ||
| } | ||
|
|
||
| const likesUtil = new PackageLikesUtils() | ||
| return await likesUtil.getLikes(packageName, oAuthSession?.did.toString()) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should probably be client-side only: