Skip to content

Commit

Permalink
chore: Release v0.2.0-beta.0 (#1504)
Browse files Browse the repository at this point in the history
  • Loading branch information
DIYgod authored Nov 8, 2024
2 parents 94971da + 33d3906 commit e655c2b
Show file tree
Hide file tree
Showing 255 changed files with 9,007 additions and 4,871 deletions.
37 changes: 4 additions & 33 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,12 @@ body:
description: If possible, please provide a video that demonstrates the bug.
validations:
required: false
- type: dropdown
id: app-type
attributes:
label: App Type
options:
- Web
- Electron / App
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The version of the app you are using.
placeholder: 1.0.0
validations:
required: true

- type: dropdown
id: os
attributes:
label: Operating System
options:
- Windows 11
- Windows 10
- macOS
- Linux
validations:
required: true
- type: input
id: browser-info
- type: textarea
id: environment
attributes:
label: Browser Info
description: If you are using the web version, please provide your browser info.
placeholder: Browser name and version
label: Environment
description: Please provide the environment in which you are using the application. You can find this information by going to Settings > About and clicking the copy button next to the version tag.

- type: checkboxes
id: checkboxes
Expand Down
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
// ["tw\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
["tw`([^`]*)`", "([^`]*)"]
["tw`([^`]*)`", "([^`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"]
],
// If you do not want to autofix some rules on save
// You can put this in your user settings or workspace settings
Expand Down
89 changes: 88 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions api/vercel_webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import crypto from "node:crypto"

import type { VercelRequest, VercelResponse } from "@vercel/node"
import getRawBody from "raw-body"

export default async function handler(request: VercelRequest, response: VercelResponse) {
const { WEBHOOK_SECRET: INTEGRATION_SECRET } = process.env

if (typeof INTEGRATION_SECRET != "string") {
return response.status(400).json({
code: "invalid_secret",
error: "No integration secret found",
})
}

const rawBody = await getRawBody(request)
const bodySignature = sha1(rawBody, INTEGRATION_SECRET)

if (bodySignature !== request.headers["x-vercel-signature"]) {
return response.status(403).json({
code: "invalid_signature",
error: "signature didn't match",
})
}

const json = JSON.parse(rawBody.toString("utf-8"))

switch (json.type) {
// https://vercel.com/docs/observability/webhooks-overview/webhooks-api#deployment.succeeded
case "deployment.succeeded": {
const { target } = json.payload || json.data

if (target === "production") {
await purgeCloudflareCache()
} else {
console.info(`Skipping non-production deployment: ${target}`, json)
}
}
// ...
}

return response.status(200).end("OK")
}

function sha1(data: Buffer, secret: string): string {
return crypto.createHmac("sha1", secret).update(data).digest("hex")
}

export const config = {
api: {
bodyParser: false,
},
}

async function purgeCloudflareCache() {
const { CF_TOKEN, CF_ZONE_ID } = process.env

if (typeof CF_TOKEN !== "string" || typeof CF_ZONE_ID !== "string") {
throw new TypeError("No Cloudflare token or zone ID found")
}

const apiUrl = `https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache`

const manifestPath = await fetch(
`https://app.follow.is/assets/manifest.txt?t=${Date.now()}`,
).then((res) => res.text())

const allPath = manifestPath.split("\n").map((path) => `https://app.follow.is/${path}`)

// Function to delay execution
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

const taskPromise = [] as Promise<Response>[]
// Batch processing
for (let i = 0; i < allPath.length; i += 30) {
const batch = allPath.slice(i, i + 30)

const r = fetch(apiUrl, {
method: "POST",
headers: {
Authorization: CF_TOKEN,
},
body: JSON.stringify({
files: batch,
}),
})

taskPromise.push(r)

// Delay for 0.5 seconds between batches
if (i + 30 < allPath.length) {
await delay(500)
}
}

const result = await Promise.allSettled(taskPromise)
console.info(`Success: ${result.filter((r) => r.status === "fulfilled").length}`)
console.info(`Failed: ${result.filter((r) => r.status === "rejected").length}`)
}
13 changes: 7 additions & 6 deletions apps/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@
"@egoist/tipc": "0.3.2",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@eneris/push-receiver": "4.2.0",
"@eneris/push-receiver": "4.3.0",
"@follow/shared": "workspace:*",
"@mozilla/readability": "^0.5.0",
"@sentry/electron": "5.6.0",
"@sentry/electron": "5.7.0",
"builder-util-runtime": "9.2.10",
"electron-context-menu": "4.0.4",
"electron-log": "5.2.0",
"electron-squirrel-startup": "1.0.1",
"electron-updater": "^6.3.9",
"fast-folder-size": "2.3.0",
"font-list": "1.5.1",
"i18next": "^23.16.0",
"i18next": "^23.16.4",
"linkedom": "^0.18.5",
"lowdb": "7.0.1",
"msedge-tts": "1.3.4",
Expand All @@ -45,9 +46,9 @@
},
"devDependencies": {
"@types/lodash-es": "4.17.12",
"@types/node": "^22.7.5",
"electron": "33.0.0",
"@types/node": "^22.8.7",
"electron": "33.0.2",
"electron-devtools-installer": "3.2.0",
"hono": "4.6.5"
"hono": "4.6.8"
}
}
3 changes: 3 additions & 0 deletions apps/main/src/constants/app.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
// 5min
export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5

// https://github.com/electron/electron/issues/25081
export const START_IN_TRAY_ARGS = "--start-in-tray"
3 changes: 2 additions & 1 deletion apps/main/src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"
const __dirname = fileURLToPath(new URL(".", import.meta.url))
const iconMap = {
prod: path.join(__dirname, "../../resources/icon.png"),
dev: path.join(__dirname, "../../resources/icon-dev.png"),
dev: path.join(__dirname, "../../static/icon-dev.png"),
}
export const getIconPath = () => iconMap[process.env.NODE_ENV === "development" ? "dev" : "prod"]
export const getTrayIconPath = () => path.join(__dirname, "../../resources/tray-icon.png")
22 changes: 14 additions & 8 deletions apps/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ 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 { setAuthSessionToken, updateNotificationsToken } from "./lib/user"
import { registerUpdater } from "./updater"
import { createMainWindow, getMainWindow, windowStateStoreKey } from "./window"
import {
createMainWindow,
getMainWindow,
getMainWindowOrCreate,
windowStateStoreKey,
} from "./window"

if (isDev) console.info("[main] env loaded:", env)

Expand Down Expand Up @@ -65,6 +71,7 @@ function bootstrap() {

updateProxy()
registerUpdater()
registerAppTray()

session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
// remove Electron, Follow from user agent
Expand All @@ -81,8 +88,10 @@ function bootstrap() {
details.requestHeaders["Origin"] = "https://app.follow.is"
} else {
const refererMatch = imageRefererMatches.find((item) => item.url.test(details.url))
const referer = refererMatch?.referer || details.url
details.requestHeaders["Referer"] = referer
const referer = refererMatch?.referer
if (referer) {
details.requestHeaders["Referer"] = referer
}
}

callback({ cancel: false, requestHeaders: details.requestHeaders })
Expand All @@ -91,11 +100,8 @@ function bootstrap() {
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.
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createMainWindow()
}

if (mainWindow) mainWindow.show()
mainWindow = getMainWindowOrCreate()
mainWindow.show()
})

app.on("open-url", (_, url) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/main/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { app, nativeTheme, Notification, shell } from "electron"
import contextMenu from "electron-context-menu"

import { getIconPath } from "./helper"
import { clearCacheCronJob } from "./lib/cleaner"
import { t } from "./lib/i18n"
import { store } from "./lib/store"
import { updateNotificationsToken } from "./lib/user"
Expand Down Expand Up @@ -59,6 +60,7 @@ export const initializeAppStage1 = () => {
registerMenuAndContextMenu()

registerPushNotifications()
clearCacheCronJob()
}

let contextMenuDisposer: () => void
Expand Down
4 changes: 3 additions & 1 deletion apps/main/src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ofetch } from "ofetch"

import type { AppType } from "../../../../packages/shared/src/hono"
import { logger } from "../logger"
import { getAuthSessionToken } from "./user"
import { getAuthSessionToken, getUser } from "./user"

const abortController = new AbortController()
export const apiFetch = ofetch.create({
Expand Down Expand Up @@ -33,10 +33,12 @@ export const apiClient = hc<AppType>("", {
fetch: async (input, options = {}) => apiFetch(input.toString(), options),
headers() {
const authSessionToken = getAuthSessionToken()
const user = getUser()
return {
"X-App-Version": PKG.version,
"X-App-Dev": process.env.NODE_ENV === "development" ? "1" : "0",
Cookie: authSessionToken ? `authjs.session-token=${authSessionToken}` : "",
"User-Agent": `Follow/${PKG.version}${user?.id ? ` uid: ${user.id}` : ""}`,
}
},
})
85 changes: 84 additions & 1 deletion apps/main/src/lib/cleaner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { statSync } from "node:fs"
import fsp from "node:fs/promises"
import { createRequire } from "node:module"
import path from "node:path"
import { promisify } from "node:util"

import { callWindowExpose } from "@follow/shared/bridge"
import { dialog } from "electron"
import { app, dialog } from "electron"

import { logger } from "~/logger"
import { getMainWindow } from "~/window"

import { t } from "./i18n"
import { store, StoreKey } from "./store"

const require = createRequire(import.meta.url)
const fastFolderSize = require("fast-folder-size") as any as typeof import("fast-folder-size")

export const clearAllDataAndConfirm = async () => {
const win = getMainWindow()
Expand Down Expand Up @@ -53,3 +64,75 @@ export const clearAllData = async () => {
caller.toast.error(`Error resetting app data: ${error.message}`)
}
}
const fastFolderSizeAsync = promisify(fastFolderSize)
export const getCacheSize = async () => {
const cachePath = path.join(app.getPath("userData"), "cache")

// Size is in bytes
const sizeInBytes = await fastFolderSizeAsync(cachePath)
return sizeInBytes || 0
}

const getCachedFilesRecursive = async (dir: string, result: string[] = []) => {
const files = await fsp.readdir(dir)

for (const file of files) {
const filePath = path.join(dir, file)
const stat = await fsp.stat(filePath)
if (stat.isDirectory()) {
const files = await getCachedFilesRecursive(filePath)
result.push(...files)
} else {
result.push(filePath)
}
}
return result
}

let timer: any = null
export const clearCacheCronJob = () => {
if (timer) {
timer = clearInterval(timer)
}
timer = setInterval(
async () => {
const hasLimit = store.get(StoreKey.CacheSizeLimit)

if (!hasLimit) {
return
}

const cacheSize = await getCacheSize()

const limitByteSize = hasLimit * 1024 * 1024
if (cacheSize > limitByteSize) {
const shouldCleanSize = cacheSize - limitByteSize - 1024 * 1024 * 50 // 50MB

const cachePath = path.join(app.getPath("userData"), "cache")
const files = await getCachedFilesRecursive(cachePath)
// Sort by last modified
files.sort((a, b) => {
const aStat = statSync(a)
const bStat = statSync(b)
return bStat.mtime.getTime() - aStat.mtime.getTime()
})

let cleanedSize = 0
for (const file of files) {
const fileSize = statSync(file).size
cleanedSize += fileSize
if (cleanedSize >= shouldCleanSize) {
logger.info(`Cleaned ${cleanedSize} bytes cache`)
break
}
}
}
},
10 * 60 * 1000,
) // 10 min

return () => {
if (!timer) return
timer = clearInterval(timer)
}
}
Loading

0 comments on commit e655c2b

Please sign in to comment.