diff --git a/src/classes/Commands/sub-classes/PricelistManager.ts b/src/classes/Commands/sub-classes/PricelistManager.ts index 0be1701f3..4d88e6647 100644 --- a/src/classes/Commands/sub-classes/PricelistManager.ts +++ b/src/classes/Commands/sub-classes/PricelistManager.ts @@ -142,6 +142,10 @@ export default class PricelistManagerCommands { params.autoprice = true; } + if (params.isPartialPriced === undefined) { + params.isPartialPriced = false; + } + if (params.sku !== undefined && !testSKU(params.sku as string)) { return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); } @@ -179,6 +183,7 @@ export default class PricelistManagerCommands { `\nšŸ“¦ Stock: ${amount} | Min: ${entry.min} | Max: ${entry.max}` + `\nšŸ“‹ Enabled: ${entry.enabled ? 'āœ…' : 'āŒ'}` + `\nšŸ”„ Autoprice: ${entry.autoprice ? 'āœ…' : 'āŒ'}` + + `\nĀ½šŸ”„ isPartialPriced: ${entry.isPartialPriced ? 'āœ…' : 'āŒ'}` + (isPremium ? `\nšŸ“¢ Promoted: ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}` : '') + `\nšŸ”° Group: ${entry.group}` + `${entry.note.buy !== null ? `\nšŸ“„ Custom buying note: ${entry.note.buy}` : ''}` + @@ -609,6 +614,10 @@ export default class PricelistManagerCommands { entry.autoprice = params.autoprice; } + if (typeof params.isPartialPriced === 'boolean') { + entry.isPartialPriced = params.isPartialPriced; + } + if (params.withgroup || params.withoutgroup) { if (typeof params.note === 'object') { // can change note if have withgroup/withoutgroup parameter @@ -623,6 +632,8 @@ export default class PricelistManagerCommands { if (params.autoprice === undefined) { entry.autoprice = false; } + + entry.isPartialPriced = false; } if (typeof params.sell === 'object') { @@ -632,6 +643,8 @@ export default class PricelistManagerCommands { if (params.autoprice === undefined) { entry.autoprice = false; } + + entry.isPartialPriced = false; } } @@ -649,6 +662,7 @@ export default class PricelistManagerCommands { promoted: entry.promoted, group: entry.group, note: entry.note, + isPartialPriced: entry.isPartialPriced, time: entry.time }, 'pricelist' @@ -807,6 +821,8 @@ export default class PricelistManagerCommands { if (params.autoprice === undefined) { params.autoprice = false; } + + params.isPartialPriced = false; } else if (typeof params.buy !== 'object' && typeof params.sell === 'object') { params['buy'] = { keys: itemEntry.buy.keys, @@ -821,6 +837,8 @@ export default class PricelistManagerCommands { if (params.autoprice === undefined) { params.autoprice = false; } + + params.isPartialPriced = false; } else if (typeof params.sell !== 'object' && typeof params.buy === 'object') { params['sell'] = { keys: itemEntry.sell.keys, @@ -951,6 +969,11 @@ export default class PricelistManagerCommands { ? `${oldEntry.autoprice ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.autoprice ? 'āœ…' : 'āŒ'}` : `${newEntry.autoprice ? 'āœ…' : 'āŒ'}` }` + + `\nĀ½šŸ”„ isPartialPriced: ${ + oldEntry.isPartialPriced !== newEntry.isPartialPriced + ? `${oldEntry.isPartialPriced ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.isPartialPriced ? 'āœ…' : 'āŒ'}` + : `${newEntry.isPartialPriced ? 'āœ…' : 'āŒ'}` + }` + (isPremium ? `\nšŸ“¢ Promoted: ${ oldEntry.promoted !== newEntry.promoted @@ -1242,13 +1265,11 @@ export default class PricelistManagerCommands { const name = this.bot.schema.getName(SKU.fromString(entry.sku)); const stock = this.bot.inventoryManager.getInventory.getAmount(entry.sku, true); - const info = `${i + 1}. ${entry.sku} - ${name}${name.length > 40 ? '\n' : ' '}(${stock}, ${entry.min}, ${ + return `${i + 1}. ${entry.sku} - ${name}${name.length > 40 ? '\n' : ' '}(${stock}, ${entry.min}, ${ entry.max - }, ${entry.intent}, ${entry.enabled ? 'āœ…' : 'āŒ'}, ${entry.autoprice ? 'āœ…' : 'āŒ'}${ - isPremium ? `, ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}, ` : ', ' - }${entry.group})`; - - return info; + }, ${entry.intent}, ${entry.enabled ? 'āœ…' : 'āŒ'}, ${entry.autoprice ? 'āœ…' : 'āŒ'}, ${entry.group}, ${ + entry.isPartialPriced ? 'āœ…' : 'āŒ' + }${isPremium ? `, ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}` : ''})`; }); const listCount = list.length; @@ -1263,7 +1284,7 @@ export default class PricelistManagerCommands { : `${ limit < listCount && limit > 0 && params.limit !== undefined ? ` (limit set to ${limit})` : '' }.` - }\n\n šŸ“Œ #. "sku" - "name" ("Current Stock", "min", "max", "intent", "enabled", "autoprice", *"promoted", "group")\n\n` + + }\n\n šŸ“Œ #. "sku" - "name" ("Current Stock", "min", "max", "intent", "enabled", "autoprice", "group", "isPartialPriced", *"promoted")\n\n` + '* - Only shown if your account is Backpack.tf Premium\n\n.' ); @@ -1292,12 +1313,14 @@ export default class PricelistManagerCommands { params.intent !== undefined || params.autoprice !== undefined || params.group !== undefined || - params.promoted !== undefined + params.promoted !== undefined || + params.isPartialPriced !== undefined ) ) { return this.bot.sendMessage( steamID, - 'āš ļø Only parameters available for !find command: enabled, max, min, intent, promoted, autoprice or group\nExample: !find intent=bank&max=2' + 'āš ļø Only parameters available for !find command: enabled, max, min, intent,' + + ' promoted, autoprice, isPartialPriced, or group\nExample: !find intent=bank&max=2' ); } @@ -1309,13 +1332,6 @@ export default class PricelistManagerCommands { filter = filter.filter(entry => entry.enabled === params.enabled); } - if (params.max !== undefined) { - if (typeof params.max !== 'number') { - return this.bot.sendMessage(steamID, 'āš ļø max parameter must be an integer'); - } - filter = filter.filter(entry => entry.max === params.max); - } - if (params.min !== undefined) { if (typeof params.min !== 'number') { return this.bot.sendMessage(steamID, 'āš ļø min parameter must be an integer'); @@ -1323,6 +1339,13 @@ export default class PricelistManagerCommands { filter = filter.filter(entry => entry.min === params.min); } + if (params.max !== undefined) { + if (typeof params.max !== 'number') { + return this.bot.sendMessage(steamID, 'āš ļø max parameter must be an integer'); + } + filter = filter.filter(entry => entry.max === params.max); + } + if (params.promoted !== undefined) { if (typeof params.promoted === 'boolean') { if (params.promoted === true) { @@ -1369,6 +1392,13 @@ export default class PricelistManagerCommands { filter = filter.filter(entry => entry.autoprice === params.autoprice); } + if (params.isPartialPriced !== undefined) { + if (typeof params.isPartialPriced !== 'boolean') { + return this.bot.sendMessage(steamID, 'āš ļø isPartialPriced parameter must be "true" or "false"'); + } + filter = filter.filter(entry => entry.isPartialPriced === params.isPartialPriced); + } + if (params.group !== undefined) { if (typeof params.group !== 'string') { return this.bot.sendMessage(steamID, 'āš ļø group parameter must be a string'); @@ -1379,8 +1409,12 @@ export default class PricelistManagerCommands { const parametersUsed = { enabled: params.enabled !== undefined ? `enabled=${(params.enabled as boolean).toString()}` : '', autoprice: params.autoprice !== undefined ? `autoprice=${(params.autoprice as boolean).toString()}` : '', - max: params.max !== undefined ? `max=${params.max as number}` : '', + isPartialPriced: + params.isPartialPriced !== undefined + ? `isPartialPriced=${(params.isPartialPriced as boolean).toString()}` + : '', min: params.min !== undefined ? `min=${params.min as number}` : '', + max: params.max !== undefined ? `max=${params.max as number}` : '', intent: params.intent !== undefined ? `intent=${ @@ -1405,13 +1439,11 @@ export default class PricelistManagerCommands { const name = this.bot.schema.getName(SKU.fromString(entry.sku)); const stock = this.bot.inventoryManager.getInventory.getAmount(entry.sku, true); - const info = `${i + 1}. ${entry.sku} - ${name}${name.length > 40 ? '\n' : ' '}(${stock}, ${ - entry.min - }, ${entry.max}, ${entry.intent}, ${entry.enabled ? 'āœ…' : 'āŒ'}, ${entry.autoprice ? 'āœ…' : 'āŒ'}${ - isPremium ? `, ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}, ` : ', ' - }${entry.group})`; - - return info; + return `${i + 1}. ${entry.sku} - ${name}${name.length > 40 ? '\n' : ' '}(${stock}, ${entry.min}, ${ + entry.max + }, ${entry.intent}, ${entry.enabled ? 'āœ…' : 'āŒ'}, ${entry.autoprice ? 'āœ…' : 'āŒ'}, ${entry.group}, ${ + entry.isPartialPriced ? 'āœ…' : 'āŒ' + }${isPremium ? `, ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}` : ''})`; }); const listCount = list.length; @@ -1428,8 +1460,8 @@ export default class PricelistManagerCommands { ? ` (limit set to ${limit})` : '' }.` - }\n\n šŸ“Œ #. "sku" - "name" ("Current Stock", "min", "max", "intent", "enabled", "autoprice", *"promoted", "group")\n\n` + - '* - Only shown if your account is Backpack.tf Premium\n\n.' + }\n\n šŸ“Œ #. "sku" - "name" ("Current Stock", "min", "max", "intent", "enabled", "autoprice", "group", "isPartialPriced", *"promoted",)\n\n` + + '* - Only shown if your account is Backpack.tf Premium.\n\n.' ); const applyLimit = limit === -1 ? listCount : limit; diff --git a/src/classes/MyHandler/MyHandler.ts b/src/classes/MyHandler/MyHandler.ts index 24ef57437..bb24ac985 100644 --- a/src/classes/MyHandler/MyHandler.ts +++ b/src/classes/MyHandler/MyHandler.ts @@ -321,6 +321,56 @@ export default class MyHandler extends Handler { this.bot.pricelist.resetFailedUpdateOldPrices = 0; } + + // Send notification to admin/Discord Webhook if there's any partially priced item got reset on updateOldPrices + const bulkUpdatedPartiallyPriced = this.bot.pricelist.partialPricedUpdateBulk; + + if (bulkUpdatedPartiallyPriced.length > 0) { + const dw = this.opt.discordWebhook.sendAlert; + const isDwEnabled = dw.enable && dw.url !== ''; + + const msg = `All items below has been updated with partial price:\n\nā€¢ ${bulkUpdatedPartiallyPriced + .map(sku => { + const name = this.bot.schema.getName(SKU.fromString(sku), this.opt.tradeSummary.showProperName); + + return `${isDwEnabled ? `[${name}](https://www.prices.tf/items/${sku})` : name} (${sku})`; + }) + .join('\n\nā€¢ ')}`; + + if (this.opt.sendAlert.enable && this.opt.sendAlert.partialPrice.onBulkUpdatePartialPriced) { + if (isDwEnabled) { + sendAlert('onBulkUpdatePartialPriced', this.bot, msg); + } else { + this.bot.messageAdmins(msg, []); + } + } + } + + // Send notification to admin/Discord Webhook if there's any partially priced item got reset on updateOldPrices + const bulkPartiallyPriced = this.bot.pricelist.autoResetPartialPriceBulk; + + if (bulkPartiallyPriced.length > 0) { + const dw = this.opt.discordWebhook.sendAlert; + const isDwEnabled = dw.enable && dw.url !== ''; + + const msg = + `All partially priced items below has been reset to use the current prices ` + + `because no longer in stock or exceed the threshold:\n\nā€¢ ${bulkPartiallyPriced + .map(sku => { + const name = this.bot.schema.getName(SKU.fromString(sku), this.opt.tradeSummary.showProperName); + + return `${isDwEnabled ? `[${name}](https://www.prices.tf/items/${sku})` : name} (${sku})`; + }) + .join('\nā€¢ ')}`; + + if (this.opt.sendAlert.enable && this.opt.sendAlert.partialPrice.onResetAfterThreshold) { + if (isDwEnabled) { + sendAlert('autoResetPartialPriceBulk', this.bot, msg); + } else { + this.bot.messageAdmins(msg, []); + } + } + } } onShutdown(): Promise { @@ -1011,6 +1061,7 @@ export default class MyHandler extends Handler { const keyPrice = this.bot.pricelist.getKeyPrice; let hasOverstock = false; + let hasOverstockAndIsPartialPriced = false; let hasUnderstock = false; // A list of things that is wrong about the offer and other information @@ -1101,6 +1152,10 @@ export default class MyHandler extends Handler { // User is offering too many hasOverstock = true; + if (match.isPartialPriced) { + hasOverstockAndIsPartialPriced = true; + } + wrongAboutOffer.push({ reason: 'šŸŸ¦_OVERSTOCKED', sku: sku, @@ -1613,6 +1668,7 @@ export default class MyHandler extends Handler { const isAcceptOverstocked = isOverstocked && canAcceptOverstockedOverpay && + !hasOverstockAndIsPartialPriced && // because partial priced will use old buying prices exchange.our.value < exchange.their.value && (isInvalidItem ? canAcceptInvalidItemsOverpay : true) && (isUnderstocked ? canAcceptUnderstockedOverpay : true) && diff --git a/src/classes/MyHandler/offer/accepted/updateListings.ts b/src/classes/MyHandler/offer/accepted/updateListings.ts index a51f2d124..ca1fc5aae 100644 --- a/src/classes/MyHandler/offer/accepted/updateListings.ts +++ b/src/classes/MyHandler/offer/accepted/updateListings.ts @@ -2,6 +2,7 @@ import { Items, TradeOffer } from '@tf2autobot/tradeoffer-manager'; import SKU from 'tf2-sku-2'; import Currencies from 'tf2-currencies-2'; import pluralize from 'pluralize'; +import dayjs from 'dayjs'; import PriceCheckQueue from './requestPriceCheck'; import Bot from '../../../Bot'; @@ -124,7 +125,7 @@ export default function updateListings( const isUpdatePartialPricedItem = inPrice !== null && inPrice.autoprice && - inPrice.group === 'isPartialPriced' && + inPrice.isPartialPriced && bot.inventoryManager.getInventory.getAmount(sku, true) < 1 && // current stock isNotPureOrWeapons; @@ -344,7 +345,7 @@ export default function updateListings( } }); } else if (isUpdatePartialPricedItem) { - // If item exist in pricelist with group "isPartialPriced" and we no longer have that in stock, + // If item exist in pricelist with "isPartialPriced" set to true and we no longer have that in stock, // then update entry with the latest prices. const oldPrice = { @@ -352,6 +353,8 @@ export default function updateListings( sell: new Currencies(inPrice.sell) }; + const oldTime = inPrice.time; + const entry = { sku: sku, enabled: inPrice.enabled, @@ -359,18 +362,21 @@ export default function updateListings( min: inPrice.min, max: inPrice.max, intent: inPrice.intent, - group: 'all' + group: inPrice.group, + isPartialPriced: false } as EntryData; bot.pricelist .updatePrice(entry, true) .then(data => { const msg = - `${name} (${sku})\nā–ø ` + + `${dwEnabled ? `[${name}](https://www.prices.tf/items/${sku})` : name} (${sku})\nā–ø ` + [ `old: ${oldPrice.buy.toString()}/${oldPrice.sell.toString()}`, `new: ${data.buy.toString()}/${data.sell.toString()}` - ].join('\nā–ø '); + ].join('\nā–ø ') + + `\n - Partial priced since ${dayjs.unix(oldTime).fromNow()}` + + `\n - Current prices last update: ${dayjs.unix(data.time).fromNow()}`; log.debug(msg); diff --git a/src/classes/Options.ts b/src/classes/Options.ts index 81e6ea3e3..aa7e7a505 100644 --- a/src/classes/Options.ts +++ b/src/classes/Options.ts @@ -65,7 +65,9 @@ export const DEFAULTS = { partialPrice: { onUpdate: true, onSuccessUpdatePartialPriced: true, - onFailedUpdatePartialPriced: true + onFailedUpdatePartialPriced: true, + onBulkUpdatePartialPriced: true, + onResetAfterThreshold: true }, receivedUnusualNotInPricelist: true, failedToUpdateOldPrices: true @@ -1039,9 +1041,11 @@ interface SendAlert extends OnlyEnable { } interface PartialPrice { - onUpdate: boolean; - onSuccessUpdatePartialPriced: boolean; - onFailedUpdatePartialPriced: boolean; + onUpdate?: boolean; + onSuccessUpdatePartialPriced?: boolean; + onFailedUpdatePartialPriced?: boolean; + onBulkUpdatePartialPriced?: boolean; + onResetAfterThreshold?: boolean; } interface AutokeysAlert { diff --git a/src/classes/Pricelist.ts b/src/classes/Pricelist.ts index 6a0243741..e2b845c3a 100644 --- a/src/classes/Pricelist.ts +++ b/src/classes/Pricelist.ts @@ -30,6 +30,7 @@ export interface EntryData { promoted?: 0 | 1; group?: string | null; note?: { buy: string | null; sell: string | null }; + isPartialPriced?: boolean; time?: number | null; } @@ -58,6 +59,8 @@ export class Entry { note: { buy: string | null; sell: string | null }; + isPartialPriced: boolean; + time: number | null; private constructor(entry: EntryData, name: string) { @@ -91,7 +94,14 @@ export class Entry { } if (entry.group) { - this.group = entry.group; + if (entry.group === 'isPartialPriced') { + // temporary v3.7.x -> v3.8.0 + this.group = 'all'; + entry.isPartialPriced = true; + this.isPartialPriced = true; + } else { + this.group = entry.group; + } } else { this.group = 'all'; } @@ -106,6 +116,12 @@ export class Entry { } else { this.note = { buy: null, sell: null }; } + + if (entry.isPartialPriced) { + this.isPartialPriced = entry.isPartialPriced; + } else { + this.isPartialPriced = false; + } } clone(): Entry { @@ -134,6 +150,7 @@ export class Entry { promoted: this.promoted, group: this.group, note: this.note, + isPartialPriced: this.isPartialPriced, time: this.time }; } @@ -163,6 +180,11 @@ export default class Pricelist extends EventEmitter { return this.globalKeyPrices.sell; } + get isDwAlertEnabled(): boolean { + const opt = this.bot.options.discordWebhook.sendAlert; + return opt.enable && opt.url !== ''; + } + /** * Current key rate before receiving new prices data, this * can be different with global key rate. @@ -178,6 +200,10 @@ export default class Pricelist extends EventEmitter { failedUpdateOldPrices: string[] = []; + partialPricedUpdateBulk: string[] = []; + + autoResetPartialPriceBulk: string[] = []; + set resetFailedUpdateOldPrices(value: number) { this.failedUpdateOldPrices.length = value; } @@ -309,8 +335,8 @@ export default class Pricelist extends EventEmitter { private async validateEntry(entry: Entry, src: PricelistChangedSource): Promise { const keyPrices = this.getKeyPrices; - if (entry.autoprice && entry.group !== 'isPartialPriced') { - // skip this part if autoprice is true and group is "isPartialPriced" + if (entry.autoprice && !entry.isPartialPriced) { + // skip this part if autoprice is false and/or isPartialPriced is true try { const price = await this.priceSource.getPrice(entry.sku, 'bptf'); @@ -761,8 +787,9 @@ export default class Pricelist extends EventEmitter { // Go through our pricelist const oldCount = old.length; - const opt = this.options.pricelist.partialPriceUpdate; - const excludedSKU = ['5021;6'].concat(opt.excludeSKU); + const ppu = this.options.pricelist.partialPriceUpdate; + const excludedSKU = ['5021;6'].concat(ppu.excludeSKU); + const keyPrice = this.getKeyPrice.metal; for (let i = 0; i < oldCount; i++) { @@ -773,10 +800,14 @@ export default class Pricelist extends EventEmitter { const sku = currPrice.sku; const isInStock = inventory.getAmount(sku, true) > 0; + const maxIsOne = currPrice.max === 1; + const isNotExcluded = !excludedSKU.includes(sku); + const item = SKU.fromString(sku); // Go through pricestf/custom pricer prices let grouped: Item[]; + try { grouped = groupedPrices[item.quality][item.killstreak]; } catch (err) { @@ -800,75 +831,63 @@ export default class Pricelist extends EventEmitter { // Found matching items if (currPrice.time < newestPrice.time) { // Times don't match, update our price - const newBuy = new Currencies(newestPrice.buy); - const newSell = new Currencies(newestPrice.sell); + const oldPrices = { + buy: new Currencies(currPrice.buy), + sell: new Currencies(currPrice.sell) + }; + + const newPrices = { + buy: new Currencies(newestPrice.buy), + sell: new Currencies(newestPrice.sell) + }; - const newBuyValue = newBuy.toValue(keyPrice); - const newSellValue = newSell.toValue(keyPrice); + const newBuyValue = newPrices.buy.toValue(keyPrice); + const newSellValue = newPrices.sell.toValue(keyPrice); // TODO: Use last bought prices instead of current buying prices const currBuyingValue = currPrice.buy.toValue(keyPrice); const currSellingValue = currPrice.sell.toValue(keyPrice); - const isNotExceedThreshold = newestPrice.time - currPrice.time < opt.thresholdInSeconds; - const isNotExcluded = !excludedSKU.includes(sku); + const isNotExceedThreshold = newestPrice.time - currPrice.time < ppu.thresholdInSeconds; - if (opt.enable && isInStock && isNotExceedThreshold && isNotExcluded) { - // if optPartialUpdate.enable is true and the item is currently in stock - // and difference between latest time and time recorded in pricelist is less than threshold + // https://github.com/TF2Autobot/tf2autobot/issues/506 + // https://github.com/TF2Autobot/tf2autobot/pull/520 + if (ppu.enable && isInStock && isNotExceedThreshold && isNotExcluded && maxIsOne) { const isNegativeDiff = newSellValue - currBuyingValue <= 0; + const isBuyingChanged = currBuyingValue !== newBuyValue; - if (isNegativeDiff || currPrice.group === 'isPartialPriced') { - // Only trigger this if difference of new selling price and current buying price is negative or zero - // Or item group is "isPartialPriced". - - if (newBuyValue < currSellingValue) { - // if new buying price is less than current selling price - // update only the buying price. - currPrice.buy = newBuy; - - if (newSellValue > currSellingValue) { - // If new selling price is more than old, then update selling price too - currPrice.sell = newSell; - } - - // no need to update time here - - currPrice.group = 'isPartialPriced'; - pricesChanged = true; - } else if (newSellValue > currSellingValue) { - // If new selling price is more than old, then update selling price too - currPrice.sell = newSell; - - pricesChanged = true; + if (isNegativeDiff || isBuyingChanged || currPrice.isPartialPriced) { + if (newSellValue > currBuyingValue || newSellValue > currSellingValue) { + currPrice.sell = newPrices.sell; + } else { + currPrice.sell = Currencies.toCurrencies(currBuyingValue + 1, keyPrice); } + + const msg = this.generatePartialPriceUpdateMsg(oldPrices, currPrice, newPrices); + this.partialPricedUpdateBulk.push(msg); + pricesChanged = true; } else { - // else, just update as usual now (except if group is "isPartialPriced"). - if (currPrice.group !== 'isPartialPriced') { - currPrice.buy = newBuy; - currPrice.sell = newSell; + if (!currPrice.isPartialPriced) { + currPrice.buy = newPrices.buy; + currPrice.sell = newPrices.sell; currPrice.time = newestPrice.time; pricesChanged = true; } } } else { - // else if optPartialUpdate.enable is false and/or the item is currently not in stock - // and/or more than threshold, update everything - if ( - currPrice.group !== 'isPartialPriced' || - (currPrice.group === 'isPartialPriced' && !(isNotExceedThreshold || isInStock)) + !currPrice.isPartialPriced || + (currPrice.isPartialPriced && !(isNotExceedThreshold || isInStock)) ) { - currPrice.buy = newBuy; - currPrice.sell = newSell; + currPrice.buy = newPrices.buy; + currPrice.sell = newPrices.sell; currPrice.time = newestPrice.time; - if (currPrice.group === 'isPartialPriced') { - // reset group to "all" - // (temporary, will not reset group once https://github.com/TF2Autobot/tf2autobot/pull/520 is reviewed, approved, and merged) - currPrice.group = 'all'; + if (currPrice.isPartialPriced) { + currPrice.isPartialPriced = false; // reset to default + this.autoResetPartialPriceBulk.push(sku); } pricesChanged = true; @@ -889,13 +908,29 @@ export default class Pricelist extends EventEmitter { }); } + private generatePartialPriceUpdateMsg(oldPrices: BuyAndSell, currPrices: Entry, newPrices: BuyAndSell): string { + return ( + `${ + this.isDwAlertEnabled + ? `[${currPrices.name}](https://www.prices.tf/items/${currPrices.sku})` + : currPrices.name + } (${currPrices.sku}):\nā–ø ` + + [ + `old: ${oldPrices.buy.toString()}/${oldPrices.sell.toString()}`, + `current: ${currPrices.buy.toString()}/${currPrices.sell.toString()}`, + `pricestf: ${newPrices.buy.toString()}/${newPrices.sell.toString()}` + ].join('\nā–ø ') + + `\n - Time in pricelist: ${currPrices.time} (${dayjs.unix(currPrices.time).fromNow()})` + ); + } + private handlePriceChange(data: GetItemPriceResponse): void { if (data.source !== 'bptf') { return; } const match = this.getPrice(data.sku); - const opt = this.options; + const opt = this.bot.options; const dw = opt.discordWebhook.priceUpdate; const isDwEnabled = dw.enable && dw.url !== ''; @@ -969,22 +1004,27 @@ export default class Pricelist extends EventEmitter { }; let pricesChanged = false; + const currentStock = this.bot.inventoryManager.getInventory.getAmount(match.sku, true); + + const ppu = opt.pricelist.partialPriceUpdate; + const isInStock = currentStock > 0; + const isNotExceedThreshold = data.time - match.time < ppu.thresholdInSeconds; + const isNotExcluded = !['5021;6'].concat(ppu.excludeSKU).includes(match.sku); + const maxIsOne = match.max === 1; + + if (ppu.enable) { + log.debug('ppu status - onHandlePriceChange', { + sku: match.sku, + inStock: isInStock, + notExceed: isNotExceedThreshold, + notExclude: isNotExcluded + }); + } - const optPartialUpdate = opt.pricelist.partialPriceUpdate; - const isInStock = this.bot.inventoryManager.getInventory.getAmount(match.sku, true) > 0; - const isNotExceedThreshold = data.time - match.time < optPartialUpdate.thresholdInSeconds; - const isNotExcluded = !['5021;6'].concat(optPartialUpdate.excludeSKU).includes(match.sku); - - if ( - optPartialUpdate.enable && - isInStock && - isNotExceedThreshold && - this.globalKeyPrices !== undefined && - isNotExcluded - ) { - // if optPartialUpdate.enable is true and the item is currently in stock - // and difference between latest time and time recorded in pricelist is less than threshold + // https://github.com/TF2Autobot/tf2autobot/issues/506 + // https://github.com/TF2Autobot/tf2autobot/pull/520 + if (ppu.enable && isInStock && isNotExceedThreshold && isNotExcluded && maxIsOne) { const keyPrice = this.getKeyPrice.metal; const newBuyValue = newPrices.buy.toValue(keyPrice); @@ -995,62 +1035,41 @@ export default class Pricelist extends EventEmitter { const currSellingValue = match.sell.toValue(keyPrice); const isNegativeDiff = newSellValue - currBuyingValue <= 0; - - if (isNegativeDiff || match.group === 'isPartialPriced') { - // Only trigger this if difference of new selling price and current buying price is negative or zero - // Or item group is "isPartialPriced". - - let isUpdate = false; - - if (newBuyValue < currSellingValue) { - // if new buying price is less than current selling price - // update only the buying price. - match.buy = newPrices.buy; - - if (newSellValue > currSellingValue) { - // If new selling price is more than old, then update selling price too - match.sell = newPrices.sell; - } - - isUpdate = true; - - // no need to update time here - } else if (newSellValue > currSellingValue) { - // If new selling price is more than old, then update selling price too + const isBuyingChanged = currBuyingValue !== newBuyValue; + + log.debug('ppu', { + newBuyValue: newBuyValue, + newSellValue: newSellValue, + currBuyingValue: currBuyingValue, + currSellingValue: currSellingValue, + isNegativeDiff: isNegativeDiff, + isBuyingChanged: isBuyingChanged, + isAlreadyPartialPriced: match.isPartialPriced + }); + + if (match.isPartialPriced || isNegativeDiff || isBuyingChanged) { + if (newSellValue > currBuyingValue || newSellValue > currSellingValue) { + log.debug('ppu - update selling price with the latest price'); match.sell = newPrices.sell; - isUpdate = true; + } else { + log.debug('ppu - update selling price with minimum profit of 1 scrap'); + match.sell = Currencies.toCurrencies(currBuyingValue + 1, keyPrice); } - if (isUpdate) { - match.group = 'isPartialPriced'; - pricesChanged = true; + match.isPartialPriced = true; + pricesChanged = true; - const dwAlert = opt.discordWebhook.sendAlert; - const isAlertEnabledDW = dwAlert.enable && dwAlert.url !== ''; - - const msg = - `${ - isAlertEnabledDW - ? `[${match.name}](https://www.prices.tf/items/${match.sku})` - : match.name - } (${match.sku}):\nā–ø ` + - [ - `old: ${oldPrice.buy.toString()}/${oldPrice.sell.toString()}`, - `current: ${match.buy.toString()}/${match.sell.toString()}`, - `pricestf: ${newPrices.buy.toString()}/${newPrices.sell.toString()}` - ].join('\nā–ø '); - - if (opt.sendAlert.partialPrice.onUpdate) { - if (isAlertEnabledDW) { - sendAlert('isPartialPriced', this.bot, msg); - } else { - this.bot.messageAdmins('Partial price update\n\n' + msg, []); - } + const msg = this.generatePartialPriceUpdateMsg(oldPrice, match, newPrices); + + if (opt.sendAlert.partialPrice.onUpdate) { + if (this.isDwAlertEnabled) { + sendAlert('isPartialPriced', this.bot, msg); + } else { + this.bot.messageAdmins('Partial price update\n\n' + msg, []); } } } else { - // else, just update as usual now (except if group is "isPartialPriced"). - if (match.group !== 'isPartialPriced') { + if (!match.isPartialPriced) { match.buy = newPrices.buy; match.sell = newPrices.sell; match.time = data.time; @@ -1059,21 +1078,14 @@ export default class Pricelist extends EventEmitter { } } } else { - // else if optPartialUpdate.enable is false and/or the item is currently not in stock - // and/or more than threshold, update everything - - if ( - match.group !== 'isPartialPriced' || - (match.group === 'isPartialPriced' && !(isNotExceedThreshold || isInStock)) - ) { + if (!match.isPartialPriced || (match.isPartialPriced && !(isNotExceedThreshold || isInStock))) { match.buy = newPrices.buy; match.sell = newPrices.sell; match.time = data.time; - if (match.group === 'isPartialPriced') { - // reset group to "all" - // (temporary, will not reset group once https://github.com/TF2Autobot/tf2autobot/pull/520 is reviewed, approved, and merged) - match.group = 'all'; + if (match.isPartialPriced) { + match.isPartialPriced = false; // reset to default + sendAlert('autoResetPartialPrice', this.bot); } pricesChanged = true; @@ -1082,32 +1094,31 @@ export default class Pricelist extends EventEmitter { if (pricesChanged) { this.priceChanged(match.sku, match); + } - if (isDwEnabled && this.globalKeyPrices !== undefined) { - const currentStock = this.bot.inventoryManager.getInventory.getAmount(match.sku, true); - const showOnlyInStock = dw.showOnlyInStock ? currentStock > 0 : true; - - if (showOnlyInStock) { - const tz = opt.timezone; - const format = opt.customTimeFormat; - - const time = dayjs() - .tz(tz ? tz : 'UTC') - .format(format ? format : 'MMMM Do YYYY, HH:mm:ss ZZ'); - - sendWebHookPriceUpdateV1( - data.sku, - data.name, - match, - time, - this.schema, - opt, - currentStock, - oldPrice, - this.getKeyPrice.metal, - this.isUseCustomPricer - ); - } + if (isDwEnabled) { + const showOnlyInStock = dw.showOnlyInStock ? currentStock > 0 : true; + + if (showOnlyInStock) { + const tz = opt.timezone; + const format = opt.customTimeFormat; + + const time = dayjs() + .tz(tz ? tz : 'UTC') + .format(format ? format : 'MMMM Do YYYY, HH:mm:ss ZZ'); + + sendWebHookPriceUpdateV1( + data.sku, + data.name, + match, + time, + this.schema, + opt, + currentStock, + oldPrice, + this.getKeyPrice.metal, + this.isUseCustomPricer + ); } } } diff --git a/src/lib/DiscordWebhook/sendAlert.ts b/src/lib/DiscordWebhook/sendAlert.ts index b407a474e..a69fb3682 100644 --- a/src/lib/DiscordWebhook/sendAlert.ts +++ b/src/lib/DiscordWebhook/sendAlert.ts @@ -36,6 +36,9 @@ type AlertType = | 'error-accept' | 'autoUpdatePartialPriceSuccess' | 'autoUpdatePartialPriceFailed' + | 'autoResetPartialPrice' + | 'autoResetPartialPriceBulk' + | 'onBulkUpdatePartialPriced' | 'isPartialPriced' | 'unusualInvalidItems' | 'failedToUpdateOldPrices'; @@ -125,6 +128,18 @@ export default function sendAlert( title = 'Failed update item prices (Partial price update)'; description = msg; color = '16711680'; // red + } else if (type === 'autoResetPartialPrice') { + title = 'āœ… Automatically reset partially priced item'; + description = msg; + color = '8323327'; // purple + } else if (type === 'onBulkUpdatePartialPriced') { + title = 'āœ… Partial price update - bulk'; + description = msg; + color = '16776960'; // yellow + } else if (type === 'autoResetPartialPriceBulk') { + title = 'āœ… Automatically reset partially priced item - bulk'; + description = msg; + color = '8323327'; // purple } else if (type === 'autoAddPaintedItems') { title = 'Added painted items to sell'; description = msg; diff --git a/src/lib/tools/summarizeItems.ts b/src/lib/tools/summarizeItems.ts index 0b64e38c4..6626b27e6 100644 --- a/src/lib/tools/summarizeItems.ts +++ b/src/lib/tools/summarizeItems.ts @@ -127,7 +127,7 @@ function listPrices(offer: TradeOffer, bot: Bot, isSteamChat: boolean): string { if (pricelist !== null) { buyPrice = pricelist.buy.toString(); sellPrice = pricelist.sell.toString(); - autoprice = pricelist.autoprice ? 'autopriced' : 'manual'; + autoprice = pricelist.autoprice ? `autopriced${pricelist.isPartialPriced ? ' - ppu' : ''}` : 'manual'; } else { buyPrice = new Currencies(prices[sku].buy).toString(); sellPrice = new Currencies(prices[sku].sell).toString(); diff --git a/src/schemas/options-json/options.ts b/src/schemas/options-json/options.ts index 89eb66252..f69bae56e 100644 --- a/src/schemas/options-json/options.ts +++ b/src/schemas/options-json/options.ts @@ -478,9 +478,20 @@ export const optionsSchema: jsonschema.Schema = { }, onFailedUpdatePartialPriced: { type: 'boolean' + }, + onBulkUpdatePartialPriced: { + type: 'boolean' + }, + onResetAfterThreshold: { + type: 'boolean' } }, - required: ['onUpdate', 'onSuccessUpdatePartialPriced', 'onFailedUpdatePartialPriced'], + required: [ + 'onUpdate', + 'onSuccessUpdatePartialPriced', + 'onFailedUpdatePartialPriced', + 'onResetAfterThreshold' + ], additionalProperties: false }, receivedUnusualNotInPricelist: { diff --git a/src/schemas/pricelist-json/pricelist-add.ts b/src/schemas/pricelist-json/pricelist-add.ts index a53d16ea5..a066858c9 100644 --- a/src/schemas/pricelist-json/pricelist-add.ts +++ b/src/schemas/pricelist-json/pricelist-add.ts @@ -22,17 +22,17 @@ export const addSchema: jsonschema.Schema = { // if the item is autopriced or not type: 'boolean' }, + min: { + // minimum stock + type: 'integer', + minimum: 0 + }, max: { // maximum stock type: 'integer', // -1 is infinite minimum: -1 }, - min: { - // minimum stock - type: 'integer', - minimum: 0 - }, buy: { // buy price $ref: 'tf2-currencies' @@ -59,6 +59,10 @@ export const addSchema: jsonschema.Schema = { }, note: { $ref: 'listing-note' + }, + isPartialPriced: { + // partialPrice feature: https://github.com/TF2Autobot/tf2autobot/pull/520 + type: 'boolean' } }, additionalProperties: false, diff --git a/src/schemas/pricelist-json/pricelist.ts b/src/schemas/pricelist-json/pricelist.ts index 8a5c7fb8e..a3d66d5b5 100644 --- a/src/schemas/pricelist-json/pricelist.ts +++ b/src/schemas/pricelist-json/pricelist.ts @@ -26,17 +26,17 @@ export const pricelistSchema: jsonschema.Schema = { // if the item is autopriced or not type: 'boolean' }, + min: { + // minimum stock + type: 'integer', + minimum: 0 + }, max: { // maximum stock type: 'integer', // -1 is infinite minimum: -1 }, - min: { - // minimum stock - type: 'integer', - minimum: 0 - }, buy: { // buy price $ref: 'tf2-currencies' @@ -64,6 +64,10 @@ export const pricelistSchema: jsonschema.Schema = { note: { $ref: 'listing-note' }, + isPartialPriced: { + // partialPrice feature: https://github.com/TF2Autobot/tf2autobot/pull/520 + type: 'boolean' + }, time: { // time when the price changed anyOf: [