import { snakeCase } from 'change-case'; import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'; import jsonlint from '@tf2autobot/jsonlint'; import * as path from 'path'; import { deepMerge } from '../lib/tools/deep-merge'; import validator from '../lib/validator'; import { Currency } from '../types/TeamFortress2'; export const DEFAULTS: JsonOptions = { miscSettings: { showOnlyMetal: { enable: true }, sortInventory: { enable: true, type: 3 }, createListings: { enable: true }, addFriends: { enable: true }, sendGroupInvite: { enable: true }, counterOffer: { enable: true, skipIncludeMessage: false }, skipItemsInTrade: { enable: true }, weaponsAsCurrency: { enable: true, withUncraft: true }, checkUses: { duel: true, noiseMaker: true }, game: { playOnlyTF2: false, customName: '' }, alwaysRemoveItemAttributes: { customTexture: { enable: true } // giftedByTag: { // enable: true // } }, deleteUntradableJunk: { enable: false }, reputationCheck: { checkMptfBanned: false, reptfAsPrimarySource: false } }, sendAlert: { enable: true, autokeys: { lowPure: true, failedToAdd: true, failedToUpdate: true, failedToDisable: true }, backpackFull: true, highValue: { gotDisabled: true, receivedNotInPricelist: true, tryingToTake: true }, autoRemoveIntentSellFailed: true, autoAddPaintedItems: true, failedAccept: true, unableToProcessOffer: true, partialPrice: { onUpdate: true, onSuccessUpdatePartialPriced: true, onFailedUpdatePartialPriced: true, onBulkUpdatePartialPriced: true, onResetAfterThreshold: true }, receivedUnusualNotInPricelist: true, failedToUpdateOldPrices: true }, pricelist: { partialPriceUpdate: { enable: false, thresholdInSeconds: 604800, // 7 days excludeSKU: [] }, filterCantAfford: { enable: false }, autoRemoveIntentSell: { enable: false }, autoAddInvalidItems: { enable: true }, autoAddInvalidUnusual: { enable: false }, autoAddPaintedItems: { enable: true }, priceAge: { maxInSeconds: 28800 } }, bypass: { escrow: { allow: false }, overpay: { allow: true }, giftWithoutMessage: { allow: false } }, tradeSummary: { declinedTrade: { enable: false }, showStockChanges: false, showTimeTakenInMS: false, showDetailedTimeTaken: true, showItemPrices: true, showPureInEmoji: false, showProperName: false, showOfferMessage: false, customText: { summary: { steamChat: 'Summary', discordWebhook: '__**Summary**__' }, asked: { steamChat: 'โข Asked:', discordWebhook: '**โข Asked:**' }, offered: { steamChat: 'โข Offered:', discordWebhook: '**โข Offered:**' }, offerMessage: { steamChat: '๐ฌ Offer message:', discordWebhook: '๐ฌ **Offer message:**' }, profitFromOverpay: { steamChat: '๐ Profit from overpay:', discordWebhook: '๐ ***Profit from overpay:***' }, lossFromUnderpay: { steamChat: '๐ Loss from underpay:', discordWebhook: '๐ ***Loss from underpay:***' }, timeTaken: { steamChat: 'โฑ Time taken:', discordWebhook: 'โฑ **Time taken:**' }, keyRate: { steamChat: '๐ Key rate:', discordWebhook: '๐ Key rate:' }, pureStock: { steamChat: '๐ฐ Pure stock:', discordWebhook: '๐ฐ Pure stock:' }, totalItems: { steamChat: '๐ Total items:', discordWebhook: '๐ Total items:' }, spells: '๐ Spells:', strangeParts: '๐ฐ Parts:', killstreaker: '๐ฅ Killstreaker:', sheen: 'โจ Sheen:', painted: '๐จ Painted:' } }, steamChat: { customInitializer: { acceptedTradeSummary: '/me', declinedTradeSummary: '/me', review: '', message: { onReceive: '/quote', toOtherAdmins: '/quote' // toTradePartner is in commands.message.customReply.fromOwner } }, notifyTradePartner: { onSuccessAccepted: true, onSuccessAcceptedEscrow: true, onDeclined: true, onCancelled: true, onTradedAway: true, onOfferForReview: true } }, highValue: { enableHold: true, spells: { names: [], exceptionSkus: [] }, sheens: { names: [], exceptionSkus: [] }, killstreakers: { names: [], exceptionSkus: [] }, strangeParts: { names: [], exceptionSkus: [] }, painted: { names: [], exceptionSkus: [] } }, normalize: { festivized: { our: false, their: false, amountIncludeNonFestivized: false }, strangeAsSecondQuality: { our: false, their: false, amountIncludeNonStrange: false }, painted: { our: true, their: true, amountIncludeNonPainted: false } }, details: { buy: 'I am buying your %name% for %price%, I have %current_stock% / %max_stock%.', sell: 'I am selling my %name% for %price%, I am selling %amount_trade%.', showBoldText: { onPrice: false, onAmount: false, onCurrentStock: false, onMaxStock: false, style: 1 }, highValue: { showSpells: true, showStrangeParts: false, showKillstreaker: true, showSheen: true, showPainted: true, customText: { spells: '๐ Spells:', strangeParts: '๐ฐ Parts:', killstreaker: '๐คฉ Killstreaker:', sheen: 'โจ Sheen:', painted: '๐จ Painted:', separator: '| ', ender: ' |' } }, uses: { duel: '(๐ข๐ก๐๐ฌ ๐ช๐๐ง๐ ๐ฑx ๐จ๐ฆ๐๐ฆ)', noiseMaker: '(๐ข๐ก๐๐ฌ ๐ช๐๐ง๐ ๐๐ฑx ๐จ๐ฆ๐๐ฆ)' } }, statistics: { lastTotalTrades: 0, startingTimeInUnix: 0, lastTotalProfitMadeInRef: 0, lastTotalProfitOverpayInRef: 0, profitDataSinceInUnix: 0, sendStats: { enable: false, time: [] } }, autokeys: { enable: false, minKeys: 3, maxKeys: 15, minRefined: 30, maxRefined: 150, banking: { enable: false }, scrapAdjustment: { enable: false, value: 1 }, accept: { understock: false } }, crafting: { manual: false, weapons: { enable: true }, metals: { enable: true, minScrap: 9, minRec: 9, threshold: 9 } }, offerReceived: { sendPreAcceptMessage: { enable: true }, alwaysDeclineNonTF2Items: true, // ๐ฅ_INVALID_VALUE invalidValue: { autoDecline: { enable: true, declineReply: '' }, exceptionValue: { skus: [], valueInRef: 0 } }, // ๐จ_INVALID_ITEMS invalidItems: { givePrice: false, autoAcceptOverpay: true, autoDecline: { enable: false, declineReply: '' } }, // ๐ง_DISABLED_ITEMS disabledItems: { autoAcceptOverpay: false, autoDecline: { enable: false, declineReply: '' } }, // ๐ฆ_OVERSTOCKED overstocked: { autoAcceptOverpay: false, autoDecline: { enable: false, declineReply: '' } }, // ๐ฉ_UNDERSTOCKED understocked: { autoAcceptOverpay: false, autoDecline: { enable: false, declineReply: '' } }, // ๐ซ_DUPED_ITEMS duped: { enableCheck: true, minKeys: 10, autoDecline: { enable: false, declineReply: '' } }, // ๐ช_DUPE_CHECK_FAILED failedToCheckDuped: { autoDecline: { enable: false, declineReply: '' } }, // โฌ_ESCROW_CHECK_FAILED escrowCheckFailed: { ignoreFailed: false }, // โฌ_BANNED_CHECK_FAILED bannedCheckFailed: { ignoreFailed: false }, // โฌ_HALTED halted: { ignoreHalted: false } }, manualReview: { enable: true, showOfferSummary: true, showReviewOfferNote: true, showOwnerCurrentTime: true, showItemPrices: true, // ๐ฅ_INVALID_VALUE invalidValue: { note: '' }, // ๐จ_INVALID_ITEMS invalidItems: { note: '' }, // ๐ง_DISABLED_ITEMS disabledItems: { note: '' }, // ๐ฆ_OVERSTOCKED overstocked: { note: '' }, // ๐ฉ_UNDERSTOCKED understocked: { note: '' }, // ๐ซ_DUPED_ITEMS duped: { note: '' }, // ๐ช_DUPE_CHECK_FAILED dupedCheckFailed: { note: '' }, // โฌ_ESCROW_CHECK_FAILED escrowCheckFailed: { note: '' }, // โฌ_BANNED_CHECK_FAILED bannedCheckFailed: { note: '' }, // โฌ_HALTED halted: { note: '' }, additionalNotes: '' }, discordWebhook: { ownerID: [], displayName: '', avatarURL: '', embedColor: '9171753', tradeSummary: { enable: true, url: [], misc: { showQuickLinks: true, showKeyRate: true, showPureStock: true, showInventory: true, note: '' }, mentionOwner: { enable: false, itemSkus: [], tradeValueInRef: 0 } }, declinedTrade: { enable: true, url: [], misc: { showQuickLinks: true, showKeyRate: true, showPureStock: true, showInventory: true, note: '' } }, offerReview: { enable: true, url: '', mentionInvalidValue: true, isMention: true, misc: { showQuickLinks: true, showKeyRate: true, showPureStock: true, showInventory: true } }, messages: { enable: true, isMention: true, url: '', showQuickLinks: true }, priceUpdate: { enable: true, showOnlyInStock: false, showFailedToUpdate: true, url: '', note: '' }, sendAlert: { enable: true, isMention: true, url: { main: '', partialPriceUpdate: '' } }, sendStats: { enable: false, url: '' } }, customMessage: { sendOffer: '', counterOffer: '', welcome: '', iDontKnowWhatYouMean: '', success: '', successEscrow: '', halted: '', decline: { general: '', hasNonTF2Items: '', giftNoNote: '', giftFailedCheckBanned: '', crimeAttempt: '', onlyMetal: '', duelingNot5Uses: '', noiseMakerNot25Uses: '', highValueItemsNotSelling: '', notTradingKeys: '', notSellingKeys: '', notBuyingKeys: '', halted: '', banned: '', escrow: '', manual: '', failedToCounter: '', takingItemsWithIntentBuy: '', givingItemsWithIntentSell: '' }, accepted: { automatic: { largeOffer: '', smallOffer: '' }, manual: { largeOffer: '', smallOffer: '' } }, tradedAway: '', failedMobileConfirmation: '', cancelledActiveForAwhile: '', clearFriends: '' }, commands: { enable: true, customDisableReply: '', how2trade: { customReply: { reply: '' } }, price: { enable: true, customReply: { disabled: '' } }, buy: { enable: true, disableForSKU: [], customReply: { disabled: '', disabledForSKU: '' } }, sell: { enable: true, disableForSKU: [], customReply: { disabled: '', disabledForSKU: '' } }, buycart: { enable: true, disableForSKU: [], customReply: { disabled: '', disabledForSKU: '' } }, sellcart: { enable: true, disableForSKU: [], customReply: { disabled: '', disabledForSKU: '' } }, cart: { enable: true, customReply: { title: '', disabled: '' } }, clearcart: { // always enable customReply: { reply: '' } }, checkout: { // always enable customReply: { empty: '' } }, addToQueue: { alreadyHaveActiveOffer: '', alreadyInQueueProcessingOffer: '', alreadyInQueueWaitingTurn: '', addedToQueueWaitingTurn: '', alteredOffer: '', processingOffer: { donation: '', isBuyingPremium: '', offer: '' }, hasBeenMadeAcceptingMobileConfirmation: { donation: '', isBuyingPremium: '', offer: '' } }, cancel: { // always enable customReply: { isBeingSent: '', isCancelling: '', isRemovedFromQueue: '', noActiveOffer: '', successCancel: '' } }, queue: { // always enable customReply: { notInQueue: '', offerBeingMade: '', hasPosition: '' } }, owner: { enable: true, customReply: { disabled: '', reply: '' } }, discord: { enable: true, inviteURL: '', customReply: { disabled: '', reply: '' } }, more: { enable: true, customReply: { disabled: '' } }, autokeys: { enable: true, customReply: { disabled: '' } }, message: { enable: true, showOwnerName: true, customReply: { disabled: '', wrongSyntax: '', fromOwner: '', success: '' } }, time: { enable: true, customReply: { disabled: '', reply: '' } }, uptime: { enable: true, customReply: { disabled: '', reply: '' } }, pure: { enable: true, customReply: { disabled: '', reply: '' } }, rate: { enable: true, customReply: { disabled: '', reply: '' } }, stock: { enable: true, maximumItems: 20, customReply: { disabled: '', reply: '' } }, craftweapon: { enable: true, showOnlyExist: true, customReply: { disabled: '', dontHave: '', have: '' } }, uncraftweapon: { enable: true, showOnlyExist: true, customReply: { disabled: '', dontHave: '', have: '' } } }, detailsExtra: { /** * Custom string to be shown in listing note if details.highValue.showSpells set to true */ spells: { 'Putrescent Pigmentation': 'PP ๐', 'Die Job': 'DJ ๐', 'Chromatic Corruption': 'CC ๐ช', 'Spectral Spectrum': 'Spec ๐ต๐ด', 'Sinister Staining': 'Sin ๐', 'Voices From Below': 'VFB ๐ฃ๏ธ', 'Team Spirit Footprints': 'TS-FP ๐ต๐ด', 'Gangreen Footprints': 'GG-FP ๐ก', 'Corpse Gray Footprints': 'CG-FP ๐ฝ', 'Violent Violet Footprints': 'VV-FP โจ๏ธ', 'Rotten Orange Footprints': 'RO-FP ๐', 'Bruised Purple Footprints': 'BP-FP ๐ท', 'Headless Horseshoes': 'HH ๐', Exorcism: '๐ป', 'Pumpkin Bombs': '๐๐ฃ', 'Halloween Fire': '๐ฅ๐ข' }, /** * Custom string to be shown in listing note if details.highValue.showSheen set to true */ sheens: { 'Team Shine': '๐ต๐ด', 'Hot Rod': '๐ธ', Manndarin: '๐ ', 'Deadly Daffodil': '๐ก', 'Mean Green': '๐ข', 'Agonizing Emerald': '๐ฉ', 'Villainous Violet': '๐ฃ' }, /** * Custom string to be shown in listing note if details.highValue.showKillstreaker set to true */ killstreakers: { 'Cerebral Discharge': 'โก', 'Fire Horns': '๐ฅ๐ฎ', Flames: '๐ฅ', 'Hypno-Beam': '๐ต๐ซ', Incinerator: '๐ฌ', Singularity: '๐', Tornado: '๐ช๏ธ' }, /** * painted.stringNote: Custom string to be shown in listing note if details.highValue.showPainted set to true * painted.price: Paint price to be added with the item base price to automatically create sell order for painted items. */ painted: { 'A Color Similar to Slate': { stringNote: '๐งช', price: { keys: 0, metal: 11 } }, 'A Deep Commitment to Purple': { stringNote: '๐ช', price: { keys: 0, metal: 15 } }, 'A Distinctive Lack of Hue': { stringNote: '๐ฉ', price: { keys: 1, metal: 5 } }, "A Mann's Mint": { stringNote: '๐ฝ', price: { keys: 0, metal: 30 } }, 'After Eight': { stringNote: '๐ด', price: { keys: 1, metal: 5 } }, 'Aged Moustache Grey': { stringNote: '๐ค', price: { keys: 0, metal: 5 } }, 'An Extraordinary Abundance of Tinge': { stringNote: '๐', price: { keys: 1, metal: 5 } }, 'Australium Gold': { stringNote: '๐', price: { keys: 0, metal: 15 } }, 'Color No. 216-190-216': { stringNote: '๐ง ', price: { keys: 0, metal: 7 } }, 'Dark Salmon Injustice': { stringNote: '๐', price: { keys: 0, metal: 15 } }, 'Drably Olive': { stringNote: '๐ฅ', price: { keys: 0, metal: 5 } }, 'Indubitably Green': { stringNote: '๐ฅฆ', price: { keys: 0, metal: 5 } }, 'Mann Co. Orange': { stringNote: '๐', price: { keys: 0, metal: 6 } }, Muskelmannbraun: { stringNote: '๐', price: { keys: 0, metal: 2 } }, "Noble Hatter's Violet": { stringNote: '๐', price: { keys: 0, metal: 7 } }, 'Peculiarly Drab Tincture': { stringNote: '๐ช', price: { keys: 0, metal: 3 } }, 'Pink as Hell': { stringNote: '๐', price: { keys: 1, metal: 10 } }, 'Radigan Conagher Brown': { stringNote: '๐ช', price: { keys: 0, metal: 2 } }, 'The Bitter Taste of Defeat and Lime': { stringNote: '๐', price: { keys: 1, metal: 10 } }, "The Color of a Gentlemann's Business Pants": { stringNote: '๐งฝ', price: { keys: 0, metal: 5 } }, 'Ye Olde Rustic Colour': { stringNote: '๐ฅ', price: { keys: 0, metal: 2 } }, "Zepheniah's Greed": { stringNote: '๐ณ', price: { keys: 0, metal: 4 } }, 'An Air of Debonair': { stringNote: '๐๐ท', price: { keys: 0, metal: 30 } }, 'Balaclavas Are Forever': { stringNote: '๐๐ท', price: { keys: 0, metal: 30 } }, "Operator's Overalls": { stringNote: '๐๐ท', price: { keys: 0, metal: 30 } }, 'Cream Spirit': { stringNote: '๐๐ฅฎ', price: { keys: 0, metal: 30 } }, 'Team Spirit': { stringNote: '๐ต๐ด', price: { keys: 0, metal: 30 } }, 'The Value of Teamwork': { stringNote: '๐', price: { keys: 0, metal: 30 } }, 'Waterlogged Lab Coat': { stringNote: '๐', price: { keys: 0, metal: 30 } }, 'Legacy Paint': { stringNote: '๐ตโ', price: { keys: 4, metal: 0 } } }, /** * Custom string to be shown in listing note if details.highValue.showStrangeParts set to true */ strangeParts: { 'Robots Destroyed': '', Kills: '', 'Airborne Enemy Kills': '', 'Damage Dealt': '', Dominations: '', 'Snipers Killed': '', 'Buildings Destroyed': '', 'Projectiles Reflected': '', 'Headshot Kills': '', 'Medics Killed': '', 'Fires Survived': '', 'Teammates Extinguished': '', 'Freezecam Taunt Appearances': '', 'Spies Killed': '', 'Allied Healing Done': '', 'Sappers Removed': '', 'Players Hit': '', 'Gib Kills': '', 'Scouts Killed': '', 'Taunt Kills': '', 'Point Blank Kills': '', 'Soldiers Killed': '', 'Long-Distance Kills': '', 'Giant Robots Destroyed': '', 'Critical Kills': '', 'Demomen Killed': '', 'Unusual-Wearing Player Kills': '', Assists: '', 'Medics Killed That Have Full รberCharge': '', 'Cloaked Spies Killed': '', 'Engineers Killed': '', 'Kills While Explosive-Jumping': '', 'Kills While Low Health': '', 'Burning Player Kills': '', 'Kills While Invuln รberCharged': '', 'Posthumous Kills': '', 'Not Crit nor MiniCrit Kills': '', 'Full Health Kills': '', 'Killstreaks Ended': '', 'Defenders Killed': '', Revenges: '', 'Robot Scouts Destroyed': '', 'Heavies Killed': '', 'Tanks Destroyed': '', 'Kills During Halloween': '', 'Pyros Killed': '', 'Submerged Enemy Kills': '', 'Kills During Victory Time': '', 'Taunting Player Kills': '', 'Robot Spies Destroyed': '', 'Kills Under A Full Moon': '', 'Robots Killed During Halloween': '' } } }; interface OnlyEnable { enable?: boolean; } // ------------ SortType ------------ interface SortInventory extends OnlyEnable { type?: number; } // ------------ WeaponsAsCurrency ------------ interface WeaponsAsCurrency extends OnlyEnable { withUncraft?: boolean; } // ------------ CheckUses ------------ interface CheckUses { duel?: boolean; noiseMaker?: boolean; } // ------------ Game ------------ interface Game { playOnlyTF2?: boolean; customName?: string; } // ------------ Counteroffer ------------ interface Counteroffer extends OnlyEnable { skipIncludeMessage?: boolean; } // --------- Misc Settings ---------- interface MiscSettings { showOnlyMetal?: OnlyEnable; sortInventory?: SortInventory; createListings?: OnlyEnable; counterOffer?: Counteroffer; addFriends?: OnlyEnable; sendGroupInvite?: OnlyEnable; skipItemsInTrade?: OnlyEnable; weaponsAsCurrency?: WeaponsAsCurrency; checkUses?: CheckUses; game?: Game; alwaysRemoveItemAttributes?: AlwaysRemoveItemAttributes; deleteUntradableJunk?: OnlyEnable; reputationCheck?: ReputationCheck; } interface ReputationCheck { checkMptfBanned?: boolean; reptfAsPrimarySource?: boolean; } interface AlwaysRemoveItemAttributes { customTexture?: OnlyEnable; // giftedByTag?: OnlyEnable; } // ------------ SendAlert ------------ interface SendAlert extends OnlyEnable { autokeys?: AutokeysAlert; backpackFull?: boolean; highValue?: HighValueAlert; autoRemoveIntentSellFailed?: boolean; autoAddPaintedItems?: boolean; failedAccept?: boolean; unableToProcessOffer?: boolean; partialPrice?: PartialPrice; receivedUnusualNotInPricelist?: boolean; failedToUpdateOldPrices?: boolean; } interface PartialPrice { onUpdate?: boolean; onSuccessUpdatePartialPriced?: boolean; onFailedUpdatePartialPriced?: boolean; onBulkUpdatePartialPriced?: boolean; onResetAfterThreshold?: boolean; } interface AutokeysAlert { lowPure?: boolean; failedToAdd?: boolean; failedToUpdate?: boolean; failedToDisable?: boolean; } interface HighValueAlert { gotDisabled?: boolean; receivedNotInPricelist?: boolean; tryingToTake?: boolean; } // ------------ Pricelist ------------ interface Pricelist { partialPriceUpdate?: PartialPriceUpdate; filterCantAfford?: OnlyEnable; autoRemoveIntentSell?: OnlyEnable; autoAddInvalidItems?: OnlyEnable; autoAddInvalidUnusual?: OnlyEnable; autoAddPaintedItems?: OnlyEnable; priceAge?: PriceAge; } interface PartialPriceUpdate extends OnlyEnable { thresholdInSeconds?: number; excludeSKU?: string[]; } interface PriceAge { maxInSeconds?: number; } // ------------ Bypass ------------ interface Bypass { escrow?: OnlyAllow; overpay?: OnlyAllow; giftWithoutMessage?: OnlyAllow; } interface OnlyAllow { allow?: boolean; } // ------------ TradeSummary ------------ export interface TradeSummary { declinedTrade?: OnlyEnable; showStockChanges?: boolean; showTimeTakenInMS?: boolean; showDetailedTimeTaken?: boolean; showItemPrices?: boolean; showPureInEmoji?: boolean; showProperName?: boolean; showOfferMessage?: boolean; customText?: TradeSummaryCustomText; } interface TradeSummaryCustomText { summary: SteamDiscord; asked: SteamDiscord; offered: SteamDiscord; offerMessage: SteamDiscord; profitFromOverpay: SteamDiscord; lossFromUnderpay: SteamDiscord; timeTaken: SteamDiscord; keyRate: SteamDiscord; pureStock: SteamDiscord; totalItems: SteamDiscord; spells: string; strangeParts: string; killstreaker: string; sheen: string; painted: string; } interface SteamDiscord { steamChat?: string; discordWebhook?: string; } // ----------- Steam Chat ------------ interface SteamChat { customInitializer?: CustomInitializer; notifyTradePartner?: NotifyTradePartner; } interface CustomInitializer { acceptedTradeSummary?: string; declinedTradeSummary?: string; review?: string; message?: CustomInitializerMessage; } interface CustomInitializerMessage { onReceive?: string; toOtherAdmins?: string; } interface NotifyTradePartner { onSuccessAccepted: boolean; onSuccessAcceptedEscrow: boolean; onDeclined: boolean; onCancelled: boolean; onTradedAway: boolean; onOfferForReview: boolean; } // ------------ HighValue ------------ export interface HighValue { enableHold?: boolean; spells?: HighValueContent; sheens?: HighValueContent; killstreakers?: HighValueContent; strangeParts?: HighValueContent; painted?: HighValueContent; } interface HighValueContent { names: string[]; exceptionSkus: string[]; } // ------------ Normalize ------------ interface Normalize { festivized?: NormalizeFestivized; strangeAsSecondQuality?: NormalizeStrange; painted?: NormalizePainted; } interface NormalizeOurOrTheir { our?: boolean; their?: boolean; } interface NormalizeFestivized extends NormalizeOurOrTheir { amountIncludeNonFestivized?: boolean; } interface NormalizeStrange extends NormalizeOurOrTheir { amountIncludeNonStrange?: boolean; } interface NormalizePainted extends NormalizeOurOrTheir { amountIncludeNonPainted?: boolean; } // ------------ Details ------------ interface Details { buy?: string; sell?: string; showBoldText?: ShowBoldText; highValue?: ShowHighValue; uses?: UsesDetails; } interface ShowBoldText { onPrice: boolean; onAmount: boolean; onCurrentStock: boolean; onMaxStock: boolean; style: number; } interface ShowHighValue { showSpells?: boolean; showStrangeParts?: boolean; showKillstreaker?: boolean; showSheen?: boolean; showPainted?: boolean; customText?: HighValueCustomText; } interface HighValueCustomText { spells?: string; strangeParts?: string; killstreaker?: string; sheen?: string; painted?: string; separator?: string; ender?: string; } interface UsesDetails { duel?: string; noiseMaker?: string; } // ------------ Statistics ------------ interface Statistics { lastTotalTrades?: number; startingTimeInUnix?: number; lastTotalProfitMadeInRef?: number; lastTotalProfitOverpayInRef?: number; profitDataSinceInUnix?: number; sendStats?: SendStats; } interface SendStats extends OnlyEnable { time?: string[]; } // ------------ Autokeys ------------ interface Autokeys { enable?: boolean; minKeys?: number; maxKeys?: number; minRefined?: number; maxRefined?: number; banking?: Banking; scrapAdjustment?: ScrapAdjustment; accept?: Accept; } interface Banking { enable?: boolean; } interface ScrapAdjustment { enable?: boolean; value?: number; } interface Accept { understock?: boolean; } // ------------ Crafting ------------ interface Crafting { manual?: boolean; weapons?: OnlyEnable; metals?: Metals; } interface Metals extends OnlyEnable { minScrap?: number; minRec?: number; threshold?: number; } // ------------ Offer Received ------------ interface OfferReceived { sendPreAcceptMessage?: OnlyEnable; alwaysDeclineNonTF2Items?: boolean; invalidValue?: InvalidValue; invalidItems?: InvalidItems; disabledItems?: AutoAcceptOverpayAndAutoDecline; overstocked?: AutoAcceptOverpayAndAutoDecline; understocked?: AutoAcceptOverpayAndAutoDecline; duped?: Duped; failedToCheckDuped: FailedToCheckDuped; escrowCheckFailed?: EscrowBannedCheckFailed; bannedCheckFailed?: EscrowBannedCheckFailed; halted?: Halted; } interface DeclineReply extends OnlyEnable { declineReply?: string; } interface InvalidValue { autoDecline: DeclineReply; exceptionValue: ExceptionValue; } interface ExceptionValue { skus: string[]; valueInRef: number; } interface AutoAcceptOverpayAndAutoDecline { autoAcceptOverpay?: boolean; autoDecline?: DeclineReply; } interface InvalidItems extends AutoAcceptOverpayAndAutoDecline { givePrice?: boolean; } interface Duped { enableCheck?: boolean; minKeys?: number; autoDecline?: DeclineReply; } interface FailedToCheckDuped { autoDecline?: DeclineReply; } interface EscrowBannedCheckFailed { ignoreFailed?: boolean; } interface Halted { ignoreHalted: boolean; } // ------------ Manual Review ------------ interface ManualReview extends OnlyEnable { showOfferSummary?: boolean; showReviewOfferNote?: boolean; showOwnerCurrentTime?: boolean; showItemPrices?: boolean; invalidValue?: OnlyNote; invalidItems?: OnlyNote; disabledItems?: OnlyNote; overstocked?: OnlyNote; understocked?: OnlyNote; duped?: OnlyNote; dupedCheckFailed?: OnlyNote; escrowCheckFailed?: OnlyNote; bannedCheckFailed?: OnlyNote; halted: OnlyNote; additionalNotes?: string; } // ------------ Discord Webhook ------------ interface DiscordWebhook { ownerID?: string[]; displayName?: string; avatarURL?: string; embedColor?: string; tradeSummary?: TradeSummaryDW; declinedTrade?: DeclinedTradeDW; offerReview?: OfferReviewDW; messages?: MessagesDW; priceUpdate?: PriceUpdateDW; sendAlert?: SendAlertStatsDW; sendStats?: SendStatsDW; } interface TradeSummaryDW extends OnlyEnable { url?: string[]; misc?: MiscTradeSummary; mentionOwner?: MentionOwner; } interface DeclinedTradeDW extends OnlyEnable { url?: string[]; misc?: MiscTradeSummary; } interface OnlyNote { note?: string; } interface MiscTradeSummary extends OnlyNote { showQuickLinks?: boolean; showKeyRate?: boolean; showPureStock?: boolean; showInventory?: boolean; } interface MentionOwner extends OnlyEnable { itemSkus?: string[]; tradeValueInRef?: number; } interface OfferReviewDW extends OnlyEnable { url?: string; mentionInvalidValue?: boolean; isMention?: boolean; misc?: MiscOfferReview; } interface MiscOfferReview { showQuickLinks?: boolean; showKeyRate?: boolean; showPureStock?: boolean; showInventory?: boolean; } interface MessagesDW extends OnlyEnable { url?: string; isMention?: boolean; showQuickLinks?: boolean; } interface PriceUpdateDW extends OnlyEnable, OnlyNote { showOnlyInStock?: boolean; showFailedToUpdate?: boolean; url?: string; } interface SendAlertStatsDW extends OnlyEnable { isMention?: boolean; url?: { main: string; partialPriceUpdate: string; }; } interface SendStatsDW extends OnlyEnable { url?: string; } // ------------ Custom Message ------------ interface CustomMessage { sendOffer?: string; counterOffer?: string; welcome?: string; iDontKnowWhatYouMean?: string; success?: string; successEscrow?: string; halted?: string; decline?: DeclineNote; accepted?: AcceptedNote; tradedAway?: string; failedMobileConfirmation?: string; cancelledActiveForAwhile?: string; clearFriends?: string; } interface DeclineNote { general?: string; hasNonTF2Items?: string; giftNoNote?: string; giftFailedCheckBanned?: string; crimeAttempt?: string; onlyMetal?: string; duelingNot5Uses?: string; noiseMakerNot25Uses?: string; highValueItemsNotSelling?: string; notTradingKeys?: string; notSellingKeys?: string; notBuyingKeys?: string; halted?: string; banned?: string; escrow?: string; manual?: string; failedToCounter?: string; takingItemsWithIntentBuy?: string; givingItemsWithIntentSell?: string; } interface AcceptedNote { automatic?: OfferType; manual?: OfferType; } export interface OfferType { largeOffer: string; smallOffer: string; } // ------------ Commands ------------ interface OnlyCustomReplyWithDisabled { disabled?: string; reply?: string; } interface Commands extends OnlyEnable { customDisableReply?: string; how2trade?: How2Trade; price?: Price; buy?: SpecificOperation; sell?: SpecificOperation; buycart?: SpecificOperation; sellcart?: SpecificOperation; cart?: Cart; clearcart?: ClearCart; checkout?: Checkout; addToQueue?: AddToQueue; cancel?: Cancel; queue?: Queue; owner?: Owner; discord?: Discord; more?: More; autokeys?: AutokeysCommand; message?: Message; time?: Time; uptime?: Uptime; pure?: Pure; rate?: Rate; stock?: Stock; craftweapon?: Weapons; uncraftweapon?: Weapons; } interface SpecificOperation extends OnlyEnable { disableForSKU?: string[]; customReply?: CustomReplyForSpecificOperation; } interface CustomReplyForSpecificOperation { disabled?: string; disabledForSKU?: string; } interface How2Trade { customReply?: Omit<OnlyCustomReplyWithDisabled, 'disabled'>; } interface Price extends OnlyEnable { customReply?: Omit<OnlyCustomReplyWithDisabled, 'reply'>; } interface Cart extends OnlyEnable { customReply?: CartCustom; } interface CartCustom extends Omit<OnlyCustomReplyWithDisabled, 'reply'> { title?: string; } interface ClearCart { customReply?: Omit<OnlyCustomReplyWithDisabled, 'disabled'>; } interface Checkout { customReply?: CheckoutReply; } interface CheckoutReply { empty?: string; } interface AddToQueue { alreadyHaveActiveOffer?: string; alreadyInQueueProcessingOffer?: string; alreadyInQueueWaitingTurn?: string; addedToQueueWaitingTurn?: string; alteredOffer?: string; processingOffer?: CartQueueProcessing; hasBeenMadeAcceptingMobileConfirmation?: CartQueueProcessing; } interface CartQueueProcessing { donation?: string; isBuyingPremium?: string; offer?: string; } interface Cancel { customReply?: CancelCustom; } interface CancelCustom { isBeingSent?: string; isCancelling?: string; isRemovedFromQueue?: string; noActiveOffer?: string; successCancel?: string; } interface Queue { customReply?: QueueCustom; } interface QueueCustom { notInQueue?: string; offerBeingMade?: string; hasPosition?: string; } interface Owner extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; } export interface Discord extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; inviteURL?: string; } interface More extends OnlyEnable { customReply?: Omit<OnlyCustomReplyWithDisabled, 'reply'>; } interface AutokeysCommand extends OnlyEnable { customReply?: Omit<OnlyCustomReplyWithDisabled, 'reply'>; } interface Message extends OnlyEnable { showOwnerName?: boolean; customReply?: MessageCustom; } interface MessageCustom { disabled?: string; wrongSyntax?: string; fromOwner?: string; success?: string; } interface Time extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; } interface Uptime extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; } interface Pure extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; } interface Rate extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; } export interface Stock extends OnlyEnable { customReply?: OnlyCustomReplyWithDisabled; maximumItems?: number; } interface Weapons extends OnlyEnable { customReply?: HaveOrNo; showOnlyExist?: boolean; } interface HaveOrNo { disabled?: string; dontHave?: string; have?: string; } // ------------ Extra Details ----------- interface DetailsExtra { spells?: Spells; sheens?: Sheens; killstreakers?: Killstreakers; painted?: Painted; strangeParts?: StrangeParts; } interface Spells { 'Putrescent Pigmentation'?: string; 'Die Job'?: string; 'Chromatic Corruption'?: string; 'Spectral Spectrum'?: string; 'Sinister Staining'?: string; 'Voices From Below'?: string; 'Team Spirit Footprints'?: string; 'Gangreen Footprints'?: string; 'Corpse Gray Footprints'?: string; 'Violent Violet Footprints'?: string; 'Rotten Orange Footprints'?: string; 'Bruised Purple Footprints'?: string; 'Headless Horseshoes'?: string; Exorcism?: string; 'Pumpkin Bombs'?: string; 'Halloween Fire'?: string; } interface Sheens { 'Team Shine'?: string; 'Hot Rod'?: string; Manndarin?: string; 'Deadly Daffodil'?: string; 'Mean Green'?: string; 'Agonizing Emerald'?: string; 'Villainous Violet'?: string; } interface Killstreakers { 'Cerebral Discharge'?: string; 'Fire Horns'?: string; Flames?: string; 'Hypno-Beam'?: string; Incinerator?: string; Singularity?: string; Tornado?: string; } interface PaintedProperties { stringNote?: string; price?: Currency; } interface Painted { 'A Color Similar to Slate'?: PaintedProperties; 'A Deep Commitment to Purple'?: PaintedProperties; 'A Distinctive Lack of Hue'?: PaintedProperties; "A Mann's Mint"?: PaintedProperties; 'After Eight'?: PaintedProperties; 'Aged Moustache Grey'?: PaintedProperties; 'An Extraordinary Abundance of Tinge'?: PaintedProperties; 'Australium Gold'?: PaintedProperties; 'Color No. 216-190-216'?: PaintedProperties; 'Dark Salmon Injustice'?: PaintedProperties; 'Drably Olive'?: PaintedProperties; 'Indubitably Green'?: PaintedProperties; 'Mann Co. Orange'?: PaintedProperties; Muskelmannbraun?: PaintedProperties; "Noble Hatter's Violet"?: PaintedProperties; 'Peculiarly Drab Tincture'?: PaintedProperties; 'Pink as Hell'?: PaintedProperties; 'Radigan Conagher Brown'?: PaintedProperties; 'The Bitter Taste of Defeat and Lime'?: PaintedProperties; "The Color of a Gentlemann's Business Pants"?: PaintedProperties; 'Ye Olde Rustic Colour'?: PaintedProperties; "Zepheniah's Greed"?: PaintedProperties; 'An Air of Debonair'?: PaintedProperties; 'Balaclavas Are Forever'?: PaintedProperties; "Operator's Overalls"?: PaintedProperties; 'Cream Spirit'?: PaintedProperties; 'Team Spirit'?: PaintedProperties; 'The Value of Teamwork'?: PaintedProperties; 'Waterlogged Lab Coat'?: PaintedProperties; 'Legacy Paint'?: PaintedProperties; } export type PaintedNames = | 'A Color Similar to Slate' | 'A Deep Commitment to Purple' | 'A Distinctive Lack of Hue' | "A Mann's Mint" | 'After Eight' | 'Aged Moustache Grey' | 'An Extraordinary Abundance of Tinge' | 'Australium Gold' | 'Color No. 216-190-216' | 'Dark Salmon Injustice' | 'Drably Olive' | 'Indubitably Green' | 'Mann Co. Orange' | 'Muskelmannbraun' | "Noble Hatter's Violet" | 'Peculiarly Drab Tincture' | 'Pink as Hell' | 'Radigan Conagher Brown' | 'The Bitter Taste of Defeat and Lime' | "The Color of a Gentlemann's Business Pants" | 'Ye Olde Rustic Colour' | "Zepheniah's Greed" | 'An Air of Debonair' | 'Balaclavas Are Forever' | "Operator's Overalls" | 'Cream Spirit' | 'Team Spirit' | 'The Value of Teamwork' | 'Waterlogged Lab Coat' | 'Legacy Paint'; interface StrangeParts { 'Robots Destroyed'?: string; Kills?: string; 'Airborne Enemy Kills'?: string; 'Damage Dealt'?: string; Dominations?: string; 'Snipers Killed'?: string; 'Buildings Destroyed'?: string; 'Projectiles Reflected'?: string; 'Headshot Kills'?: string; 'Medics Killed'?: string; 'Fires Survived'?: string; 'Teammates Extinguished'?: string; 'Freezecam Taunt Appearances'?: string; 'Spies Killed'?: string; 'Allied Healing Done'?: string; 'Sappers Removed'?: string; 'Players Hit'?: string; 'Gib Kills'?: string; 'Scouts Killed'?: string; 'Taunt Kills'?: string; 'Point Blank Kills'?: string; 'Soldiers Killed'?: string; 'Long-Distance Kills'?: string; 'Giant Robots Destroyed'?: string; 'Critical Kills'?: string; 'Demomen Killed'?: string; 'Unusual-Wearing Player Kills'?: string; Assists?: string; 'Medics Killed That Have Full รberCharge'?: string; 'Cloaked Spies Killed'?: string; 'Engineers Killed': string; 'Kills While Explosive-Jumping': string; 'Kills While Low Health': string; 'Burning Player Kills': string; 'Kills While Invuln รberCharged': string; 'Posthumous Kills'?: string; 'Not Crit nor MiniCrit Kills'?: string; 'Full Health Kills'?: string; 'Killstreaks Ended'?: string; 'Defenders Killed'?: string; Revenges?: string; 'Robot Scouts Destroyed'?: string; 'Heavies Killed'?: string; 'Tanks Destroyed'?: string; 'Kills During Halloween'?: string; 'Pyros Killed'?: string; 'Submerged Enemy Kills'?: string; 'Kills During Victory Time'?: string; 'Taunting Player Kills'?: string; 'Robot Spies Destroyed'?: string; 'Kills Under A Full Moon'?: string; 'Robots Killed During Halloween'?: string; } // ------------ JsonOptions ------------ export interface JsonOptions { miscSettings?: MiscSettings; sendAlert?: SendAlert; pricelist?: Pricelist; bypass?: Bypass; tradeSummary?: TradeSummary; steamChat?: SteamChat; highValue?: HighValue; normalize?: Normalize; details?: Details; statistics?: Statistics; autokeys?: Autokeys; crafting?: Crafting; offerReceived?: OfferReceived; manualReview?: ManualReview; discordWebhook?: DiscordWebhook; customMessage?: CustomMessage; commands?: Commands; detailsExtra?: DetailsExtra; } export default interface Options extends JsonOptions { steamAccountName?: string; steamPassword?: string; steamSharedSecret?: string; steamIdentitySecret?: string; bptfAccessToken?: string; bptfApiKey?: string; useragentHeaderCustom?: string; mptfApiKey?: string; admins?: string[]; keep?: string[]; itemStatsWhitelist?: string[]; groups?: string[]; alerts?: string[]; enableSocket?: boolean; customPricerApiToken?: string; customPricerUrl?: string; skipBPTFTradeofferURL?: boolean; skipUpdateProfileSettings?: boolean; timezone?: string; customTimeFormat?: string; timeAdditionalNotes?: string; debug?: boolean; debugFile?: boolean; enableSaveLogFile?: boolean; folderName?: string; filePrefix?: string; enableHttpApi?: boolean; httpApiPort?: number; } function getOption<T>(option: string, def: T, parseFn: (target: string) => T, options?: Options): T { try { if (options && options[option]) { return options[option] as T; } const envVar = snakeCase(option).toUpperCase(); return process.env[envVar] ? parseFn(process.env[envVar]) : def; } catch { return def; } } function throwLintError(filepath: string, e: Error): void { if (e instanceof Error && 'message' in e) { throw new Error(`${filepath}\n${e.message}`); } throw e; } function lintPath(filepath: string): void { const rawOptions = readFileSync(filepath, { encoding: 'utf8' }); try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call jsonlint.parse(rawOptions); } catch (e) { throwLintError(filepath, e as Error); } } function lintAllTheThings(directory: string): void { if (existsSync(directory)) { readdirSync(directory, { withFileTypes: true }) .filter(ent => path.extname(ent.name) === '.json') .forEach(ent => lintPath(path.join(directory, ent.name))); } } function loadJsonOptions(optionsPath: string, options?: Options): JsonOptions { let fileOptions; const workingDefault = deepMerge({}, DEFAULTS); const incomingOptions = options ? deepMerge({}, options) : deepMerge({}, DEFAULTS); try { const rawOptions = readFileSync(optionsPath, { encoding: 'utf8' }); try { const parsedRaw = JSON.parse(rawOptions) as JsonOptions; if (replaceOldProperties(parsedRaw)) { writeFileSync(optionsPath, JSON.stringify(parsedRaw, null, 4), { encoding: 'utf8' }); } fileOptions = deepMerge({}, workingDefault, parsedRaw); return deepMerge(fileOptions, incomingOptions); } catch (e) { if (e instanceof SyntaxError) { // lint the rawOptions to give better feedback since it is SyntaxError try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call jsonlint.parse(rawOptions); } catch (e) { throwLintError(optionsPath, e as Error); } } throw e; } } catch (e) { // file or directory is missing or something else is wrong if (!existsSync(path.dirname(optionsPath))) { // check for dir mkdirSync(path.dirname(optionsPath), { recursive: true }); writeFileSync(optionsPath, JSON.stringify(DEFAULTS, null, 4), { encoding: 'utf8' }); return deepMerge({}, DEFAULTS); } else if (!existsSync(optionsPath)) { // directory is present, see if file was missing writeFileSync(optionsPath, JSON.stringify(DEFAULTS, null, 4), { encoding: 'utf8' }); return deepMerge({}, DEFAULTS); } else { // something else is wrong, throw the error throw e; } } } export function removeCliOptions(incomingOptions: Options): void { const findNonEnv = validator(incomingOptions, 'options'); if (findNonEnv) { findNonEnv .filter(e => e.includes('unknown property')) .map(e => e.slice(18, -1)) .map(e => delete incomingOptions[e]); } } function replaceOldProperties(options: Options): boolean { // Automatically replace old properties let isChanged = false; // <= v4.1.5 โ v4.2.0 const hv = options.highValue; if (hv) { const spells = hv.spells; if (Array.isArray(spells)) { options.highValue.spells = { names: spells, exceptionSkus: [] }; isChanged = true; } const sheens = hv.sheens; if (Array.isArray(sheens)) { options.highValue.sheens = { names: sheens, exceptionSkus: [] }; isChanged = true; } const killstreakers = hv.killstreakers; if (Array.isArray(killstreakers)) { options.highValue.killstreakers = { names: killstreakers, exceptionSkus: [] }; isChanged = true; } const strangeParts = hv.strangeParts; if (Array.isArray(strangeParts)) { options.highValue.strangeParts = { names: strangeParts, exceptionSkus: [] }; isChanged = true; } const painted = hv.painted; if (Array.isArray(painted)) { options.highValue.painted = { names: painted, exceptionSkus: [] }; isChanged = true; } } // <= v4.2.0 โ v4.2.1 if (options.discordWebhook) { const ownerID = options.discordWebhook.ownerID; if (!Array.isArray(ownerID)) { options.discordWebhook.ownerID = ownerID === '' ? [] : [ownerID]; isChanged = true; } else { // Automatically remove first element if it's an emptry string // (was accidentally added when updating from <= v4.2.0 to v4.2.4) if (ownerID[0] === '') { if (ownerID.length > 1) { options.discordWebhook.ownerID.shift(); } else { options.discordWebhook.ownerID.length = 0; } isChanged = true; } } } // v4.4.3/v4.4.4 -> v4.4.5 - Automatically remove takingItemsWithZeroSellingPrice // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore if (options.customMessage?.decline?.takingItemsWithZeroSellingPrice !== undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore delete options.customMessage.decline.takingItemsWithZeroSellingPrice; options.customMessage.decline['takingItemsWithIntentBuy'] = ''; options.customMessage.decline['givingItemsWithIntentSell'] = ''; isChanged = true; } // v4.7.0 -> v4.7.1 - Automatically remove miscSettings.autobump // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore if (options.miscSettings?.autobump !== undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore delete options.miscSettings.autobump; isChanged = true; } // v4.8.0 -> v4.9.0 - Automatically make room to separate different discord urls for sendAlert if (typeof options.discordWebhook?.sendAlert?.url === 'string') { const mainUrl = options.discordWebhook.sendAlert.url; options.discordWebhook.sendAlert.url = { main: mainUrl, partialPriceUpdate: '' }; isChanged = true; } // v4.12.1 -> v4.13.0 /*eslint-disable */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore if (options.bypass?.bannedPeople !== undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore const mptfCheckValue = options.bypass.bannedPeople?.checkMptfBanned; if (options.miscSettings.reputationCheck !== undefined) { options.miscSettings.reputationCheck.checkMptfBanned = typeof mptfCheckValue === 'boolean' ? mptfCheckValue : true; } else { options.miscSettings['reputationCheck'] = { checkMptfBanned: process.env.MPTF_API_KEY !== undefined ? mptfCheckValue : false, // below v4.13.0 -> v4.13.1 reptfAsPrimarySource: true }; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore delete options.bypass.bannedPeople; isChanged = true; } /*eslint-enable */ return isChanged; } export function loadOptions(options?: Options): Options { const incomingOptions = options ? deepMerge({}, options) : {}; const steamAccountName = getOption('steamAccountName', '', String, incomingOptions); lintAllTheThings(getFilesPath(steamAccountName)); // you shall not pass const jsonParseArray = (jsonString: string): string[] => JSON.parse(jsonString) as unknown as string[]; const jsonParseBoolean = (jsonString: string): boolean => JSON.parse(jsonString) as unknown as boolean; const jsonParseNumber = (jsonString: string): number => JSON.parse(jsonString) as unknown as number; const envOptions = { steamAccountName: steamAccountName, steamPassword: getOption('steamPassword', '', String, incomingOptions), steamSharedSecret: getOption('steamSharedSecret', '', String, incomingOptions), steamIdentitySecret: getOption('steamIdentitySecret', '', String, incomingOptions), bptfAccessToken: getOption('bptfAccessToken', '', String, incomingOptions), bptfApiKey: getOption('bptfApiKey', '', String, incomingOptions), useragentHeaderCustom: getOption('useragentHeaderCustom', '', String, incomingOptions), mptfApiKey: getOption('mptfApiKey', '', String, incomingOptions), admins: getOption('admins', [], jsonParseArray, incomingOptions), keep: getOption('keep', [], jsonParseArray, incomingOptions), itemStatsWhitelist: getOption('itemStatsWhitelist', [], jsonParseArray, incomingOptions), groups: getOption('groups', ['103582791469033930'], jsonParseArray, incomingOptions), alerts: getOption('alerts', ['trade'], jsonParseArray, incomingOptions), enableSocket: getOption('enableSocket', true, jsonParseBoolean, incomingOptions), customPricerApiToken: getOption('customPricerApiToken', '', String, incomingOptions), customPricerUrl: getOption('customPricerUrl', '', String, incomingOptions), skipBPTFTradeofferURL: getOption('skipBPTFTradeofferURL', true, jsonParseBoolean, incomingOptions), skipUpdateProfileSettings: getOption('skipUpdateProfileSettings', true, jsonParseBoolean, incomingOptions), timezone: getOption('timezone', '', String, incomingOptions), customTimeFormat: getOption('customTimeFormat', '', String, incomingOptions), timeAdditionalNotes: getOption('timeAdditionalNotes', '', String, incomingOptions), debug: getOption('debug', true, jsonParseBoolean, incomingOptions), debugFile: getOption('debugFile', true, jsonParseBoolean, incomingOptions), enableSaveLogFile: getOption('enableSaveLogFile', true, jsonParseBoolean, incomingOptions), enableHttpApi: getOption('enableHttpApi', false, jsonParseBoolean, incomingOptions), httpApiPort: getOption('httpApiPort', 3001, jsonParseNumber, incomingOptions) }; if (!envOptions.steamAccountName) { throw new Error('STEAM_ACCOUNT_NAME must be set in the environment'); } removeCliOptions(incomingOptions); const jsonOptions = loadJsonOptions(getOptionsPath(envOptions.steamAccountName), incomingOptions); const errors = validator(jsonOptions, 'options'); if (errors !== null) { throw new Error(errors.join(', ')); } return deepMerge(jsonOptions, envOptions, incomingOptions); } export function getFilesPath(accountName: string): string { return path.resolve(__dirname, '..', '..', 'files', accountName); } export function getOptionsPath(accountName: string): string { return path.resolve(getFilesPath(accountName), 'options.json'); }