-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Event Editor: Select Profile (import) #421
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?
Changes from all commits
ce63130
17528a5
a7d44aa
0a536c0
f2a35cc
d7436e9
1ed021d
8e21f83
471aca5
2805de5
4f86241
818075d
6414a7e
446d526
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,52 @@ | ||
| import { IgApiClient } from 'instagram-private-api' | ||
|
|
||
| export type InstagramProfileData = { | ||
| username: string | ||
| fullName: string | ||
| biography: string | ||
| photoUrl?: string | ||
| externalUrl?: string | ||
| followerCount: number | ||
| isVerified: boolean | ||
| } | ||
|
|
||
| export async function getInstagramProfile( | ||
| instagramUrl: string | ||
| ): Promise<InstagramProfileData> { | ||
| const username = instagramUrl | ||
| .replace('https://', '') | ||
| .replace('http://', '') | ||
| .replace('www.', '') | ||
| .replace('instagram.com/', '') | ||
| .replace('instagr.am/', '') | ||
| .replace(/\/$/, '') | ||
| .split('/')[0] | ||
| .split('?')[0] | ||
|
|
||
| if (!username) { | ||
| throw new Error('Invalid Instagram URL: could not extract valid username') | ||
| } | ||
| console.log(`Fetching instagram profile for ${username}`) | ||
| const instagram = new IgApiClient() | ||
|
|
||
| const IG_USER = process.env.INSTAGRAM_USERNAME | ||
| const IG_PASS = process.env.INSTAGRAM_PASSWORD | ||
| if (!IG_USER || !IG_PASS) { | ||
| throw new Error('INSTAGRAM_USERNAME and INSTAGRAM_PASSWORD must be set') | ||
| } | ||
|
|
||
| instagram.state.generateDevice(IG_USER) | ||
| await instagram.account.login(IG_USER, IG_PASS) | ||
| const userInfo = await instagram.user.usernameinfo(username) | ||
|
|
||
| console.log(`Successfully fetched data for @${username}`) | ||
| return { | ||
| username: userInfo.username, | ||
| fullName: userInfo.full_name || username, | ||
| biography: userInfo.biography || '', | ||
| photoUrl: userInfo.profile_pic_url || '', | ||
| externalUrl: userInfo.external_url || '', | ||
| followerCount: userInfo.follower_count || 0, | ||
| isVerified: userInfo.is_verified || false, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,74 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const model = defineModel() as Ref<any> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { toast } from 'vue-sonner' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const model = defineModel<any>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { $client } = useNuxtApp() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const searchQuery = ref('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const queryKey = computed(() => ['profiles.search', searchQuery.value.trim()]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data } = useQuery<any>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryKey: ['profiles.search', searchQuery], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryFn: () => $client.profiles.search.query({ query: searchQuery.value }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryFn: ({ queryKey: [, term] }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $client.profiles.search.query({ query: term as string }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enabled: computed(() => !!(queryKey.value[1] as string)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retry: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isOpen = ref(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const instagramUsername = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return extractInstagramUsername(searchQuery.value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const importFromInstagram = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!instagramUsername.value) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const promise = $client.profiles.createFromInstagram.mutate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instagramUrl: `https://instagram.com/${instagramUsername.value}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.promise(promise, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loading: 'Scheduling profile import...', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: (profile: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (profile) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model.value = profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen.value = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchQuery.value = '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'Profile import scheduled successfully!' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'Profile import scheduled but no profile returned' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: (error: any) => (error as Error).message, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Browser-safe duplication of the backend parsing logic. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isValidInstagramUsername(username: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!username || username.length === 0) return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (username.length > 30) return false // Instagram username limit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validPattern = /^[a-zA-Z0-9_]([a-zA-Z0-9_.]*[a-zA-Z0-9_])?$/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasConsecutivePeriods = /\.\./.test(username) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return validPattern.test(username) && !hasConsecutivePeriods | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function extractInstagramUsername(input: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = input.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw.startsWith('@')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return isValidInstagramUsername(username) ? username : '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidate = match ? match[1] : '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A guard for the most common non-profile routes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (/^(p|reel|stories)$/i.test(candidate)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = candidate.toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return isValidInstagramUsername(normalized) ? normalized : '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+71
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. Broaden extraction: support instagr.am and /_u; strengthen guards. Current regex misses instagr.am and mis-parses /_u links (e.g., imports “u”). Normalize and validate. Apply: function extractInstagramUsername(input: string): string {
const raw = input.trim()
if (raw.startsWith('@')) {
const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase()
return isValidInstagramUsername(username) ? username : ''
}
- const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/)
- const candidate = match ? match[1] : ''
-
- // A guard for the most common non-profile routes
- if (/^(p|reel|stories)$/i.test(candidate)) {
- return ''
- }
-
- const normalized = candidate.toLowerCase()
- return isValidInstagramUsername(normalized) ? normalized : ''
+ const hostRe = /^(?:https?:\/\/)?(?:www\.)?(instagram\.com|instagr\.am)\//i
+ const afterHost = hostRe.test(raw)
+ ? raw.replace(hostRe, '')
+ : raw
+ // Handle /_u/username deep links
+ const parts = afterHost.split(/[/?#]/).filter(Boolean)
+ let candidate = parts[0] && parts[0].toLowerCase() === '_u' ? parts[1] || '' : parts[0] || ''
+ candidate = candidate.replace(/^@+/, '').toLowerCase()
+ const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+ if (!candidate || reserved.has(candidate)) return ''
+ return isValidInstagramUsername(candidate) ? candidate : ''
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -57,9 +115,26 @@ const isOpen = ref(false) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxAnchor> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p class="text-xs text-muted-foregorund mt-1 px-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Or paste a link for an Instagram profile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxList class="w-[260px]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxEmpty> No profiles found. </ComboboxEmpty> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div v-if="instagramUsername"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @click="importFromInstagram" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon name="heroicons:link" class="h-4 w-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>Import @{{ instagramUsername }} from Instagram</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxEmpty | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| v-if="!data?.length && !instagramUsername && searchQuery" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| No profiles found. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ComboboxEmpty> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ComboboxItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
Use URL-based parsing; handle /_u and validate username.
String replaces are brittle; enforce host, support /_u/{username}, and validate.
Apply:
📝 Committable suggestion
🤖 Prompt for AI Agents