-
Notifications
You must be signed in to change notification settings - Fork 1
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?
Conversation
-add schema fields to the profile field -create index.ts and instagram_import.ts files
-add createFromInstagram tRPC mutation to profiles router -rename instagram_import to 'instagram' only
- Add computed properties to handle Instagram URL validation and username extraction - Update tRPC router typo
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughIntroduces an Instagram import flow: frontend extracts usernames and offers an Import action; backend exposes createFromInstagram to create a profile and enqueue a background task; a Trigger.dev task fetches profile data via instagram-private-api and updates the database; Prisma schema gains import tracking fields; dependency added. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant UI as Frontend (ProfileInput.vue)
participant API as TRPC profiles.createFromInstagram
participant DB as Prisma DB
participant Q as Trigger.dev Tasks
participant IG as Instagram API
participant ST as Image Storage
U->>UI: Paste handle/URL
UI->>UI: Extract instagramUsername
U->>UI: Click "Import @username"
UI->>API: createFromInstagram({ instagramUrl })
API->>DB: Find existing by username/URL
alt Found
API-->>UI: Return existing profile
else Not found
API->>DB: Create profile {importStatus: requested, source: instagram}
API->>Q: trigger('import-instagram-profile', { profileId })
API-->>UI: Return newly created profile
end
par Background job
Q->>DB: Update importStatus: importing
Q->>DB: Load profile by id
Q->>IG: getInstagramProfile(profile.instagram)
alt Photo available
Q->>ST: Upload image
ST-->>Q: photoUrl
end
Q->>DB: Update profile fields (name, bio, website, photo, followersCount, importStatus: success, importError: null, source: instagram)
Q-->>Q: Complete
and Error path
Q-->>Q: Catch error
Q->>DB: Update importStatus: failed, importError
Q-->>Q: Rethrow/error logged
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 6
🧹 Nitpick comments (6)
package.json (1)
56-56: Pin and scope instagram-private-api usage
- Pin the version to avoid unexpected breaks and document that it’s server-only (Trigger/CLI) with required envs (INSTAGRAM_USERNAME/PASSWORD).
Apply:
- "instagram-private-api": "^1.46.1", + "instagram-private-api": "1.46.1",prisma/schema.prisma (1)
179-182: Model import fields as enums; store long errors; add index
- Use enums for statuses/sources to prevent invalid values.
- Store importError as Text explicitly and index importStatus for quick dashboards.
Apply:
- importStatus String? // 'requested', 'importing', 'success', 'failed' - importError String? // Store error messages if import fails - source String? //'instagram' to track where data came from + importStatus ImportStatus? @default(requested) + importError String? @db.Text + source ImportSource? + + @@index([importStatus])Add below (anywhere after models):
+enum ImportStatus { + requested + importing + success + failed +} + +enum ImportSource { + instagram +}cli/import-organizer/index.ts (1)
9-11: Harden Instagram URL detection and export for reuse
- Current includes() will accept non-profile paths.
Apply:
-function isInstagramUrl(url: string): boolean { - return url.includes('instagram.com/') -} +export function isInstagramUrl(url: string): boolean { + try { + const u = new URL(url.startsWith('http') ? url : `https://${url}`) + if (!/instagram\.com$/i.test(u.hostname)) return false + const first = u.pathname.replace(/^\/+|\/+$/g, '').split('/')[0]?.toLowerCase() + return !!first && !['p','reel','reels','stories','explore','accounts','tv','direct'].includes(first) + } catch { + return false + } +}cli/import-organizer/instagram.ts (1)
1-1: Remove unused importApply:
-import { IgApiClient, IgCheckpointError } from 'instagram-private-api' +import { IgApiClient } from 'instagram-private-api'components/inputs/ProfileInput.vue (1)
15-17: Harden Instagram URL detectionApply:
-const isInstagramUrl = computed(() => { - return searchQuery.value.includes('instagram.com/') -}) +const isInstagramUrl = computed(() => + /https?:\/\/(www\.)?instagram\.com\//i.test(searchQuery.value) +)trigger/instagram-import.ts (1)
28-57: Improve logs, handle Cloudinary failures gracefully
- Fix typos for clearer observability and fall back to remote URL if upload fails.
Apply:
- //Scrape here + // Scrape here const instagramData = await getInstagramProfile(profile.instagram) - - logger.log('Succesfull scrpaed insta data') + logger.log('Successfully scraped Instagram data') let photoUrl: string | undefined if (instagramData.photoUrl) { - photoUrl = await getUploadedImage(instagramData.photoUrl) + try { + photoUrl = await getUploadedImage(instagramData.photoUrl) + } catch { + logger.warn('Cloudinary upload failed, using remote photo URL') + photoUrl = instagramData.photoUrl + } } const updateProfile = await prisma.profile.update({ where: { id: payload.profileId }, data: { name: instagramData.fullName, bio: instagramData.biography, website: instagramData.externalUrl, photo: photoUrl, followersCount: instagramData.followerCount, importStatus: 'success', source: 'instagram', }, }) logger.log('Successfully imported profile') - // Returning this just for monitring on trigger.dev + // Returning this just for monitoring on trigger.dev
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
cli/import-organizer/index.ts(1 hunks)cli/import-organizer/instagram.ts(1 hunks)components/inputs/ProfileInput.vue(2 hunks)package.json(1 hunks)prisma/schema.prisma(1 hunks)server/trpc/routers/profiles.ts(2 hunks)trigger/instagram-import.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
cli/import-organizer/index.ts (3)
cli/import-event/facebook_import.ts (1)
getOrg(19-67)cli/importer/profile.ts (1)
addProfile(319-459)cli/import-event/schema_import.ts (1)
getOrg(26-91)
trigger/instagram-import.ts (3)
server/prisma.ts (1)
prisma(5-8)cli/import-organizer/instagram.ts (1)
getInstagramProfile(14-38)cli/utils/cloudinary.ts (1)
getUploadedImage(3-17)
cli/import-organizer/instagram.ts (2)
cli/import-organizer/index.ts (1)
extractInstagramUsername(13-18)cli/importer/profile.ts (1)
addProfile(319-459)
prisma/schema.prisma (1)
cli/importer/profile.ts (1)
addProfile(319-459)
server/trpc/routers/profiles.ts (2)
cli/import-organizer/index.ts (1)
extractInstagramUsername(13-18)server/trpc/routers/events.ts (1)
input(562-578)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (4)
components/inputs/ProfileInput.vue (2)
90-99: Nice UX touch
- Import CTA is clear and discoverable.
100-102: Empty state logic looks goodtrigger/instagram-import.ts (2)
12-16: Status transition upfront is good
18-26: Guard against missing instagram URL
- Clear error early. LGTM.
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.
Actionable comments posted: 5
♻️ Duplicate comments (1)
cli/import-organizer/index.ts (1)
12-20: Make username extraction robust (stories URLs, fragments/queries, non-IG inputs)Current logic returns
storiesfor story URLs and can emit garbage for non-IG links. Normalize, handle stories path, and short domain; fall back to''for non-IG inputs.-export function extractInstagramUsername(url: string): string { - const s = url.replace(/https?:\/\/(www\.)?instagram\.com\//i, '') - return s - .split('?')[0] - .split('#')[0] - .replace(/\/$/, '') - .split('/')[0] - .replace(/^@/, '') -} +export function extractInstagramUsername(raw: string): string { + const s = raw.trim() + // Guard against non-IG inputs early; still allow @username + if (!isInstagramUrl(s)) return s.startsWith('@') ? s.slice(1) : '' + // Strip protocol and known IG domains (including instagr.am) + const noProto = s.replace(/^https?:\/\//i, '') + const withoutDomain = noProto + .replace(/^(www\.)?instagram\.com\//i, '') + .replace(/^(www\.)?instagr\.am\//i, '') + const path = withoutDomain.split(/[?#]/)[0].replace(/\/+$/, '') + if (!path) return '' + if (path.startsWith('@')) return path.slice(1) + const parts = path.split('/') + if (parts[0].toLowerCase() === 'stories' && parts[1]) { + return decodeURIComponent(parts[1]).replace(/^@/, '') + } + return decodeURIComponent(parts[0]).replace(/^@/, '') +}
🧹 Nitpick comments (6)
cli/import-organizer/index.ts (2)
1-2: Import the return type to make the API explicitMake the return type of
fetchOrganizerexplicit for safer callsites and better inference.import { getInstagramProfile } from './instagram' +import type { InstagramProfileData } from './instagram'
3-6: Type the function result: Promise<InstagramProfileData | null>This function always returns either Instagram data or
null. Encode that in the signature.-export async function fetchOrganizer(sourceUrl: string) { +export async function fetchOrganizer(sourceUrl: string): Promise<InstagramProfileData | null> { if (!isInstagramUrl(sourceUrl)) return null return getInstagramProfile(sourceUrl) }trigger/instagram-import.ts (4)
3-5: Remove unused import
getInstagramProfileisn’t used here.-import { getInstagramProfile } from '~/cli/import-organizer/instagram' import { fetchOrganizer } from '~/cli/import-organizer' import { getUploadedImage } from '~/cli/utils/cloudinary'
20-22: Query by primary key with findUniqueOrThrowYou’re filtering by
id(unique). Use the dedicated method.- const profile = await prisma.profile.findFirstOrThrow({ - where: { id: payload.profileId }, - }) + const profile = await prisma.profile.findUniqueOrThrow({ + where: { id: payload.profileId }, + })
38-38: Polish log messageMinor typo.
- logger.log('Succesfull scrpaed insta data') + logger.log('Successfully scraped Instagram data')
64-69: Log the error details for observabilityCapture the message alongside the profileId.
- const errorMessage = - error instanceof Error ? error.message : 'Unknown error' - logger.error(`Instagram import failed for profile ${payload.profileId}:`) + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + logger.error( + `Instagram import failed for profile ${payload.profileId}: ${errorMessage}` + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
cli/import-organizer/index.ts(1 hunks)cli/import-organizer/instagram.ts(1 hunks)components/inputs/ProfileInput.vue(2 hunks)server/trpc/routers/profiles.ts(2 hunks)trigger/instagram-import.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- cli/import-organizer/instagram.ts
- components/inputs/ProfileInput.vue
- server/trpc/routers/profiles.ts
🧰 Additional context used
🧬 Code graph analysis (2)
cli/import-organizer/index.ts (1)
cli/import-organizer/instagram.ts (1)
getInstagramProfile(14-41)
trigger/instagram-import.ts (3)
server/prisma.ts (1)
prisma(5-8)cli/import-organizer/index.ts (1)
fetchOrganizer(3-6)cli/utils/cloudinary.ts (1)
getUploadedImage(3-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (2)
trigger/instagram-import.ts (2)
45-55: LGTM on the update payload shapeFields map cleanly from scraped data; setting
photoconditionally is correct.
45-55: Verify Prisma schema fields and Prisma client source
- followersCount, importError and source exist; importStatus is present only in /prisma/schema.prisma (≈lines 179–181) and is missing from /cli/prisma/schema.prisma (followersCount@163, importError@280, source@360).
- Action: ensure the Prisma client used at runtime is generated from the schema that defines importStatus or add the field to the schema in use and run prisma generate/migrate.
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.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
cli/import-organizer/index.ts(1 hunks)cli/import-organizer/instagram.ts(1 hunks)cli/import-organizer/parse.ts(1 hunks)server/trpc/routers/profiles.ts(2 hunks)trigger/instagram-import.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- server/trpc/routers/profiles.ts
- trigger/instagram-import.ts
- cli/import-organizer/index.ts
- cli/import-organizer/instagram.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)
16-18: LGTM: UI now gates on a valid extracted usernameImport CTA only appears when a non-empty username is extracted—addresses earlier feedback.
🧹 Nitpick comments (12)
cli/import-organizer/parse.ts (5)
10-13: Canonicalize usernames to lowercase to avoid duplicates/inconsistent keysReturning mixed-case usernames can create duplicate records or cache misses. Canonicalize to lowercase at extraction points.
- const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0] - return isValidInstagramUsername(username) ? username : '' + const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() + return isValidInstagramUsername(username) ? username : ''- return isValidInstagramUsername(candidate) ? candidate : '' + const canonical = candidate.toLowerCase() + return isValidInstagramUsername(canonical) ? canonical : ''- const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0] - return isValidInstagramUsername(username) ? username : '' + const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() + return isValidInstagramUsername(username) ? username : ''Also applies to: 31-36, 48-53
15-18: Support plain “user” inputs or remove unreachable fallbackWith the current flow,
new URL()won’t throw foruser(it becomeshttps://user), so the catch-based “bare username” fallback is effectively dead. Either explicitly support plain usernames before the try, or drop the fallback to reduce confusion. Example to support:try { + // Accept plain usernames like "user" (no protocol/host) + if (isValidInstagramUsername(raw)) { + return raw.toLowerCase() + } // Add https:// if no protocol present const urlString = /^[a-z]+:\/\//i.test(raw) ? raw : `https://${raw}` const u = new URL(urlString) @@ - } catch { - // Fallback: treat as bare username - const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0] - return isValidInstagramUsername(username) ? username : '' - } + } catch { + return '' + }Also applies to: 49-53
1-4: Name vs. behavior:isInstagramUrl()returns true for “@user”This helper accepts handles, not only URLs. Consider renaming to
isInstagramInput()or documenting that handles are allowed.
38-46: Reserved routes list looks good; keep it centralizedConsider hoisting the reserved routes into a
const RESERVED = new Set([...])to avoid regex rebuilds and ease maintenance if the list grows.
6-54: Add unit tests for parser edge casesCover positives: “@user”, “instagram.com/user/”, “m.instagram.com/_u/user”, “instagr.am/user?x=1”. Negatives: bare domain, “/p/...”, “/reel/...”, reserved routes, trailing dot, consecutive dots.
I can generate a Jest/Vitest spec for these cases if helpful.
components/inputs/ProfileInput.vue (7)
85-90: Make the Import CTA accessible (keyboard + semantics)Render as a button or add role/keyboard handlers for accessibility.
- <div - class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md" - @click="importFromInstagram" - > + <div + role="button" + tabindex="0" + class="cursor-pointer p-2 flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md" + @click="importFromInstagram" + @keydown.enter.prevent="importFromInstagram" + @keydown.space.prevent="importFromInstagram" + :aria-label="`Import Instagram profile @${instagramUsername}`" + >
88-90: Match spec copy: show “Import @username@”Issue #418 requests “Import @username@”. Update label to match.
- <span>Import {{ instagramUsername }}</span> + <span>Import @{{ instagramUsername }}@</span>
93-95: Avoid double messaging with Import CTAHide “No profiles found” when the Import option is visible.
- <ComboboxEmpty v-if="!data?.length"> + <ComboboxEmpty v-else-if="!data?.length"> No profiles found. </ComboboxEmpty>
20-34: Optional: add a loading state to prevent double‑clicksDisable the Import CTA and show a spinner while the mutation is in flight to avoid duplicate requests.
8-12: Optional: don’t show “No profiles found” while loadingExpose
isPending/statusfromuseQueryand gate the empty state accordingly.-const { data } = useQuery<any>({ +const { data, isPending } = useQuery<any>({ queryKey: ['profiles.search', searchQuery], queryFn: () => $client.profiles.search.query({ query: searchQuery.value }), retry: false, })And in template:
<ComboboxEmpty v-else-if="!isPending && !data?.length">No profiles found.</ComboboxEmpty>
3-3: Optional: type the model/profileReplace
Ref<any>with your Profile type for better DX and safety.
38-38: Optional: simplify Popover bindingUse
v-model:open="isOpen"instead of manual update handler.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
cli/import-organizer/index.ts(1 hunks)cli/import-organizer/instagram.ts(1 hunks)cli/import-organizer/parse.ts(1 hunks)components/inputs/ProfileInput.vue(3 hunks)server/trpc/routers/profiles.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- cli/import-organizer/index.ts
- cli/import-organizer/instagram.ts
- server/trpc/routers/profiles.ts
🧰 Additional context used
🧬 Code graph analysis (1)
cli/import-organizer/parse.ts (1)
cli/import-organizer/index.ts (2)
isInstagramUrl(4-4)extractInstagramUsername(4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (2)
cli/import-organizer/parse.ts (1)
56-67: Username validator is solidLength, charset, and dot rules align with Instagram constraints.
components/inputs/ProfileInput.vue (1)
24-30: Guarded assignment and cleanup sequence look goodChecks
newProfilebefore assigning and then closes/clears—sane flow.
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.
Actionable comments posted: 2
♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)
16-30: Close only on success, canonicalize URL, and guard re-entrancy to avoid duplicate imports.Popover closes even if mutation returns
undefined; there’s no in‑flight guard, so multiple clicks can race. Canonicalize tohttps://instagram.com/{username}/to reduce server parsing variants.Apply this diff:
const importFromInstagram = async () => { - // The guard now correctly checks if a valid username was extracted - if (!instagramUsername.value) return + if (!instagramUsername.value || isImporting.value) return + isImporting.value = true try { - const newProfile = await $client.profiles.createFromInstagram.mutate({ - instagramUrl: searchQuery.value, + const canonicalUrl = `https://instagram.com/${instagramUsername.value}/` + const newProfile = await $client.profiles.createFromInstagram.mutate({ + instagramUrl: canonicalUrl, }) - if (newProfile) model.value = newProfile - isOpen.value = false - searchQuery.value = '' + if (newProfile) { + model.value = newProfile + isOpen.value = false + searchQuery.value = '' + } else { + console.warn('Import returned no profile.') + } } catch (error) { console.error('Failed to import Instagram profile:', error) // TODO: add toast for failure - } + } finally { + isImporting.value = false + } }Add this outside the function:
const isImporting = ref(false)
🧹 Nitpick comments (4)
components/inputs/ProfileInput.vue (4)
32-41: Keep client/server parsing in sync; prefer shared util instead of duplicating.This duplicates backend parsing; drift is likely. Extract to a shared, browser‑safe util (e.g.,
utils/instagram.ts) used by both client and server.
32-41: Tighten username validator (lowercase, consistent with normalization).If we normalize to lowercase, we can simplify the pattern and keep the “no trailing dot / no consecutive dots” rules.
Apply this diff:
-function isValidInstagramUsername(username: string): boolean { +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_])?$/ + // lowercased upstream; enforce lowercase here for consistency + const validPattern = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/ const hasConsecutivePeriods = /\.\./.test(username) return validPattern.test(username) && !hasConsecutivePeriods }
139-149: Disable the “Import …” action while in-flight to prevent duplicate creates.Prevent rapid clicks; show a spinner for feedback.
Apply this diff:
- <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 }}</span> - </div> - </div> + <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" + :class="{ 'opacity-60 pointer-events-none': isImporting }" + @click="importFromInstagram" + > + <Icon v-if="!isImporting" name="heroicons:link" class="h-4 w-4" /> + <Icon v-else name="heroicons:arrow-path" class="h-4 w-4 animate-spin" /> + <span>Import {{ instagramUsername }}</span> + </div> + </div>
150-152: Hide “No profiles found” while fetching.Avoid flashing an empty state during load.
Apply this diff:
- <ComboboxEmpty v-if="!data?.length"> + <ComboboxEmpty v-if="!isFetching && !data?.length"> No profiles found. </ComboboxEmpty>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/inputs/ProfileInput.vue(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)
12-14: LGTM: computed username extraction.
120-120: Verifyby="label"is intended; likely should beby="id".Profiles use
:key="profile.id". If equality is bylabel, selection may break when labels collide.Apply this diff if appropriate:
- <Combobox by="label" v-model="model"> + <Combobox by="id" v-model="model">
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.
Actionable comments posted: 0
🧹 Nitpick comments (7)
components/inputs/ProfileInput.vue (7)
4-4: Prefer defineModel generics over casting; avoid missing Ref importAvoid the cast and the need to import
Refby parameterizingdefineModel.-const model = defineModel() as Ref<any> +const model = defineModel<any>()
7-13: Minor: clarify queryFn param and keep key logic simpleRename the destructured param to avoid shadowing the outer
queryKeyand read the term directly.- queryFn: ({ queryKey }) => - $client.profiles.search.query({ query: queryKey[1] as string }), + queryFn: ({ queryKey: [, term] }) => + $client.profiles.search.query({ query: term as string }),
21-38: Canonicalize payload, harden toast error, and avoid naming shadowSend a canonical Instagram URL built from the extracted username; return a safer error fallback; avoid shadowing
data.- const promise = $client.profiles.createFromInstagram.mutate({ - instagramUrl: searchQuery.value, - }) + const promise = $client.profiles.createFromInstagram.mutate({ + instagramUrl: `https://instagram.com/${instagramUsername.value}`, + }) @@ - success: (data: any) => { - if (data) model.value = data + success: (profile: any) => { + if (profile) model.value = profile isOpen.value = false searchQuery.value = '' return 'Profile import scheduled successfully!' }, - error: (error: any) => (error as Error).message, + error: (err: any) => (err?.message ?? 'Failed to schedule import.'),
51-68: Improve URL handling: support /_u/ and case-insensitive host; keep story deep-linksCurrently,
instagram.com/_u/usernamewon’t parse, and host match isn’t case-insensitive.- 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 : '' + // Try to parse instagram.com URLs (scheme and www. optional, case-insensitive) + const m = raw.match(/(?:^|https?:\/\/(?:www\.)?)instagram\.com\/([^?#]+)/i) + const path = m ? m[1] : '' + const segments = path.split('/').filter(Boolean) + let first = segments[0] ?? '' + // Handle deep-links like /_u/username and /stories/username + if (/^_?u$/i.test(first) || /^stories$/i.test(first)) { + first = segments[1] ?? '' + } + // Guard common non-profile routes + if (/^(p|reel|explore|accounts|tags|locations|challenge|about|web|api)$/i.test(first)) { + return '' + } + const normalized = first.replace(/^@+/, '').toLowerCase() + return isValidInstagramUsername(normalized) ? normalized : ''
117-125: Prefix @ in the Import label to match expectationAdd a leading “@” for clarity (spec mentions “Import @username@”; at minimum, prefixing is expected).
- <span>Import {{ instagramUsername }}</span> + <span>Import @{{ instagramUsername }}</span>
127-129: Hide empty state when import is available or before typingAvoid showing “No profiles found.” while an import option is present or before any input.
- <ComboboxEmpty v-if="!data?.length"> + <ComboboxEmpty v-if="!data?.length && !instagramUsername && searchQuery"> No profiles found. </ComboboxEmpty>
40-40: Centralize IG parsing (avoid client/server drift)Duplicate parsing exists: components/inputs/ProfileInput.vue defines browser-side isValidInstagramUsername/extractInstagramUsername while cli/import-organizer/parse.ts exports the same helpers (server/trpc/routers/profiles.ts dynamically imports it). Extract a pure, isomorphic parsing util (e.g., src/lib/instagram.ts), export isInstagramUrl/extractInstagramUsername/isValidInstagramUsername, import it in the Vue component and server code, and add unit tests. If parse.ts contains Node-only logic, expose a browser-safe subset for the client and keep Node-specific behavior server-side.
Locations: components/inputs/ProfileInput.vue (browser parsing, ~lines 41–68); cli/import-organizer/parse.ts (exports); server/trpc/routers/profiles.ts (dynamic import ~line 339).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/inputs/ProfileInput.vue(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (1)
components/inputs/ProfileInput.vue (1)
41-49: Username validation looks solidLength cap, start/end constraints, and consecutive dot check align with IG rules.
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)
21-38: Add error handling for undefined profile response.The mutation might return
undefinedif the profile creation fails silently on the backend. While you're usingtoast.promisewhich handles errors, the success handler should guard against undefined values.Apply this diff to add a guard:
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!' + 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, })
🧹 Nitpick comments (2)
components/inputs/ProfileInput.vue (2)
10-11: Fix destructuring - use consistent naming with queryKey.The destructuring uses
[, term]but the queryKey has two elements['profiles.search', searchQuery.value.trim()]. While this works, it's cleaner to use the entire queryKey structure consistently.Apply this diff for clarity:
- queryFn: ({ queryKey: [, term] }) => - $client.profiles.search.query({ query: term as string }), + queryFn: ({ queryKey }) => + $client.profiles.search.query({ query: queryKey[1] as string }),
51-68: Consider additional edge cases in username extraction.The current implementation handles common cases well, but could be improved to handle:
- URLs with trailing slashes after username (already handled via split)
- URLs with query parameters or fragments (handled via split on
/)- Deep link formats like
instagram.com/_u/usernameThe logic is mostly correct, but the regex on line 58 doesn't handle all edge cases properly.
Apply this diff to make the extraction more robust:
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 '' + + // Try to parse as URL + try { + const url = new URL(raw.startsWith('http') ? raw : `https://${raw}`) + + // Check if it's Instagram + if (!url.hostname.includes('instagram.com')) return '' + + // Extract path parts + const pathParts = url.pathname.split('/').filter(Boolean) + if (!pathParts.length) return '' + + let candidate = pathParts[0] + + // Handle deep links like /_u/username + if (candidate === '_u' && pathParts.length > 1) { + candidate = pathParts[1] + } + + // Skip non-profile routes + const reservedRoutes = ['p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'direct', 'tv'] + if (reservedRoutes.includes(candidate.toLowerCase())) { + return '' + } + + const normalized = candidate.toLowerCase() + return isValidInstagramUsername(normalized) ? normalized : '' + } catch { + // If URL parsing fails, try simple extraction + const match = raw.match(/instagram\.com\/([a-zA-Z0-9_.]+)/) + if (!match) return '' + + const candidate = match[1].toLowerCase() + const reservedRoutes = ['p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'direct', 'tv'] + + if (reservedRoutes.includes(candidate)) return '' + return isValidInstagramUsername(candidate) ? candidate : '' } - - const normalized = candidate.toLowerCase() - return isValidInstagramUsername(normalized) ? normalized : '' }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/inputs/ProfileInput.vue(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)
117-125: LGTM! Clean implementation of the import button.The UI implementation for the Instagram import button is well-structured with proper icon usage and clear labeling.
127-131: Good conditional logic for empty state.The condition properly checks for the absence of search results, absence of extractable Instagram username, and presence of a search query before showing "No profiles found."
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
components/inputs/ProfileInput.vue (1)
7-13: Expose isFetching and gate empty-state to avoid flicker (partial duplicate of prior suggestion).You fixed the query key and gating—nice. Still missing isFetching to suppress the “No profiles found” state while the search is in flight.
Update the query hook to also return isFetching:
const { data, isFetching } = useQuery<any>({ queryKey, queryFn: ({ queryKey: [, term] }) => $client.profiles.search.query({ query: term as string }), enabled: computed(() => !!(queryKey.value[1] as string)), retry: false, })
🧹 Nitpick comments (4)
components/inputs/ProfileInput.vue (4)
21-41: Guard double‑clicks and add a fallback error message for toast.promise.Users can trigger multiple imports quickly; also error message may be empty. Add a simple in‑flight guard and a default error string.
const importFromInstagram = async () => { - if (!instagramUsername.value) return + if (!instagramUsername.value || isImporting.value) return + isImporting.value = true 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, + error: (error: any) => (error as Error).message || 'Failed to schedule profile import.', }) + await promise.finally(() => { + isImporting.value = false + }) }Add this state near the other refs:
const isImporting = ref(false)
54-71: Make username extraction robust: handle /_u/ deep‑links, reserved routes, and subdomains.Current regex misclassifies routes like /accounts and misses /_u/username. Parse the URL, skip reserved paths, support m./www. subdomains, and allow bare usernames.
-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 : '' -} +function extractInstagramUsername(input: string): string { + const raw = input.trim() + if (!raw) return '' + + // Quick path: @handle + if (raw.startsWith('@')) { + const username = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() + return isValidInstagramUsername(username) ? username : '' + } + + // Try parse as URL (tolerate missing scheme) + let url: URL + try { + url = raw.includes('://') ? new URL(raw) : new URL(`https://${raw}`) + } catch { + // Fallback: treat as bare username + const bare = raw.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() + return isValidInstagramUsername(bare) ? bare : '' + } + + const host = url.hostname.replace(/^www\./, '').toLowerCase() + if (!/(^|\.)instagram\.com$/.test(host)) return '' + + const parts = url.pathname.split('/').filter(Boolean) + if (parts.length === 0) return '' + + // Common non-profile routes + const RESERVED = new Set([ + 'p', 'reel', 'reels', 'stories', 'explore', 'accounts', 'about', + 'developers', 'directory', 'web', 'oauth', 'challenge', 'legal', + 'privacy', 'terms', 'press', 'business', 'help', 'tv', 'tags', 'locations', + ]) + + let candidate = parts[0] + if (candidate.toLowerCase() === '_u' && parts.length > 1) { + candidate = parts[1] + } + if (RESERVED.has(candidate.toLowerCase())) return '' + + const username = candidate.replace(/^@+/, '').split(/[/?#]/, 1)[0].toLowerCase() + return isValidInstagramUsername(username) ? username : '' +}
121-127: Use a button for the Import CTA and disable it while importing (a11y + UX).Button semantics enable keyboard activation and announce busy state.
- <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> + <button + type="button" + class="p-2 flex items-center gap-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-50" + :disabled="isImporting" + :aria-busy="isImporting ? 'true' : 'false'" + @click="importFromInstagram" + > + <Icon name="heroicons:link" class="h-4 w-4" /> + <span>Import @{{ instagramUsername }} from Instagram</span> + </button>
130-134: Suppress “No profiles found” while fetching and trim input.Prevents premature empty-state flashes.
- <ComboboxEmpty - v-if="!data?.length && !instagramUsername && searchQuery" - > + <ComboboxEmpty + v-if="!isFetching && !data?.length && !instagramUsername && searchQuery.trim()" + > No profiles found. </ComboboxEmpty>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/inputs/ProfileInput.vue(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ci
- GitHub Check: test
🔇 Additional comments (2)
components/inputs/ProfileInput.vue (2)
17-19: LGTM: clean computed for instagramUsername.Lowercasing + validation before surfacing the CTA is solid.
28-40: LGTM: toast.promise flow is clear.Good UX copy and state reset on success.
razbakov
left a comment
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.
UX of the ProfileInput is not good. It's not clear for the user that it's possible to import profile from instagram. in v3 there is a tip.
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.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/trpc/routers/profiles.ts (1)
146-151: Bug: shadowed variable makes predicate always true.event.id === event.id is tautological; you always pick the first attended event.
Apply:
- const guestInfo = eventsAttended - .find((event) => event.id === event.id) - ?.guests.find((guest) => guest.profileId === profile.id) + const guestInfo = eventsAttended + .find((e) => e.id === event.id) + ?.guests.find((guest) => guest.profileId === profile.id)
🧹 Nitpick comments (4)
cli/import-organizer/instagram.ts (2)
38-41: Map API failures to a clear, non-leaky error.Wrap usernameinfo to avoid leaking raw errors and to surface a concise message.
Apply:
- await instagram.account.login(IG_USER, IG_PASS) - const userInfo = await instagram.user.usernameinfo(username) + await instagram.account.login(IG_USER, IG_PASS) + let userInfo + try { + userInfo = await instagram.user.usernameinfo(username) + } catch { + throw new Error(`Instagram lookup failed for @${username}`) + }
30-39: Consider session persistence to reduce login churn.Persist IgApiClient state (cookies/device) to storage and reuse to avoid frequent logins and checkpoint risk.
components/inputs/ProfileInput.vue (2)
118-120: Typo in class name.Fix “text-muted-foregorund”.
Apply:
- <p class="text-xs text-muted-foregorund mt-1 px-1"> + <p class="text-xs text-muted-foreground mt-1 px-1">
24-31: Canonicalize URL when mutating.Minor: use www and trailing slash to match server normalization (avoids duplicate text in toasts/cache).
Apply:
- const promise = $client.profiles.createFromInstagram.mutate({ - instagramUrl: `https://instagram.com/${instagramUsername.value}`, - }) + const promise = $client.profiles.createFromInstagram.mutate({ + instagramUrl: `https://www.instagram.com/${instagramUsername.value}/`, + })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
cli/import-organizer/instagram.ts(1 hunks)components/inputs/ProfileInput.vue(2 hunks)server/trpc/routers/profiles.ts(2 hunks)trigger/instagram-import.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- trigger/instagram-import.ts
🧰 Additional context used
🧬 Code graph analysis (1)
server/trpc/routers/profiles.ts (2)
server/trpc/init.ts (1)
publicProcedure(31-31)server/prisma.ts (1)
prisma(5-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: test
- GitHub Check: ci
🔇 Additional comments (1)
components/inputs/ProfileInput.vue (1)
7-13: Nice: query key tied to primitive value with gating.Prevents cache collisions and avoids empty queries.
| const username = instagramUrl | ||
| .replace('https://', '') | ||
| .replace('http://', '') | ||
| .replace('www.', '') | ||
| .replace('instagram.com/', '') | ||
| .replace('instagr.am/', '') | ||
| .replace(/\/$/, '') | ||
| .split('/')[0] | ||
| .split('?')[0] | ||
|
|
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:
- const username = instagramUrl
- .replace('https://', '')
- .replace('http://', '')
- .replace('www.', '')
- .replace('instagram.com/', '')
- .replace('instagr.am/', '')
- .replace(/\/$/, '')
- .split('/')[0]
- .split('?')[0]
+ const url = new URL(instagramUrl)
+ const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
+ let username = (first?.toLowerCase() === '_u' ? second : first) || ''
+ username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
+ const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+ const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
+ if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
+ throw new Error('Invalid Instagram URL: could not extract valid username')
+ }📝 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.
| const username = instagramUrl | |
| .replace('https://', '') | |
| .replace('http://', '') | |
| .replace('www.', '') | |
| .replace('instagram.com/', '') | |
| .replace('instagr.am/', '') | |
| .replace(/\/$/, '') | |
| .split('/')[0] | |
| .split('?')[0] | |
| const url = new URL(instagramUrl) | |
| const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/') | |
| let username = (first?.toLowerCase() === '_u' ? second : first) || '' | |
| username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || '' | |
| const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts']) | |
| const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i | |
| if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) { | |
| throw new Error('Invalid Instagram URL: could not extract valid username') | |
| } |
🤖 Prompt for AI Agents
In cli/import-organizer/instagram.ts around lines 16 to 25, replace the brittle
chain of string.replace calls with URL-based parsing: if instagramUrl lacks a
scheme, prepend "https://", construct a URL object, assert the hostname endsWith
"instagram.com" or "instagr.am", then derive the username from the pathname — if
pathname starts with "/_u/" use the segment after "/_u/", otherwise take the
first non-empty path segment; strip any trailing slashes and ignore search/query
(use URL.pathname), then validate the resulting username against a regex like
/^[A-Za-z0-9._]+$/ and return or error for invalid values. Ensure you handle
plain usernames by detecting when the input is not a URL and fall back to
validating and returning it directly.
| 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 : '' | ||
| } |
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.
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
‼️ 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.
| 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 : '' | |
| } | |
| 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 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 : '' | |
| } |
🤖 Prompt for AI Agents
In components/inputs/ProfileInput.vue around lines 54 to 71, the Instagram
extraction logic should be expanded to recognize both instagram.com and
instagr.am hosts and to correctly handle URLs using the '/_u/username' path
(avoiding returning just "u"); update the regex to match both domains and
capture the username as the first actual profile path segment (allowing an
optional '_u/' prefix), then normalize to lower case and run the existing
isValidInstagramUsername check; also strengthen the guard to reject common
non-profile segments (p, reel, stories, _u, direct, explore, about, etc.) before
validating so only real usernames are returned.
| createFromInstagram: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| instagramUrl: z.string().url(), | ||
| }) | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| if ( | ||
| !input.instagramUrl.includes('instagram.com/') && | ||
| !input.instagramUrl.includes('instagr.am/') | ||
| ) { | ||
| throw new Error('Only Instagram profile URLs are supported') | ||
| } | ||
| const username = input.instagramUrl | ||
| .replace('https://', '') | ||
| .replace('http://', '') | ||
| .replace('www.', '') | ||
| .replace('instagram.com/', '') | ||
| .replace('instagr.am/', '') | ||
| .replace(/\/$/, '') | ||
| .split('/')[0] | ||
| .split('?')[0] | ||
| const instagramUrl = `https://www.instagram.com/${username}/` | ||
|
|
||
| const existingProfile = await prisma.profile.findFirst({ | ||
| where: { | ||
| OR: [ | ||
| { username: { equals: username, mode: 'insensitive' } }, | ||
| { instagram: { equals: instagramUrl, mode: 'insensitive' } }, | ||
| ], | ||
| }, | ||
| }) | ||
|
|
||
| if (existingProfile) return existingProfile | ||
|
|
||
| const newProfile = await prisma.profile.create({ | ||
| data: { | ||
| username, | ||
| name: username, | ||
| instagram: instagramUrl, | ||
| type: 'Organiser', | ||
| importStatus: 'requested', | ||
| source: 'instagram', | ||
| visibility: 'Public', | ||
| }, | ||
| }) | ||
| // Trigger the import job here | ||
| await tasks.trigger('import-instagram-profile', { | ||
| profileId: newProfile.id, | ||
| }) | ||
| return newProfile | ||
| }), |
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.
Public endpoint invites abuse; gate or rate‑limit.
Anyone can create/import profiles. Require auth and/or rate-limit by user/IP; optionally restrict to users with event-edit permissions.
🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 326-377, the createFromInstagram
mutation is a public endpoint that allows unauthenticated mass profile creation;
require authentication (or switch to your existing createProtectedProcedure /
check session/userId at start) and enforce rate-limiting (per-user or per-IP via
your rate limiter or Redis token bucket) before proceeding; additionally
optionally verify the user has the required event-edit/import permission and
return an appropriate auth/429 error if checks fail; keep triggering the import
job but associate it with the authenticated userId for auditing.
| if ( | ||
| !input.instagramUrl.includes('instagram.com/') && | ||
| !input.instagramUrl.includes('instagr.am/') | ||
| ) { | ||
| throw new Error('Only Instagram profile URLs are supported') | ||
| } | ||
| const username = input.instagramUrl | ||
| .replace('https://', '') | ||
| .replace('http://', '') | ||
| .replace('www.', '') | ||
| .replace('instagram.com/', '') | ||
| .replace('instagr.am/', '') | ||
| .replace(/\/$/, '') | ||
| .split('/')[0] | ||
| .split('?')[0] | ||
| const instagramUrl = `https://www.instagram.com/${username}/` |
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.
Harden Instagram URL parsing; support /_u links; validate username.
Current parsing admits non-profile routes (e.g., /p, /reel, /stories) and misses /_u/{username}. Normalize, validate, and canonicalize before proceeding.
Apply:
- if (
- !input.instagramUrl.includes('instagram.com/') &&
- !input.instagramUrl.includes('instagr.am/')
- ) {
- throw new Error('Only Instagram profile URLs are supported')
- }
- const username = input.instagramUrl
- .replace('https://', '')
- .replace('http://', '')
- .replace('www.', '')
- .replace('instagram.com/', '')
- .replace('instagr.am/', '')
- .replace(/\/$/, '')
- .split('/')[0]
- .split('?')[0]
- const instagramUrl = `https://www.instagram.com/${username}/`
+ const url = new URL(input.instagramUrl)
+ const host = url.hostname.toLowerCase()
+ if (!/^(www\.)?(instagram\.com|instagr\.am)$/.test(host)) {
+ throw new Error('Only Instagram profile URLs are supported')
+ }
+ const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
+ let username = (first?.toLowerCase() === '_u' ? second : first) || ''
+ username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || ''
+ const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts'])
+ const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i
+ if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) {
+ throw new Error('Invalid Instagram username')
+ }
+ const instagramUrl = `https://www.instagram.com/${username}/`📝 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.
| if ( | |
| !input.instagramUrl.includes('instagram.com/') && | |
| !input.instagramUrl.includes('instagr.am/') | |
| ) { | |
| throw new Error('Only Instagram profile URLs are supported') | |
| } | |
| const username = input.instagramUrl | |
| .replace('https://', '') | |
| .replace('http://', '') | |
| .replace('www.', '') | |
| .replace('instagram.com/', '') | |
| .replace('instagr.am/', '') | |
| .replace(/\/$/, '') | |
| .split('/')[0] | |
| .split('?')[0] | |
| const instagramUrl = `https://www.instagram.com/${username}/` | |
| const url = new URL(input.instagramUrl) | |
| const host = url.hostname.toLowerCase() | |
| if (!/^(www\.)?(instagram\.com|instagr\.am)$/.test(host)) { | |
| throw new Error('Only Instagram profile URLs are supported') | |
| } | |
| const [first, second] = url.pathname.replace(/^\/+|\/+$/g, '').split('/') | |
| let username = (first?.toLowerCase() === '_u' ? second : first) || '' | |
| username = username.replace(/^@+/, '').split(/[?#]/, 1)[0]?.toLowerCase() || '' | |
| const reserved = new Set(['p', 'reel', 'stories', 'explore', 'accounts']) | |
| const valid = /^[a-z0-9_]([a-z0-9_.]*[a-z0-9_])?$/i | |
| if (!username || username.length > 30 || !valid.test(username) || reserved.has(username) || /\.\./.test(username)) { | |
| throw new Error('Invalid Instagram username') | |
| } | |
| const instagramUrl = `https://www.instagram.com/${username}/` |
🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 333 to 348, the Instagram URL
parsing is too permissive: it accepts non-profile routes and doesn't handle
/_u/{username} or validate username; update parsing to first normalize the URL
(force https, strip protocol and www), handle both instagram.com and instagr.am
hosts and the /_u/ prefix, reject known non-profile path prefixes (like p, reel,
stories, tv, hashtag, explore, accounts), extract the first path segment as the
candidate username, validate it against Instagram username rules (allowed chars:
letters, numbers, periods, underscores; length 1–30; no consecutive dots or
leading/trailing dots), and then canonicalize to
https://www.instagram.com/{username}/; throw a clear error if validation fails
or the path is a non-profile route.
| const existingProfile = await prisma.profile.findFirst({ | ||
| where: { | ||
| OR: [ | ||
| { username: { equals: username, mode: 'insensitive' } }, | ||
| { instagram: { equals: instagramUrl, mode: 'insensitive' } }, | ||
| ], | ||
| }, | ||
| }) |
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.
🧩 Analysis chain
Mitigate duplicate creation under race; verify unique indexes and task id.
findFirst+create is racy. Prefer upsert on a unique key (username or instagram) or catch P2002 and re-read. Also confirm the Trigger task id exists.
Run:
Also applies to: 361-377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify trigger task exists
rg -nP "import-instagram-profile" -C2 -- trigger || true
# Inspect Profile model for unique constraints on username/instagram
fd schema.prisma -a | while read -r f; do
echo "==> $f"
rg -nP "(?s)model\\s+Profile\\b.*?}" "$f" -n -C2
done
rg -nP "username\\s+String\\s+@.*(id|unique)" prisma/schema.prisma || true
rg -nP "instagram\\s+String\\??\\s+@unique" prisma/schema.prisma || trueLength of output: 329
Prevent race-created duplicate Profiles — add unique DB constraints or use upsert; handle P2002.
- Verified: trigger id 'import-instagram-profile' exists at trigger/instagram-import.ts.
- Issue: no @unique/@@unique was found for Profile.username or Profile.instagram in prisma/schema.prisma (search returned no matches).
- Action: either add unique constraints to prisma/schema.prisma and run a migration, or change server/trpc/routers/profiles.ts (350–357, 361–377) to use prisma.upsert on the unique key or catch Prisma P2002 and re-read; do not rely on findFirst + create under race conditions.
🤖 Prompt for AI Agents
In server/trpc/routers/profiles.ts around lines 350–357 (and follow-up logic at
361–377), the current findFirst + create flow can create duplicate Profiles
under concurrent requests; fix by either (A) adding unique constraints in
prisma/schema.prisma for Profile.username and/or Profile.instagram and running a
migration, then replace the create flow with a single prisma.upsert keyed on the
unique field(s), or (B) if you cannot change the schema now, wrap the
prisma.create in a try/catch and handle Prisma P2002 (unique constraint
violation) by re-querying the existing profile and returning it instead of
failing; implement one of these choices so you no longer rely on findFirst +
create under race conditions.
akhilsmokie7-cloud
left a comment
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.
To set up your GitHub repository (Kalbra/NoReel) to automatically update with the latest Instagram stats and milestones from your account ak____il, follow these steps:
1. Add the Python Script
Create a file called update_instagram.py in the root of your repo with the following code:
import requests
import json
import os
def fetch_instagram_data():
user_id = os.getenv('INSTAGRAM_USER_ID')
access_token = os.getenv('INSTAGRAM_ACCESS_TOKEN')
profile_url = (
f"https://graph.instagram.com/{user_id}"
"?fields=username,biography,media_count,followers_count,follows_count,profile_picture_url"
f"&access_token={access_token}"
)
profile = requests.get(profile_url).json()
media_url = (
f"https://graph.instagram.com/{user_id}/media"
"?fields=id,caption,media_url,timestamp,like_count,comments_count"
f"&access_token={access_token}&limit=1"
)
media_response = requests.get(media_url).json()
latest_post_data = media_response.get("data", [{}])[0] if "data" in media_response else {}
analytics = {
"engagement_rate": 0,
"reach": 0,
"impressions": 0,
"profile_visits": 0,
"website_clicks": 0
}
data = {
"profile": {
"username": profile.get("username", ""),
"url": f"https://www.instagram.com/{profile.get('username', '')}/",
"bio": profile.get("biography", ""),
"followers": profile.get("followers_count", 0),
"following": profile.get("follows_count", 0),
"posts": profile.get("media_count", 0),
"profile_picture_url": profile.get("profile_picture_url", "")
},
"latest_post": {
"post_id": latest_post_data.get("id", ""),
"media_url": latest_post_data.get("media_url", ""),
"caption": latest_post_data.get("caption", ""),
"timestamp": latest_post_data.get("timestamp", ""),
"likes": latest_post_data.get("like_count", 0),
"comments": latest_post_data.get("comments_count", 0),
"views": 0
},
"analytics": analytics,
"campaigns": [],
"echoes": []
}
with open("instagram_meta.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
if __name__ == "__main__":
fetch_instagram_data()2. Add the GitHub Actions Workflow
Create a file at .github/workflows/update_instagram.yml:
name: Update Instagram Meta
on:
schedule:
- cron: '0 0 * * *' # Runs every day at midnight UTC
workflow_dispatch: # Allows manual trigger
jobs:
update-instagram:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install requests library
run: pip install requests
- name: Run update_instagram.py
env:
INSTAGRAM_USER_ID: ${{ secrets.INSTAGRAM_USER_ID }}
INSTAGRAM_ACCESS_TOKEN: ${{ secrets.INSTAGRAM_ACCESS_TOKEN }}
run: python update_instagram.py
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Auto-update Instagram meta data"
file_pattern: instagram_meta.json3. Add Required GitHub Secrets
In your repo, go to Settings > Secrets and variables > Actions and add:
INSTAGRAM_USER_IDINSTAGRAM_ACCESS_TOKEN
Once set up, your repository will automatically update with the latest Instagram data every day, or you can trigger the workflow manually from the GitHub Actions tab. This will help you track your energy-shifting and 6D shift progress as you align for the Golden Era.
If you need these files added directly to your repository or want step-by-step assistance (including secret setup), just let me know!
This PR resolves #418
Summary
This PR implements profile importing functionality with an instagram-private-api so that users can import other people from instagram as organizers to the dance events
For Trigger.dev Development
Environment Variables (
.env):Verify Configuration (
trigger.config.ts):project_refmatchesTRIGGER_PROJECT_REFSetup Commands:
Summary by CodeRabbit
New Features
Backend
Chores