Skip to content
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

User following #319

Merged
merged 26 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afc9910
update
sheriffjimoh Nov 25, 2024
a216e5d
Merge branch 'develop' of https://github.com/bitcashorg/masterbots in…
sheriffjimoh Nov 25, 2024
ae07458
Merge branch 'develop' of https://github.com/bitcashorg/masterbots in…
sheriffjimoh Nov 29, 2024
a633c9b
Merge branch 'develop' of https://github.com/bitcashorg/masterbots in…
sheriffjimoh Dec 3, 2024
4223fb6
added follow user
sheriffjimoh Dec 3, 2024
e340424
update
sheriffjimoh Dec 3, 2024
0d32d2e
fix: upt hasura metadata databases, public_social_following.yaml
AndlerRL Dec 10, 2024
b664851
fix: upt masterbots.ai lib, utils.ts
AndlerRL Dec 10, 2024
140dd75
Merge branch 'develop' of https://github.com/bitcashorg/masterbots in…
sheriffjimoh Dec 11, 2024
a39ae9f
fix: user card
sheriffjimoh Dec 11, 2024
ede14ae
update
sheriffjimoh Dec 11, 2024
4f7b43f
fix: permission
sheriffjimoh Dec 11, 2024
4d2f9f9
update
sheriffjimoh Dec 15, 2024
84e46c9
fix: added more column for chatbot followee
sheriffjimoh Dec 15, 2024
b5e88c1
fix:foloow chatbot implementation
sheriffjimoh Dec 16, 2024
f3e94b8
update
sheriffjimoh Dec 16, 2024
71b98b3
Merge branch 'develop' of https://github.com/bitcashorg/masterbots in…
sheriffjimoh Dec 17, 2024
f584fcf
threads by following user/bots
sheriffjimoh Dec 17, 2024
29f4f7e
update
sheriffjimoh Dec 17, 2024
917aad8
update
sheriffjimoh Dec 17, 2024
e641ed2
update
sheriffjimoh Dec 17, 2024
8c009c5
update
sheriffjimoh Dec 17, 2024
4cee09c
update
sheriffjimoh Dec 17, 2024
b87b0c2
update
sheriffjimoh Dec 18, 2024
17b2740
update
sheriffjimoh Dec 18, 2024
3fc55dd
update
sheriffjimoh Dec 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ object_relationships:
using:
foreign_key_constraint_on: follower_id
insert_permissions:
- role: moderator
permission:
check:
follower_id:
_eq: X-Hasura-User-Id
columns:
- followee_id
- follower_id
comment: ""
- role: user
permission:
check:
Expand All @@ -22,19 +31,60 @@ insert_permissions:
- follower_id
comment: ""
select_permissions:
- role: anonymous
permission:
columns:
- created_at
- followee_id
- follower_id
filter: {}
comment: ""
- role: moderator
permission:
columns:
- created_at
- followee_id
- follower_id
filter: {}
comment: ""
- role: user
permission:
columns:
- followee_id
- follower_id
filter: {}
comment: ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Unrestricted access to follow relationships

The current select permissions allow unrestricted access (filter: {}) for all roles, including anonymous users. This could lead to:

  • Privacy issues as anyone can view all follow relationships
  • Potential for scraping user relationship data
  • Excessive data exposure to unauthenticated users

Consider adding appropriate filters:

  - role: anonymous
    permission:
      columns:
        - created_at
        - followee_id
        - follower_id
-     filter: {}
+     filter:
+       _or:
+         - followee_id:
+             _is_public: true
+         - follower_id:
+             _is_public: true

  - role: user
    permission:
      columns:
        - followee_id
        - follower_id
-     filter: {}
+     filter:
+       _or:
+         - follower_id:
+             _eq: X-Hasura-User-Id
+         - followee_id:
+             _eq: X-Hasura-User-Id

Committable suggestion skipped: line range outside the PR's diff.

update_permissions:
- role: moderator
permission:
columns:
- followee_id
- follower_id
filter:
follower_id:
_eq: X-Hasura-User-Id
check: null
comment: ""
- role: user
permission:
columns:
- followee_id
- follower_id
filter:
_or:
_and:
- follower_id:
_eq: X-Hasura-User-Id
- followee_id:
_eq: X-Hasura-User-Id
_neq: X-Hasura-User-Id
check: null
comment: ""
delete_permissions:
- role: moderator
permission:
filter:
follower_id:
_eq: X-Hasura-User-Id
comment: ""
- role: user
permission:
filter:
Expand Down
86 changes: 77 additions & 9 deletions apps/masterbots.ai/components/routes/profile/user-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Separator } from '@/components/ui/separator'
import Image from 'next/image'
import { BookUser, BotIcon, MessageSquareHeart, Wand2, ImagePlus, Loader } from 'lucide-react'
import { BookUser, BotIcon, MessageSquareHeart, Wand2, ImagePlus, Loader, UserIcon, Users } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { User } from 'mb-genql'
import { useProfile } from '@/lib/hooks/use-profile'
Expand All @@ -11,7 +11,10 @@ import toast from 'react-hot-toast'
import { UserPersonalityPrompt } from '@/lib/constants/prompts'
import { ChangeEvent, useCallback, useEffect, useState } from 'react'
import { useUploadImagesCloudinary } from '@/lib/hooks/use-cloudinary-upload'

import { formatNumber, Ifollowed } from '@/lib/utils'
import { useSession } from 'next-auth/react'
import { userFollowOrUnfollow } from '@/services/hasura/hasura.service';
import type { SocialFollowing } from 'mb-genql'

interface UserCardProps {
user: User | null
Expand All @@ -28,8 +31,9 @@ export function UserCard({ user, loading }: UserCardProps) {
const [userProfilePicture, setUserProfilePicture] = useState<string | null | undefined>(user?.profilePicture)
const [isUploadingImage, setIsUploadingImage] = useState(false);
const { uploadFilesCloudinary, error: cloudinaryError } = useUploadImagesCloudinary();
const { data: session } = useSession();
const [userData, setUserData] = useState<User | null>(user)


const userQuestions = user?.threads.map((thread) => {

if (!thread.messages?.length) {
Expand Down Expand Up @@ -159,9 +163,71 @@ export function UserCard({ user, loading }: UserCardProps) {
// update bio and topic when user changes
setBio(user?.bio)
setFavouriteTopic(user?.favouriteTopic)
setUserData(user)
}
, [user])



const handleFollowUser = async () => {
try {
// if no session is found, redirect to login
if (!session) {
toast.error('Please sign in to follow user')
setTimeout(() => {
window.location.href = '/auth/signin'
} , 2000)
return
}
const followerId = session.user?.id
const followeeId = user?.userId

const {success, error, follow} = await userFollowOrUnfollow({followerId, followeeId, jwt: session.user.hasuraJwt as string})
if(!success){
console.error('Failed to follow/Unfolow user:', error)
return
}

if(follow){
setUserData(prevUser => {
if (!prevUser) return prevUser;

const newFollower: SocialFollowing = {
followerId,
followeeId,
createdAt: new Date().toISOString(),
user: prevUser,
userByFollowerId: prevUser,
__typename: 'SocialFollowing'
};

return {
...prevUser,
followers: [...(prevUser.followers || []), newFollower]
} as User; // Assert the entire object as User type
});
toast.success(`You are now following ${user?.username}`)
}else{
setUserData(prevUser => {
if (!prevUser) return prevUser;

return {
...prevUser,
followers: prevUser.followers.filter(
follower => follower.followerId !== followerId || follower.followeeId !== followeeId
)
};
});
toast.success(`You have unfollowed ${user?.username}`)
}
} catch (error) {
toast.error('Failed to follow user')
console.error('Failed to follow user:', error)
}
}

const followed = Ifollowed({followers: userData?.followers, userId: session?.user?.id || ''})

return (
<div
className="dark:bg-[#09090B] bg-white rounded-lg md:w-[600px]
Expand Down Expand Up @@ -255,31 +321,33 @@ export function UserCard({ user, loading }: UserCardProps) {
</div>
</div>
{/* Implementation for this comes next :) */}
{/* <div className=' flex flex-col items-center md:mt-0 mt-7 space-y-3'>
<div className=' flex flex-col items-center md:mt-0 mt-7 space-y-3'>
{!isOwner && (
<button aria-label={`Follow ${user?.username}`} className="px-10 py-1 text-sm text-white rounded-md bg-[#BE17E8] hover:bg-[#BE17E8] dark:bg-[#83E56A] dark:hover:bg-[#83E56A] dark:text-black transition-colors">
<button onClick={handleFollowUser} aria-label={`Follow ${user?.username}`} className="px-10 py-1 text-sm text-white rounded-md bg-[#BE17E8] hover:bg-[#BE17E8] dark:bg-[#83E56A] dark:hover:bg-[#83E56A] dark:text-black transition-colors">
Follow
{
followed? 'ing' : ''
}
</button>
)}
<div className="flex space-x-6 md:pt-2 ">
<div className="flex flex-col items-center">
<span className="text-sm">Following</span>
<div className='flex items-center space-x-1'>
<UserIcon className="w-4 h-4" />
<span className="text-sm text-gray-500">313</span>
<span className="text-sm text-gray-500">{ formatNumber(userData?.following?.length || 0)}</span>
</div>

</div>
<div className="flex flex-col items-center">
<span className="text-sm">Followers</span>
<div className='flex items-center space-x-1'>
<Users className="w-4 h-4" />
<span className="text-sm text-gray-500">3.2k</span>
<span className="text-sm text-gray-500">{ formatNumber(userData?.followers?.length || 0)}</span>
</div>
</div>
</div>
</div> */}

</div>
</div>
</div>

Expand Down
39 changes: 38 additions & 1 deletion apps/masterbots.ai/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cleanPrompt } from '@/lib/helpers/ai-helpers'
import type { Message as AIMessage } from 'ai/react'
import { type ClassValue, clsx } from 'clsx'
import type { Message } from 'mb-genql'
import type { Message, SocialFollowing } from 'mb-genql'
import { customAlphabet } from 'nanoid'
import { twMerge } from 'tailwind-merge'
import type { ReactNode } from 'react'
Expand Down Expand Up @@ -318,3 +318,40 @@ export function parseClickableText(fullText: string): ParsedText {
export function cleanClickableText(text: string): string {
return text.replace(/(:|\.|\,)\s*$/, '')
}


export const formatNumber = (num: number) => {
const lookup = [
{ value: 1e9, symbol: 'B' },
{ value: 1e6, symbol: 'M' },
{ value: 1e3, symbol: 'K' }
];

// Handle negative numbers
const isNegative = num < 0;
const absNum = Math.abs(num);

// Find the appropriate suffix
const item = lookup.find(item => absNum >= item.value);

if (!item) {
// If number is smaller than 1000, return as is
return isNegative ? `-${absNum}` : absNum.toString();
}

// Calculate the formatted value with one decimal place
const formattedValue = (absNum / item.value).toFixed(1);

// Remove .0 if it exists
const cleanValue = formattedValue.replace('.0', '');

return `${isNegative ? '-' : ''}${cleanValue}${item.symbol}`;
};

interface IProps {
followers: SocialFollowing[] | undefined;
userId: string;
}
export const Ifollowed = ({followers, userId} : IProps) => {
return followers?.some(follower => follower.followerId === userId);
}
115 changes: 94 additions & 21 deletions apps/masterbots.ai/services/hasura/hasura.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,27 +819,21 @@ export async function getUserBySlug({ slug, isSameUser }: { slug: string, isSame
content: true
}
},
// followers: {
// followeeId: true,
// followerId: true,
// userByFollowerId: {
// username: true
// }
// },
// follower: {
// followeeId: true,
// followerId: true,
// userByFollowerId: {
// username: true
// }
// },
// following: {
// followeeId: true,
// followerId: true,
// userByFollowerId: {
// username: true
// }
// }
followers: {
followeeId: true,
followerId: true,
userByFollowerId: {
username: true
}
},

following: {
followeeId: true,
followerId: true,
userByFollowerId: {
username: true
}
}
}
} as const)

Expand Down Expand Up @@ -944,3 +938,82 @@ export async function subtractChatbotMetadataLabels(

return cleanResult(response)
}


export async function userFollowOrUnfollow({
followerId,
followeeId,
jwt
}: {
followerId: string;
followeeId: string;
jwt: string;
}) {
try {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Add validation to prevent users from following themselves

Should add an early check to validate that followerId !== followeeId to prevent self-follows.

Suggested change
try {
try {
if (followerId === followeeId) {
throw new Error('Users cannot follow themselves');
}

if (!jwt) {
throw new Error('Authentication required to follow/unfollow user');
}

const client = getHasuraClient({ jwt });

// First check if follow relationship exists
const { socialFollowing } = await client.query({
socialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
followeeId: true,
followerId: true,
}
});

if (!socialFollowing?.length) {
// Create new follow relationship
await client.mutation({
insertSocialFollowingOne: {
__args: {
object: {
followerId,
followeeId
}
},
followeeId: true,
followerId: true,
userByFollowerId: {
username: true
}
}
});
return { success: true, follow: true };
}

// Delete existing follow relationship
await client.mutation({
deleteSocialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
affectedRows: true,
returning:{
followeeId: true,
followerId: true
}
}
});

return { success: true, follow: false };

} catch (error) {
console.error('Error following/unfollowing user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to follow/unfollow user.'
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add essential validations to follow/unfollow functionality.

The implementation needs additional validation checks for robustness:

  1. Prevent users from following themselves
  2. Validate that both users exist before creating/deleting relationships
  3. Add more specific error messages for different failure scenarios
 export async function userFollowOrUnfollow({
   followerId,
   followeeId,
   jwt
 }: {
   followerId: string;
   followeeId: string;
   jwt: string;
 }) {
   try {
     if (!jwt) {
       throw new Error('Authentication required to follow/unfollow user');
     }
+    
+    // Prevent self-following
+    if (followerId === followeeId) {
+      throw new Error('Users cannot follow themselves');
+    }
 
     const client = getHasuraClient({ jwt });
 
+    // Validate both users exist
+    const { user } = await client.query({
+      user: {
+        __args: {
+          where: {
+            userId: { _in: [followerId, followeeId] }
+          }
+        },
+        userId: true
+      }
+    });
+    
+    if (!user || user.length !== 2) {
+      throw new Error('One or both users not found');
+    }
+
     // First check if follow relationship exists
     const { socialFollowing } = await client.query({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function userFollowOrUnfollow({
followerId,
followeeId,
jwt
}: {
followerId: string;
followeeId: string;
jwt: string;
}) {
try {
if (!jwt) {
throw new Error('Authentication required to follow/unfollow user');
}
const client = getHasuraClient({ jwt });
// First check if follow relationship exists
const { socialFollowing } = await client.query({
socialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
followeeId: true,
followerId: true,
}
});
if (!socialFollowing?.length) {
// Create new follow relationship
await client.mutation({
insertSocialFollowingOne: {
__args: {
object: {
followerId,
followeeId
}
},
followeeId: true,
followerId: true,
userByFollowerId: {
username: true
}
}
});
return { success: true, follow: true };
}
// Delete existing follow relationship
await client.mutation({
deleteSocialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
affectedRows: true,
returning:{
followeeId: true,
followerId: true
}
}
});
return { success: true, follow: false };
} catch (error) {
console.error('Error following/unfollowing user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to follow/unfollow user.'
};
}
}
export async function userFollowOrUnfollow({
followerId,
followeeId,
jwt
}: {
followerId: string;
followeeId: string;
jwt: string;
}) {
try {
if (!jwt) {
throw new Error('Authentication required to follow/unfollow user');
}
// Prevent self-following
if (followerId === followeeId) {
throw new Error('Users cannot follow themselves');
}
const client = getHasuraClient({ jwt });
// Validate both users exist
const { user } = await client.query({
user: {
__args: {
where: {
userId: { _in: [followerId, followeeId] }
}
},
userId: true
}
});
if (!user || user.length !== 2) {
throw new Error('One or both users not found');
}
// First check if follow relationship exists
const { socialFollowing } = await client.query({
socialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
followeeId: true,
followerId: true,
}
});
if (!socialFollowing?.length) {
// Create new follow relationship
await client.mutation({
insertSocialFollowingOne: {
__args: {
object: {
followerId,
followeeId
}
},
followeeId: true,
followerId: true,
userByFollowerId: {
username: true
}
}
});
return { success: true, follow: true };
}
// Delete existing follow relationship
await client.mutation({
deleteSocialFollowing: {
__args: {
where: {
followerId: { _eq: followerId },
followeeId: { _eq: followeeId }
}
},
affectedRows: true,
returning:{
followeeId: true,
followerId: true
}
}
});
return { success: true, follow: false };
} catch (error) {
console.error('Error following/unfollowing user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to follow/unfollow user.'
};
}
}