Skip to content

Commit

Permalink
improve sendDirectToThread
Browse files Browse the repository at this point in the history
  • Loading branch information
Amir Ziaei committed Apr 5, 2024
1 parent e5cd046 commit 28ea434
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 63 deletions.
4 changes: 2 additions & 2 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"type": "module",
"module": "src/entry.ts",
"scripts": {
"dev": "bun --watch .",
"start": "bun .",
"dev": "cross-env NODE_ENV=development bun --hot .",
"start": "cross-env NODE_ENV=production bun .",
"deploy": "fly deploy --remote-only",
"typecheck": "tsc",
"test:e2e:dev": "playwright test --ui",
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/modules/http/http-router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ErrorLike } from 'bun'
import { type ErrorLike } from 'bun'

const HttpMethods = {
GET: 'GET',
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ErrorLike } from 'bun'
import { type ErrorLike } from 'bun'
import {
UnauthorizedRequest,
requireAuthentication,
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/routes/healthcheck.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type RouteHandler } from '../modules/http/http-router'
import { type RouteHandler } from '../modules/http/http-router'

export const get: RouteHandler = async () => {
return new Response('OK')
Expand Down
215 changes: 157 additions & 58 deletions apps/app/src/services/instagram.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect } from '@playwright/test'
import { expect, type Locator, type Page } from '@playwright/test'
import { chromium } from 'playwright'

export type SendDirectToThreadParams = {
Expand All @@ -8,89 +8,188 @@ export type SendDirectToThreadParams = {
failureTraceSavePath: string
}

function logger() {
const prefix = 'sendDirectToThread'
return {
log(...args: Parameters<typeof console.log>) {
console.log(`${prefix}:`, ...args)
},
error(...args: Parameters<typeof console.error>) {
console.error(`${prefix}:`, ...args)
},
}
}

const { log, error } = logger()

export async function sendDirectToThread({
message,
threadId,
authStatePath,
failureTraceSavePath,
}: SendDirectToThreadParams) {
const browser = await chromium.launch({ headless: true })
console.log('sendDirectToThread: Chromium launched')
const context = await browser.newContext({
storageState: authStatePath,
locale: 'en-US',
timezoneId: 'Europe/Vilnius',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
})
console.log('sendDirectToThread: Context created')
await context.tracing.start({ screenshots: true, snapshots: true })
console.log('sendDirectToThread: Tracing started')
let browser, context
try {
browser = await chromium.launch({ headless: true })
log('Chromium launched')

context = await browser.newContext({
storageState: authStatePath,
locale: 'en-US',
timezoneId: 'Europe/Vilnius',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
})
log('Browser context created')

await context.tracing.start({ screenshots: true, snapshots: true })
log('Tracing started')

const page = await context.newPage()
console.log('sendDirectToThread: New page opened')
log('New browser page opened')

const threadUrl = `https://www.instagram.com/direct/t/${threadId}/`
await page.goto(threadUrl)
console.log('sendDirectToThread: Went to thread url')
log(`Gone to the chat page: ${threadUrl}`)

const pageUrl = page.url()
console.log(`sendDirectToThread: Instagram launched:: ${pageUrl}`)
log(`Current loaded page:: ${pageUrl}`)

const isAuthenticated = pageUrl === threadUrl
console.log(`sendDirectToThread: Authentication status: ${isAuthenticated}`)
log(`Authentication status: ${isAuthenticated}`)

if (!isAuthenticated) {
const continueAs = page.getByRole('button', {
name: `Continue as ${process.env.IG_USERNAME}`,
})
if (await continueAs.isVisible()) {
await continueAs.click()
console.log(
`sendDirectToThread: Continued as ${process.env.IG_USERNAME}`,
)
}
const declineCookies = page.getByRole('button', {
name: 'Decline optional cookies',
})
if (await declineCookies.isVisible()) {
await declineCookies.click()
console.log('sendDirectToThread: Declined optional cookies')
}
await page
.getByLabel('Phone number, username, or')
.fill(process.env.IG_USERNAME)
await page.getByLabel('Password').fill(process.env.IG_PASSWORD)
console.log('sendDirectToThread: Entered credentials')
await page.getByRole('button', { name: 'Log in', exact: true }).click()
console.log('sendDirectToThread: Logged in')
await page.getByRole('button', { name: 'Save info' }).click()
console.log('sendDirectToThread: Saved info')
if (await page.getByText('Turn on Notifications').isVisible()) {
await page.getByRole('button', { name: 'Not Now' }).click()
console.log('sendDirectToThread: Turned off notifications')
}
await expect(page.getByRole('link', { name: 'Home Home' })).toBeVisible()
log('Logging in...')
await login(page)
log('Logged in successfully')
await context.storageState({ path: authStatePath })
console.log('sendDirectToThread: Auth state saved')
log('Auth state saved')
}

if (await isElementVisible(page.getByText('Turn on Notifications'))) {
log(`Presented with the notifications preferences modal`)
await page.getByRole('button', { name: 'Not Now' }).click()
log('Turned off notifications')
}

if (page.url() !== threadUrl) {
console.log(
'Does not seem to be on the chat page, navigating there now...',
)
await page.goto(threadUrl)
console.log('sendDirectToThread: Thread opened')
log('Chat opened')
}

await page.getByRole('paragraph').fill(message)
console.log('sendDirectToThread: Message filled')
await new Promise(resolve => setTimeout(resolve, 1_000))
log('Message filled')

await wait(500)
await page.getByRole('button', { name: /send/i }).click()
console.log('sendDirectToThread: Send button pressed')
await new Promise(resolve => setTimeout(resolve, 2_000))
log('Send button pressed')

await wait(1000)
await expect(page.getByRole('paragraph')).toHaveText('')
await expect(
page.getByLabel('Messages in conversation with'),
).toContainText(message)
console.log('sendDirectToThread: Message sent successfully')
log('Message sent successfully')
} catch (err) {
console.error('sendDirectToThread: Error occurred', err)
await context.tracing.stop({ path: failureTraceSavePath })
error('an error occurred', err)
if (context) {
await context.tracing.stop({ path: failureTraceSavePath })
log(`Trace saved at: ${failureTraceSavePath}`)
}
throw err
} finally {
await browser.close()
console.log('sendDirectToThread: Browser closed')
if (browser) {
await browser.close()
log('Browser closed')
}
}
}

async function login(page: Page) {
if (await isElementVisible(page.getByText('Allow the use of cookies from'))) {
log(`Presented with the cookie preferences modal`)
await page.getByRole('button', { name: 'Decline optional cookies' }).click()
log('Declined cookies')
}

const continueAsBtn = page.getByRole('button', {
name: `Continue as ${process.env.IG_USERNAME}`,
})
const usernameTextField = page.getByLabel('Phone number, username, or')

const [isContinueAsBtnVisible, isUsernameTextFieldVisible] =
await Promise.all([
isElementVisible(continueAsBtn),
isElementVisible(usernameTextField),
])
log(`Continue as button visible: ${isContinueAsBtnVisible}`)
log(`Username field visible: ${isUsernameTextFieldVisible}`)

const status = isContinueAsBtnVisible
? 'AUTH_RECOVERABLE'
: isUsernameTextFieldVisible
? 'AUTH_REQUIRED'
: 'UNKNOWN'
log(`Auth status: ${status}`)

switch (status) {
case 'AUTH_RECOVERABLE': {
log(`Continuing as ${process.env.IG_USERNAME}`)
await continueAsBtn.click()
log(`Pressed continued as ${process.env.IG_USERNAME}`)
break
}
case 'AUTH_REQUIRED': {
await usernameTextField.fill(process.env.IG_USERNAME)
log('Entered username')
await page.getByLabel('Password').fill(process.env.IG_PASSWORD)
log('Entered password')
await page.getByRole('button', { name: 'Log in', exact: true }).click()
log('Pressed the Log in button')
break
}
case 'UNKNOWN':
default: {
throw new Error(
'Encountered an unfamiliar situation and auth flow cannot continue.',
)
}
}

const isLoggedIn = await isElementVisible(
page.getByRole('link', { name: 'Home Home' }),
)
if (!isLoggedIn) {
error('Could not find the Home link. Auth has appeared to failed')
throw new Error('Auth failed')
}

if (await isElementVisible(page.getByText('Save your login info?'))) {
await page.getByRole('button', { name: 'Save info' }).click()
log('Saved login info')
}

return page.waitForURL('**/?__coig_login=1')
}

function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}

async function isElementVisible(
locator: Locator,
{ timeout }: { timeout?: number } = { timeout: 5_000 },
) {
try {
await locator.waitFor({ timeout: timeout })
if (await locator.isVisible()) {
return true
}
return false
} catch {
return false
}
}

0 comments on commit 28ea434

Please sign in to comment.