Folo/apps/main/src/index.ts

268 lines
8.2 KiB
TypeScript

import { electronApp, optimizer } from "@electron-toolkit/utils"
import { callWindowExpose } from "@follow/shared/bridge"
import { APP_PROTOCOL } from "@follow/shared/constants"
import { env } from "@follow/shared/env"
import { imageRefererMatches, selfRefererMatches } from "@follow/shared/image"
import { parse } from "cookie-es"
import { app, BrowserWindow, session } from "electron"
import type { Cookie } from "electron/main"
import squirrelStartup from "electron-squirrel-startup"
import { DEVICE_ID } from "./constants/system"
import { isDev, isMacOS } from "./env"
import { initializeAppStage0, initializeAppStage1 } from "./init"
import { updateProxy } from "./lib/proxy"
import { handleUrlRouting } from "./lib/router"
import { store } from "./lib/store"
import { registerAppTray } from "./lib/tray"
import { setBetterAuthSessionCookie, updateNotificationsToken } from "./lib/user"
import { registerUpdater } from "./updater"
import { cleanupOldRender } from "./updater/hot-updater"
import {
createMainWindow,
getMainWindow,
getMainWindowOrCreate,
windowStateStoreKey,
} from "./window"
if (isDev) console.info("[main] env loaded:", env)
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
console.info("[main] device id:", DEVICE_ID)
if (squirrelStartup) {
app.quit()
}
function bootstrap() {
initializeAppStage0()
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
return
}
let mainWindow: BrowserWindow
initializeAppStage1()
app.on("second-instance", (_, commandLine) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
}
const url = commandLine.pop()
if (url) {
handleOpen(url)
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// Set app user model id for windows
electronApp.setAppUserModelId(`re.${APP_PROTOCOL}`)
mainWindow = createMainWindow()
// restore cookies
const cookies = store.get("cookies") as Cookie[]
if (cookies) {
Promise.all(
cookies.map((cookie) => {
const setCookieDetails: Electron.CookiesSetDetails = {
url: apiURL,
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
expirationDate: cookie.expirationDate,
sameSite: cookie.sameSite as "unspecified" | "no_restriction" | "lax" | "strict",
}
return mainWindow.webContents.session.cookies.set(setCookieDetails)
}),
)
}
updateProxy()
registerUpdater()
registerAppTray()
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
// remove Electron, Follow from user agent
let userAgent = details.requestHeaders["User-Agent"]
if (userAgent) {
userAgent = userAgent.replace(/\s?Electron\/[\d.]+/, "")
userAgent = userAgent.replace(/\s?Follow\/[\d.a-zA-Z-]+/, "")
}
details.requestHeaders["User-Agent"] = userAgent
// set referer and origin
if (selfRefererMatches.some((item) => details.url.startsWith(item))) {
details.requestHeaders["Referer"] = "https://app.follow.is"
details.requestHeaders["Origin"] = "https://app.follow.is"
} else {
const refererMatch = imageRefererMatches.find((item) => item.url.test(details.url))
const referer = refererMatch?.referer
if (referer) {
details.requestHeaders["Referer"] = referer
}
}
callback({ cancel: false, requestHeaders: details.requestHeaders })
})
// handle session cookie when sign in with email in electron
session.defaultSession.webRequest.onHeadersReceived(
{
urls: [`${apiURL}/better-auth/sign-in/email?*`],
},
(detail, callback) => {
const { responseHeaders } = detail
if (responseHeaders?.["set-cookie"]) {
const cookies = responseHeaders["set-cookie"] as string[]
cookies.forEach((cookie) => {
const cookieObj = parse(cookie)
Object.keys(cookieObj).forEach((name) => {
const value = cookieObj[name]
mainWindow.webContents.session.cookies.set({
url: apiURL,
name,
value,
secure: true,
httpOnly: true,
domain: new URL(apiURL).hostname,
sameSite: "no_restriction",
})
})
})
}
callback({ cancel: false, responseHeaders })
},
)
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
mainWindow = getMainWindowOrCreate()
mainWindow.show()
})
app.on("open-url", (_, url) => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
} else {
mainWindow = createMainWindow()
}
url && handleOpen(url)
})
// for dev debug
if (process.env.NODE_ENV === "development") {
import("electron-devtools-installer").then(
({ default: installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS }) => {
;[REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS].forEach((extension) => {
installExtension(extension, {
loadExtensionOptions: { allowFileAccess: true },
})
.then((name) => console.info(`Added Extension: ${name}`))
.catch((err) => console.info("An error occurred:", err))
})
session.defaultSession.getAllExtensions().forEach((e) => {
session.defaultSession.loadExtension(e.path)
})
},
)
}
})
app.on("before-quit", async () => {
// store window pos when before app quit
const window = getMainWindow()
if (!window || window.isDestroyed()) return
const bounds = window.getBounds()
store.set(windowStateStoreKey, {
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
})
await session.defaultSession.cookies.flushStore()
const cookies = await session.defaultSession.cookies.get({})
store.set("cookies", cookies)
await cleanupOldRender()
})
const handleOpen = async (url: string) => {
const isValid = URL.canParse(url)
if (!isValid) return
const urlObj = new URL(url)
if (urlObj.hostname === "auth" || urlObj.pathname === "//auth") {
const ck = urlObj.searchParams.get("ck")
const userId = urlObj.searchParams.get("userId")
if (ck && apiURL) {
setBetterAuthSessionCookie(ck)
const cookie = parse(atob(ck))
Object.keys(cookie).forEach((name) => {
const value = cookie[name]
mainWindow.webContents.session.cookies.set({
url: apiURL,
name,
value,
secure: true,
httpOnly: true,
domain: new URL(apiURL).hostname,
sameSite: "no_restriction",
})
})
userId && (await callWindowExpose(mainWindow).clearIfLoginOtherAccount(userId))
mainWindow.reload()
updateNotificationsToken()
}
} else {
handleUrlRouting(url)
}
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (!isMacOS) {
app.quit()
}
})
app.on("before-quit", () => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((window) => window.destroy())
})
}
bootstrap()