Skip to content

Commit

Permalink
auto update extension prices
Browse files Browse the repository at this point in the history
  • Loading branch information
0x7d8 committed Nov 30, 2024
1 parent 68b648b commit c45004f
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ DATABASE_URL="postgresql://blueprint_api:local@db:5432/blueprint_api"
PORT=8000
INTERNAL_KEY="HKj5LZnavEfjqYDTEUi" # random string, if leaked change ASAP

# SXC_TOKEN="" # for automatic price updating on extensions (sourceXchange)
# BBB_TOKEN="" # for automatic price updating on extensions (BuiltByBit)

LOG_LEVEL="info"
LOG_DIRECTORY="/app/logs"

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@
"@rjweb/runtime-node": "^1.1.1",
"@rjweb/utils": "^1.12.26",
"ansi-colors": "^4.1.3",
"drizzle-orm": "^0.36.4",
"drizzle-kit": "^0.28.1",
"drizzle-orm": "^0.36.4",
"module-alias": "^2.2.3",
"node-cron": "^3.0.3",
"pg": "^8.13.1",
"rjweb-server": "^9.8.4",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.10.1",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.11.10",
"esbuild": "^0.24.0",
"typescript": "^5.7.2"
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions src/crontab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import env from "@/globals/env"
import database from "@/globals/database"
import cache from "@/globals/cache"
import getVersion from "@/index"
import logger from "@/globals/logger"
import * as sourcexchange from "@/globals/sourcexchange"
import * as builtbybit from "@/globals/builtbybit"

export const runContext = {
/**
* The Environment Variables of the Server
* @since 1.0.0
*/ env,
/**
* The SQLite Database Connection
* @since 1.0.0
*/ database,
/**
* The Redis Cache Client
* @since 1.0.0
*/ cache,
/**
* The Logger
* @since 1.0.0
*/ logger,
/**
* The SourceXchange Client
* @since 1.0.0
*/ sourcexchange,
/**
* The BuiltByBit Client
* @since 1.0.0
*/ builtbybit,
/**
* The App Version
* @since 1.0.0
*/ appVersion: getVersion(),

/**
* Create Multi Line Strings
* @since 1.0.0
*/ join(...strings: string[]): string {
return strings.join('\n')
}
} as const

export type CrontabContext = typeof runContext

export default class Builder<Excluded extends (keyof Builder)[] = []> {
protected interval: string = '* * * * *'
protected listener: (ctx: typeof runContext) => any | Promise<any> = () => undefined

/**
* Set the Interval
* @since 1.0.0
*/ public cron(interval: string): Omit<Builder<[...Excluded, 'cron']>, 'cron' | Excluded[number]> {
this.interval = interval

return this as any
}

/**
* Listen for The Crontab
* @since 1.0.0
*/ public listen(callback: (ctx: typeof runContext) => any | Promise<any>): Omit<Builder<[...Excluded, 'listen']>, 'listen' | Excluded[number]> {
this.listener = callback as any

return this as any
}
}
83 changes: 83 additions & 0 deletions src/crontabs/update-prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Crontab from "@/crontab"
import logger from "@/globals/logger"
import env from "@/globals/env"
import { eq } from "drizzle-orm"

let isRunning = false, blockRunning = false
export default new Crontab()
.cron(env.LOG_LEVEL === 'debug' ? '*/3 * * * * *' : '0 */6 * * *')
.listen(async(ctx) => {
if (isRunning || blockRunning) return
isRunning = true

if (env.LOG_LEVEL === 'debug') blockRunning = true

logger()
.text('Running Update Prices Schedule')
.info()

const [ extensions, sxcProducts ] = await Promise.all([
ctx.database.select({
id: ctx.database.schema.extensions.id,
name: ctx.database.schema.extensions.name,
platforms: ctx.database.schema.extensions.platforms,
})
.from(ctx.database.schema.extensions)
.where(eq(ctx.database.schema.extensions.pending, false)),
ctx.sourcexchange.products()
])

for (const extension of extensions) {
if (!Object.keys(extension.platforms).filter((platform) => platform !== 'GITHUB').length) continue

logger()
.text('Updating Extension Prices of')
.text(extension.name, (c) => c.cyan)
.info()

const platforms = Object.assign({}, extension.platforms)

if (extension.platforms.SOURCEXCHANGE) {
const product = sxcProducts.find((product) => product.url === extension.platforms.SOURCEXCHANGE.url)

if (product) {
platforms.SOURCEXCHANGE = {
url: extension.platforms.SOURCEXCHANGE.url,
price: product.price,
currency: product.currency
}
}
}

if (extension.platforms.BUILTBYBIT) {
const product = await ctx.builtbybit.product(parseInt(extension.platforms.BUILTBYBIT.url.split('.').pop() as string))

if (product) {
platforms.BUILTBYBIT = {
url: extension.platforms.BUILTBYBIT.url,
price: product.price,
currency: product.currency
}
}
}

if (JSON.stringify(platforms) !== JSON.stringify(extension.platforms)) {
await ctx.database.update(ctx.database.schema.extensions)
.set({
platforms
})
.where(eq(ctx.database.schema.extensions.id, extension.id))
}

logger()
.text('Updated Extension Prices of')
.text(extension.name, (c) => c.cyan)
.info()
}

logger()
.text('Finished Update Prices Schedule')
.info()

isRunning = false
})
23 changes: 23 additions & 0 deletions src/globals/builtbybit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import env from "@/globals/env"
import { Currency } from "@/schema"

export type BBBProduct = {
price: number
currency: Currency
}

/**
* Get Information for a Blueprint Product on BuiltByBit
* @since 1.0.0
*/ export async function product(product: number) {
if (!env.BBB_TOKEN) return null

const data = await fetch(`https://api.builtbybit.com/v1/resources/${product}`, {
headers: {
Authorization: `Private ${env.BBB_TOKEN}`,
Accept: 'application/json'
}
}).then((res) => res.json()).catch(() => ({ data: null })) as { data: BBBProduct }

return data.data
}
3 changes: 3 additions & 0 deletions src/globals/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const infos = z.object({
PORT: z.string().transform((str) => parseInt(str)).optional(),
INTERNAL_KEY: z.string(),

SXC_TOKEN: z.string().optional(),
BBB_TOKEN: z.string().optional(),

LOG_LEVEL: z.enum(['none', 'info', 'debug']),
LOG_DIRECTORY: z.string().optional(),

Expand Down
24 changes: 24 additions & 0 deletions src/globals/sourcexchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import env from "@/globals/env"
import { Currency } from "@/schema"

export type SXCProduct = {
price: number
currency: Currency
url: string
}

/**
* Get all Blueprint products on Sourcexchange
* @since 1.0.0
*/ export async function products() {
if (!env.SXC_TOKEN) return []

const data = await fetch('https://www.sourcexchange.net/api/products/blueprint', {
headers: {
Authorization: `Bearer ${env.SXC_TOKEN}`,
Accept: 'application/json'
}
}).then((res) => res.json()).catch(() => ({ data: [] })) as { data: SXCProduct[] }

return data.data
}
24 changes: 22 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logger from "@/globals/logger"
import * as fs from "fs"
import env from "@/globals/env"
import { system } from "@rjweb/utils"
import { filesystem, system } from "@rjweb/utils"
import Crontab, { runContext } from "@/crontab"
import cron from "node-cron"

export default function getVersion() {
return `${JSON.parse(fs.readFileSync('../package.json', 'utf8')).version}:${system.execute('git rev-parse --short=10 HEAD').trim()}`
Expand All @@ -14,4 +16,22 @@ logger()
.text('\n')
.info()

require('@/api')
Promise.all([ ...filesystem.getFiles(`${__dirname}/crontabs`, { recursive: true }).filter((file) => file.endsWith('js')).map(async(file) => {
const cronFile = (await import(file)).default.default

if (cronFile instanceof Crontab) {
cron.schedule(cronFile['interval'], async() => {
try {
await Promise.resolve(cronFile['listener'](runContext))
} catch (error: any) {
logger()
.text('Crontab Error')
.text('\n')
.text(error?.stack ?? error, (c) => c.red)
.error()
}
})
}
}) ]).then(() => {
if (env.PORT) require('@/api')
})
7 changes: 5 additions & 2 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { sql } from "drizzle-orm"
import { integer, pgTable, varchar, uniqueIndex, pgEnum, serial, timestamp, jsonb, text, boolean, primaryKey, char, index } from "drizzle-orm/pg-core"

export type Platform = 'SOURCEXCHANGE' | 'BUILTBYBIT' | 'GITHUB'
export type Currency = 'USD' | 'EUR'
export const platforms = Object.freeze(['SOURCEXCHANGE', 'BUILTBYBIT', 'GITHUB'] as const)
export const currency = Object.freeze(['USD', 'EUR'] as const)

export type Platform = typeof platforms[number]
export type Currency = typeof currency[number]

export const extensionType = pgEnum('extension_type', ['THEME', 'EXTENSION'])

Expand Down

0 comments on commit c45004f

Please sign in to comment.