Compare commits
12 Commits
f6d600e9ae
...
handlebars
| Author | SHA1 | Date | |
|---|---|---|---|
| dcb3c2102e | |||
| 1e6be9bfd9 | |||
| ff444d6de2 | |||
| 382ee2f39a | |||
| d56c127306 | |||
| fc01b7e7b9 | |||
| edb82fdcfa | |||
| b57e3e5cba | |||
| fd32ca704a | |||
| 76fdee2cfc | |||
| 13a1bd9ab0 | |||
| 7bfb00ad77 |
@@ -18,7 +18,3 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- cd /home/pi/warframe-market-bot
|
- cd /home/pi/warframe-market-bot
|
||||||
- npm install
|
- npm install
|
||||||
- name: restart
|
|
||||||
commands:
|
|
||||||
- export PM2_HOME=/home/pi/.pm2
|
|
||||||
- pm2 restart warframe-market-bot
|
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -2,4 +2,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
.env
|
.env
|
||||||
public/index.html
|
public/*.html
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
Список Прайм предметов: https://warframe.fandom.com/wiki/Prime
|
Список Прайм предметов: https://warframe.fandom.com/wiki/Prime
|
||||||
|
|
||||||
Маркет: https://warframe.market
|
Маркет: https://warframe.market
|
||||||
|
|
||||||
|
Документация API маркета: https://warframe.market/api_docs
|
||||||
|
|||||||
9
ecosystem.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'warframe-market-bot',
|
||||||
|
script: './src/index.js',
|
||||||
|
watch: true,
|
||||||
|
ignore_watch: ['node_modules', 'public'],
|
||||||
|
restart_delay: 1 * 60 * 1000
|
||||||
|
}]
|
||||||
|
}
|
||||||
38
index.js
@@ -1,38 +0,0 @@
|
|||||||
require('dotenv').config()
|
|
||||||
const puppeteer = require('puppeteer')
|
|
||||||
const items = require('./items')
|
|
||||||
const output = require('./output')
|
|
||||||
|
|
||||||
async function initPuppeteer (page) {
|
|
||||||
await page.setDefaultNavigationTimeout(0)
|
|
||||||
await page.setRequestInterception(true)
|
|
||||||
page.on('request', (request) => {
|
|
||||||
if (['image'].includes(request.resourceType())) request.abort()
|
|
||||||
else request.continue()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const browser = await puppeteer.launch({
|
|
||||||
executablePath: process.env.EXECUTABLE
|
|
||||||
})
|
|
||||||
const page = await browser.newPage()
|
|
||||||
await initPuppeteer(page)
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
console.log(`Looking at ${item.name}`)
|
|
||||||
|
|
||||||
let partsPrice = 0
|
|
||||||
for (const part of item.parts) {
|
|
||||||
partsPrice += await part.getPrice(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setPrice = await item.set.getPrice(page)
|
|
||||||
if (partsPrice < setPrice) {
|
|
||||||
output.addItem(item.name, partsPrice, setPrice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.close()
|
|
||||||
output.submit()
|
|
||||||
})()
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = class Part {
|
|
||||||
constructor (set, part, amount) {
|
|
||||||
this.part = part
|
|
||||||
this.url = `${set}_prime_${part}`
|
|
||||||
this.amount = amount ?? 1
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPrice (page) {
|
|
||||||
const marketUrl = 'https://warframe.market/items/'
|
|
||||||
|
|
||||||
await page.goto(marketUrl + this.url)
|
|
||||||
const element = await page.waitForSelector('b.price')
|
|
||||||
const value = await element.evaluate(el => el.textContent)
|
|
||||||
return Number(value) * this.amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const Handlebars = require('handlebars')
|
|
||||||
const OutputItem = require('./OutputItem')
|
|
||||||
|
|
||||||
class Output {
|
|
||||||
constructor () {
|
|
||||||
this.items = []
|
|
||||||
this.timestamp = null
|
|
||||||
}
|
|
||||||
|
|
||||||
addItem (...p) {
|
|
||||||
this.items.push(new OutputItem(...p))
|
|
||||||
}
|
|
||||||
|
|
||||||
async compileTemplate () {
|
|
||||||
const templateFile = await fs.readFileSync('./output/template.hbs', 'utf8')
|
|
||||||
const template = Handlebars.compile(templateFile)
|
|
||||||
return template(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
async writeToFile (content) {
|
|
||||||
const filename = './public/index.html'
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.unlinkSync(filename)
|
|
||||||
} catch {
|
|
||||||
console.log('File probably doesnt exist')
|
|
||||||
}
|
|
||||||
fs.writeFileSync(filename, content, 'utf-8')
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit () {
|
|
||||||
this.timestamp = new Date()
|
|
||||||
const content = await this.compileTemplate()
|
|
||||||
await this.writeToFile(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Output()
|
|
||||||
3411
package-lock.json
generated
@@ -9,11 +9,13 @@
|
|||||||
"author": "Anatoly Kopyl",
|
"author": "Anatoly Kopyl",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.26.0",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"puppeteer": "^13.3.2"
|
"limiter": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-standard": "^16.0.3"
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"pm2": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.1 MiB |
BIN
public/assets/languages/de.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/assets/languages/en.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/languages/fr.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/assets/languages/ru.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/assets/languages/ua.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 234 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -60,6 +60,7 @@ a {
|
|||||||
background: white;
|
background: white;
|
||||||
color: black;
|
color: black;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero > .main {
|
.hero > .main {
|
||||||
@@ -93,6 +94,31 @@ a {
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.languages {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 10px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languages > a {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languages img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languages .selected {
|
||||||
|
background: rgb(233, 233, 233);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
margin: 32px auto;
|
margin: 32px auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
31
src/api.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const axios = require('axios')
|
||||||
|
const { RateLimiter } = require('limiter')
|
||||||
|
|
||||||
|
class Api {
|
||||||
|
constructor () {
|
||||||
|
this.baseUrl = 'https://api.warframe.market/v1/'
|
||||||
|
this.delay = process.env.API_DELAY ?? 3000
|
||||||
|
this.limiter = new RateLimiter({ tokensPerInterval: 1, interval: Number(this.delay) })
|
||||||
|
}
|
||||||
|
|
||||||
|
async _get (url) {
|
||||||
|
await this.limiter.removeTokens(1)
|
||||||
|
return axios.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrders (url) {
|
||||||
|
const response = await this._get(this.baseUrl + 'items/' + url + '/orders')
|
||||||
|
return response.data.payload.orders.filter(function (order) {
|
||||||
|
return order.order_type === 'sell' && order.user.status !== 'offline'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSortedOrders (url) {
|
||||||
|
const orders = await this.getOrders(url)
|
||||||
|
return orders.sort(function (a, b) {
|
||||||
|
return a.platinum - b.platinum
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Api()
|
||||||
21
src/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const items = require('./items')
|
||||||
|
const output = require('./output');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for (const item of items) {
|
||||||
|
console.log(`Looking at ${item.name}`)
|
||||||
|
|
||||||
|
let partsPrice = 0
|
||||||
|
for (const part of item.parts) {
|
||||||
|
partsPrice += await part.getPrice()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPrice = await item.set.getPrice()
|
||||||
|
if (partsPrice < setPrice) {
|
||||||
|
output.addItem(item.name, partsPrice, setPrice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.submit()
|
||||||
|
})()
|
||||||
14
src/items/Part.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const Api = require('../api')
|
||||||
|
|
||||||
|
module.exports = class Part {
|
||||||
|
constructor (set, part, amount) {
|
||||||
|
this.part = part
|
||||||
|
this.url = `${set}_prime_${part}`
|
||||||
|
this.amount = amount ?? 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrice () {
|
||||||
|
const orders = await Api.getSortedOrders(this.url)
|
||||||
|
return Number(orders[0].platinum) * this.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = class OutputItem {
|
module.exports = class ListingSet {
|
||||||
constructor (name, parts, set) {
|
constructor (name, parts, set) {
|
||||||
this.name = name + ' prime'
|
this.name = name + ' prime'
|
||||||
this.parts = parts
|
this.parts = parts
|
||||||
9
src/output/helpers.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
function ifEq (a, b, opts) {
|
||||||
|
if (a === b) {
|
||||||
|
return opts.fn(this)
|
||||||
|
} else {
|
||||||
|
return opts.inverse(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { ifEq }
|
||||||
35
src/output/i18n.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"link": "",
|
||||||
|
"title": "Market Gaps",
|
||||||
|
"description": "Find a profitable difference between the price of the set and the price of the sum of it's parts.",
|
||||||
|
"filter_by_difference": "Filter by difference:",
|
||||||
|
"name": "Name",
|
||||||
|
"parts_price": "Parts price",
|
||||||
|
"set_price": "Set price",
|
||||||
|
"difference": "Difference",
|
||||||
|
"generated_at": "Generated at"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"link": "ru",
|
||||||
|
"title": "Market Gaps",
|
||||||
|
"description": "Находите выгоду между покупкой сета по отдельности и продажей его в собранном виде.",
|
||||||
|
"filter_by_difference": "Фильтровать по разнице:",
|
||||||
|
"name": "Название",
|
||||||
|
"parts_price": "Стоимость частей",
|
||||||
|
"set_price": "Стоимость сета",
|
||||||
|
"difference": "Разница",
|
||||||
|
"generated_at": "Сгенерировано в"
|
||||||
|
},
|
||||||
|
"ua": {
|
||||||
|
"link": "ua",
|
||||||
|
"title": "Market Gaps",
|
||||||
|
"description": "Знаходьте вигоду між покупкою сета окремо та продажем його в зібраному вигляді.",
|
||||||
|
"filter_by_difference": "Фільтрувати за різницею:",
|
||||||
|
"name": "Назва",
|
||||||
|
"parts_price": "Вартість частин",
|
||||||
|
"set_price": "Вартість сета",
|
||||||
|
"difference": "Різниця",
|
||||||
|
"generated_at": "Згенеровано в"
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/output/index.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const Handlebars = require('handlebars')
|
||||||
|
const { ifEq } = require('./helpers')
|
||||||
|
const ListingSet = require('./ListingSet')
|
||||||
|
const i18n = require('./i18n.json')
|
||||||
|
|
||||||
|
Handlebars.registerHelper('if_eq', ifEq)
|
||||||
|
|
||||||
|
class Output {
|
||||||
|
constructor () {
|
||||||
|
this.items = []
|
||||||
|
this.timestamp = null
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem (...p) {
|
||||||
|
this.items.push(new ListingSet(...p))
|
||||||
|
}
|
||||||
|
|
||||||
|
async _compileTemplate () {
|
||||||
|
const templateFile = await fs.readFileSync('./src/output/template.hbs', 'utf8')
|
||||||
|
return Handlebars.compile(templateFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _writeToFile (filename, content) {
|
||||||
|
try {
|
||||||
|
await fs.unlinkSync(filename)
|
||||||
|
} catch {
|
||||||
|
console.log('File probably doesnt exist')
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filename, content, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit () {
|
||||||
|
this.timestamp = new Date()
|
||||||
|
const template = await this._compileTemplate()
|
||||||
|
|
||||||
|
Object.keys(i18n).forEach(locale => {
|
||||||
|
const filename = i18n[locale].link || 'index'
|
||||||
|
this._writeToFile(`./public/${filename}.html`, template({
|
||||||
|
...this,
|
||||||
|
t: i18n[locale],
|
||||||
|
languages: i18n
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Output()
|
||||||
@@ -4,8 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Find profits in the Warframe market">
|
||||||
|
<meta name="keywords" content="warframe,market,buy,sell,trade,mod,blueprint,syndicate,relic,order,auction,riven,wts,wtb,sales,worth,platinum,price,checking">
|
||||||
|
<link rel="canonical" href="https://warframe.center/">
|
||||||
|
<link rel="alternate" hreflang="en" href="https://warframe.center/">
|
||||||
|
<link rel="alternate" hreflang="ru" href="https://warframe.center/ru/">
|
||||||
|
<link rel="alternate" hreflang="uk" href="https://warframe.center/ua/">
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://warframe.center/">
|
||||||
|
<meta property="og:title" content="Warframe Center">
|
||||||
|
<meta property="og:description" content="Find profits in the Warframe market">
|
||||||
|
<meta property="og:url" content="https://warframe.center">
|
||||||
|
<meta property="og:image" content="https://warframe.center/assets/warframe_logo.png">
|
||||||
|
<meta property="og:site_name" content="Warframe Center">
|
||||||
|
<meta property="og:locale" content="en">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<title>Warframe Market Gaps</title>
|
<title>Warframe Center</title>
|
||||||
|
|
||||||
<!-- Yandex.Metrika counter -->
|
<!-- Yandex.Metrika counter -->
|
||||||
<script type="text/javascript" >
|
<script type="text/javascript" >
|
||||||
@@ -28,16 +42,27 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<img class="logo" src="./assets/warframe_logo.png">
|
<img class="logo" src="./assets/warframe_logo.png">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<h1>Market Gaps</h1>
|
<h1>{{t.title}}</h1>
|
||||||
<p>Find a profitable difference between the price of the set and the price of the sum of it's parts.</p>
|
<p>{{t.description}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img id="gunner2" src="./assets/gunner2.png">
|
<img id="gunner2" src="./assets/gunner2.png">
|
||||||
|
|
||||||
|
<div class="languages">
|
||||||
|
{{#each languages}}
|
||||||
|
<a
|
||||||
|
href="/{{this.link}}"
|
||||||
|
class="{{#if_eq ../t.link this.link}}selected{{/if_eq}}"
|
||||||
|
>
|
||||||
|
<img src="./assets/languages/{{@key}}.png">
|
||||||
|
</a>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-column">
|
<div class="main-column">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label for="min-difference">Filter by difference:</label>
|
<label for="min-difference">{{t.filter_by_difference}}</label>
|
||||||
<input type="range" min="1" max="60" value="1" id="min-difference">
|
<input type="range" min="1" max="60" value="1" id="min-difference">
|
||||||
<span id="filter-value">1</span>
|
<span id="filter-value">1</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,10 +70,10 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{{t.name}}</th>
|
||||||
<th>Parts price</th>
|
<th>{{t.parts_price}}</th>
|
||||||
<th>Set price</th>
|
<th>{{t.set_price}}</th>
|
||||||
<th>Difference</th>
|
<th>{{t.difference}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="items">
|
<tbody id="items">
|
||||||
@@ -69,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timestamp">
|
<div class="timestamp">
|
||||||
Generated at {{timestamp}}
|
{{t.generated_at}} {{timestamp}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="index.js"></script>
|
<script src="index.js"></script>
|
||||||