diff --git a/server/.eslintrc b/server/.eslintrc index 5e5410579..84ba24b17 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -31,7 +31,8 @@ "singleQuote": false, "trailingComma": "es5", "bracketSpacing": true, - "endOfLine": "auto" + "endOfLine": "auto", + "useTabs": true } ], "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/server/.git-blame-ignore-revs b/server/.git-blame-ignore-revs new file mode 100644 index 000000000..3aff33d94 --- /dev/null +++ b/server/.git-blame-ignore-revs @@ -0,0 +1 @@ +#188 - Spaces to tabs diff --git a/server/src/datasets/words.ts b/server/src/datasets/words.ts index f90619f9a..aec066e20 100644 --- a/server/src/datasets/words.ts +++ b/server/src/datasets/words.ts @@ -8,2887 +8,2887 @@ // I hope your editor supports rollup. export const adjectives = [ - "Abandoned", - "Able", - "Absolute", - "Adorable", - "Adventurous", - "Academic", - "Acceptable", - "Acclaimed", - "Accomplished", - "Accurate", - "Aching", - "Acidic", - "Acrobatic", - "Active", - "Actual", - "Adept", - "Admirable", - "Admired", - "Adolescent", - "Adorable", - "Adored", - "Advanced", - "Afraid", - "Affectionate", - "Aged", - "Aggravating", - "Aggressive", - "Agile", - "Agitated", - "Agonizing", - "Agreeable", - "Ajar", - "Alarmed", - "Alarming", - "Alert", - "Alienated", - "Alive", - "All", - "Altruistic", - "Amazing", - "Ambitious", - "Ample", - "Amused", - "Amusing", - "Anchored", - "Ancient", - "Angelic", - "Angry", - "Anguished", - "Animated", - "Annual", - "Another", - "Antique", - "Anxious", - "Any", - "Apprehensive", - "Appropriate", - "Apt", - "Arctic", - "Arid", - "Aromatic", - "Artistic", - "Ashamed", - "Assured", - "Astonishing", - "Athletic", - "Attached", - "Attentive", - "Attractive", - "Austere", - "Authentic", - "Authorized", - "Automatic", - "Avaricious", - "Average", - "Aware", - "Awesome", - "Awful", - "Awkward", - "Babyish", - "Bad", - "Back", - "Baggy", - "Bare", - "Barren", - "Basic", - "Beautiful", - "Belated", - "Beloved", - "Beneficial", - "Better", - "Best", - "Bewitched", - "Big", - "Big-hearted", - "Biodegradable", - "Bite-sized", - "Bitter", - "Black", - "Black-and-white", - "Bland", - "Blank", - "Blaring", - "Bleak", - "Blind", - "Blissful", - "Blond", - "Blue", - "Blushing", - "Bogus", - "Boiling", - "Bold", - "Bony", - "Boring", - "Bossy", - "Both", - "Bouncy", - "Bountiful", - "Bowed", - "Brave", - "Breakable", - "Brief", - "Bright", - "Brilliant", - "Brisk", - "Broken", - "Bronze", - "Brown", - "Bruised", - "Bubbly", - "Bulky", - "Bumpy", - "Buoyant", - "Burdensome", - "Burly", - "Bustling", - "Busy", - "Buttery", - "Buzzing", - "Calculating", - "Calm", - "Candid", - "Canine", - "Capital", - "Carefree", - "Careful", - "Careless", - "Caring", - "Cautious", - "Cavernous", - "Celebrated", - "Charming", - "Cheap", - "Cheerful", - "Cheery", - "Chief", - "Chilly", - "Chubby", - "Circular", - "Classic", - "Clean", - "Clear", - "Clear-cut", - "Clever", - "Close", - "Closed", - "Cloudy", - "Clueless", - "Clumsy", - "Cluttered", - "Coarse", - "Cold", - "Colorful", - "Colorless", - "Colossal", - "Comfortable", - "Common", - "Compassionate", - "Competent", - "Complete", - "Complex", - "Complicated", - "Composed", - "Concerned", - "Concrete", - "Confused", - "Conscious", - "Considerate", - "Constant", - "Content", - "Conventional", - "Cooked", - "Cool", - "Cooperative", - "Coordinated", - "Corny", - "Corrupt", - "Costly", - "Courageous", - "Courteous", - "Crafty", - "Crazy", - "Creamy", - "Creative", - "Creepy", - "Criminal", - "Crisp", - "Critical", - "Crooked", - "Crowded", - "Cruel", - "Crushing", - "Cuddly", - "Cultivated", - "Cultured", - "Cumbersome", - "Curly", - "Curvy", - "Cute", - "Cylindrical", - "Damaged", - "Damp", - "Dangerous", - "Dapper", - "Daring", - "Darling", - "Dark", - "Dazzling", - "Dead", - "Deadly", - "Deafening", - "Dear", - "Dearest", - "Decent", - "Decimal", - "Decisive", - "Deep", - "Defenseless", - "Defensive", - "Defiant", - "Deficient", - "Definite", - "Definitive", - "Delayed", - "Delectable", - "Delicious", - "Delightful", - "Delirious", - "Demanding", - "Dense", - "Dental", - "Dependable", - "Dependent", - "Descriptive", - "Deserted", - "Detailed", - "Determined", - "Devoted", - "Different", - "Difficult", - "Digital", - "Diligent", - "Dim", - "Dimpled", - "Dimwitted", - "Direct", - "Disastrous", - "Discrete", - "Disfigured", - "Disgusting", - "Disloyal", - "Dismal", - "Distant", - "Downright", - "Dreary", - "Dirty", - "Disguised", - "Dishonest", - "Dismal", - "Distant", - "Distinct", - "Distorted", - "Dizzy", - "Dopey", - "Doting", - "Double", - "Downright", - "Drab", - "Drafty", - "Dramatic", - "Dreary", - "Droopy", - "Dry", - "Dual", - "Dull", - "Dutiful", - "Each", - "Eager", - "Earnest", - "Early", - "Easy", - "Easy-going", - "Ecstatic", - "Edible", - "Educated", - "Elaborate", - "Elastic", - "Elated", - "Elderly", - "Electric", - "Elegant", - "Elementary", - "Elliptical", - "Embarrassed", - "Embellished", - "Eminent", - "Emotional", - "Empty", - "Enchanted", - "Enchanting", - "Energetic", - "Enlightened", - "Enormous", - "Enraged", - "Entire", - "Envious", - "Equal", - "Equatorial", - "Essential", - "Esteemed", - "Ethical", - "Euphoric", - "Even", - "Evergreen", - "Everlasting", - "Every", - "Evil", - "Exalted", - "Excellent", - "Exemplary", - "Exhausted", - "Excitable", - "Excited", - "Exciting", - "Exotic", - "Expensive", - "Experienced", - "Expert", - "Extraneous", - "Extroverted", - "Extra-large", - "Extra-small", - "Fabulous", - "Failing", - "Faint", - "Fair", - "Faithful", - "Fake", - "False", - "Familiar", - "Famous", - "Fancy", - "Fantastic", - "Far", - "Faraway", - "Far-flung", - "Far-off", - "Fast", - "Fat", - "Fatal", - "Fatherly", - "Favorable", - "Favorite", - "Fearful", - "Fearless", - "Feisty", - "Feline", - "Female", - "Feminine", - "Few", - "Fickle", - "Filthy", - "Fine", - "Finished", - "Firm", - "First", - "Firsthand", - "Fitting", - "Fixed", - "Flaky", - "Flamboyant", - "Flashy", - "Flat", - "Flawed", - "Flawless", - "Flickering", - "Flimsy", - "Flippant", - "Flowery", - "Fluffy", - "Fluid", - "Flustered", - "Focused", - "Fond", - "Foolhardy", - "Foolish", - "Forceful", - "Forked", - "Formal", - "Forsaken", - "Forthright", - "Fortunate", - "Fragrant", - "Frail", - "Frank", - "Frayed", - "Free", - "French", - "Fresh", - "Frequent", - "Friendly", - "Frightened", - "Frightening", - "Frigid", - "Frilly", - "Frizzy", - "Frivolous", - "Front", - "Frosty", - "Frozen", - "Frugal", - "Fruitful", - "Full", - "Fumbling", - "Functional", - "Funny", - "Fussy", - "Fuzzy", - "Gargantuan", - "Gaseous", - "General", - "Generous", - "Gentle", - "Genuine", - "Giant", - "Giddy", - "Gigantic", - "Gifted", - "Giving", - "Glamorous", - "Glaring", - "Glass", - "Gleaming", - "Gleeful", - "Glistening", - "Glittering", - "Gloomy", - "Glorious", - "Glossy", - "Glum", - "Golden", - "Good", - "Good-natured", - "Gorgeous", - "Graceful", - "Gracious", - "Grand", - "Grandiose", - "Granular", - "Grateful", - "Grave", - "Gray", - "Great", - "Greedy", - "Green", - "Gregarious", - "Grim", - "Grimy", - "Gripping", - "Grizzled", - "Gross", - "Grotesque", - "Grouchy", - "Grounded", - "Growing", - "Growling", - "Grown", - "Grubby", - "Gruesome", - "Grumpy", - "Guilty", - "Gullible", - "Gummy", - "Hairy", - "Half", - "Handmade", - "Handsome", - "Handy", - "Happy", - "Happy-go-lucky", - "Hard", - "Hard-to-find", - "Harmful", - "Harmless", - "Harmonious", - "Harsh", - "Hasty", - "Hateful", - "Haunting", - "Healthy", - "Heartfelt", - "Hearty", - "Heavenly", - "Heavy", - "Hefty", - "Helpful", - "Helpless", - "Hidden", - "Hideous", - "High", - "High-level", - "Hilarious", - "Hoarse", - "Hollow", - "Homely", - "Honest", - "Honorable", - "Honored", - "Hopeful", - "Horrible", - "Hospitable", - "Hot", - "Huge", - "Humble", - "Humiliating", - "Humming", - "Humongous", - "Hungry", - "Hurtful", - "Husky", - "Icky", - "Icy", - "Ideal", - "Idealistic", - "Identical", - "Idle", - "Idiotic", - "Idolized", - "Ignorant", - "Ill", - "Illegal", - "Ill-fated", - "Ill-informed", - "Illiterate", - "Illustrious", - "Imaginary", - "Imaginative", - "Immaculate", - "Immaterial", - "Immediate", - "Immense", - "Impassioned", - "Impeccable", - "Impartial", - "Imperfect", - "Imperturbable", - "Impish", - "Impolite", - "Important", - "Impossible", - "Impractical", - "Impressionable", - "Impressive", - "Improbable", - "Impure", - "Inborn", - "Incomparable", - "Incompatible", - "Incomplete", - "Inconsequential", - "Incredible", - "Indelible", - "Inexperienced", - "Indolent", - "Infamous", - "Infantile", - "Infatuated", - "Inferior", - "Infinite", - "Informal", - "Innocent", - "Insecure", - "Insidious", - "Insignificant", - "Insistent", - "Instructive", - "Insubstantial", - "Intelligent", - "Intent", - "Intentional", - "Interesting", - "Internal", - "International", - "Intrepid", - "Ironclad", - "Irresponsible", - "Irritating", - "Itchy", - "Jaded", - "Jagged", - "Jam-packed", - "Jaunty", - "Jealous", - "Jittery", - "Joint", - "Jolly", - "Jovial", - "Joyful", - "Joyous", - "Jubilant", - "Judicious", - "Juicy", - "Jumbo", - "Junior", - "Jumpy", - "Juvenile", - "Kaleidoscopic", - "Keen", - "Key", - "Kind", - "Kindhearted", - "Kindly", - "Klutzy", - "Knobby", - "Knotty", - "Knowledgeable", - "Knowing", - "Known", - "Kooky", - "Kosher", - "Lame", - "Lanky", - "Large", - "Last", - "Lasting", - "Late", - "Lavish", - "Lawful", - "Lazy", - "Leading", - "Lean", - "Leafy", - "Left", - "Legal", - "Legitimate", - "Light", - "Lighthearted", - "Likable", - "Likely", - "Limited", - "Limp", - "Limping", - "Linear", - "Lined", - "Liquid", - "Little", - "Live", - "Lively", - "Livid", - "Loathsome", - "Lone", - "Lonely", - "Long", - "Long-term", - "Loose", - "Lopsided", - "Lost", - "Loud", - "Lovable", - "Lovely", - "Loving", - "Low", - "Loyal", - "Lucky", - "Lumbering", - "Luminous", - "Lumpy", - "Lustrous", - "Luxurious", - "Mad", - "Made-up", - "Magnificent", - "Majestic", - "Major", - "Male", - "Mammoth", - "Married", - "Marvelous", - "Masculine", - "Massive", - "Mature", - "Meager", - "Mealy", - "Mean", - "Measly", - "Meaty", - "Medical", - "Mediocre", - "Medium", - "Meek", - "Mellow", - "Melodic", - "Memorable", - "Menacing", - "Merry", - "Messy", - "Metallic", - "Mild", - "Milky", - "Mindless", - "Miniature", - "Minor", - "Minty", - "Miserable", - "Miserly", - "Misguided", - "Misty", - "Mixed", - "Modern", - "Modest", - "Moist", - "Monstrous", - "Monthly", - "Monumental", - "Moral", - "Mortified", - "Motherly", - "Motionless", - "Mountainous", - "Muddy", - "Muffled", - "Multicolored", - "Mundane", - "Murky", - "Mushy", - "Musty", - "Muted", - "Mysterious", - "Naive", - "Narrow", - "Nasty", - "Natural", - "Naughty", - "Nautical", - "Near", - "Neat", - "Necessary", - "Needy", - "Negative", - "Neglected", - "Negligible", - "Neighboring", - "Nervous", - "New", - "Next", - "Nice", - "Nifty", - "Nimble", - "Nippy", - "Nocturnal", - "Noisy", - "Nonstop", - "Normal", - "Notable", - "Noted", - "Noteworthy", - "Novel", - "Noxious", - "Numb", - "Nutritious", - "Nutty", - "Obedient", - "Obese", - "Oblong", - "Oily", - "Oblong", - "Obvious", - "Occasional", - "Odd", - "Oddball", - "Offbeat", - "Offensive", - "Official", - "Old", - "Old-fashioned", - "Only", - "Open", - "Optimal", - "Optimistic", - "Opulent", - "Orange", - "Orderly", - "Organic", - "Ornate", - "Ornery", - "Ordinary", - "Original", - "Other", - "Our", - "Outlying", - "Outgoing", - "Outlandish", - "Outrageous", - "Outstanding", - "Oval", - "Overcooked", - "Overdue", - "Overjoyed", - "Overlooked", - "Palatable", - "Pale", - "Paltry", - "Parallel", - "Parched", - "Partial", - "Passionate", - "Past", - "Pastel", - "Peaceful", - "Peppery", - "Perfect", - "Perfumed", - "Periodic", - "Perky", - "Personal", - "Pertinent", - "Pesky", - "Pessimistic", - "Petty", - "Phony", - "Physical", - "Piercing", - "Pink", - "Pitiful", - "Plain", - "Plaintive", - "Plastic", - "Playful", - "Pleasant", - "Pleased", - "Pleasing", - "Plump", - "Plush", - "Polished", - "Polite", - "Political", - "Pointed", - "Pointless", - "Poised", - "Poor", - "Popular", - "Portly", - "Posh", - "Positive", - "Possible", - "Potable", - "Powerful", - "Powerless", - "Practical", - "Precious", - "Present", - "Prestigious", - "Pretty", - "Precious", - "Previous", - "Pricey", - "Prickly", - "Primary", - "Prime", - "Pristine", - "Private", - "Prize", - "Probable", - "Productive", - "Profitable", - "Profuse", - "Proper", - "Proud", - "Prudent", - "Punctual", - "Pungent", - "Puny", - "Pure", - "Purple", - "Pushy", - "Putrid", - "Puzzled", - "Puzzling", - "Quaint", - "Qualified", - "Quarrelsome", - "Quarterly", - "Queasy", - "Querulous", - "Questionable", - "Quick", - "Quick-witted", - "Quiet", - "Quintessential", - "Quirky", - "Quixotic", - "Quizzical", - "Radiant", - "Ragged", - "Rapid", - "Rare", - "Rash", - "Raw", - "Recent", - "Reckless", - "Rectangular", - "Ready", - "Real", - "Realistic", - "Reasonable", - "Red", - "Reflecting", - "Regal", - "Regular", - "Reliable", - "Relieved", - "Remarkable", - "Remorseful", - "Remote", - "Repentant", - "Required", - "Respectful", - "Responsible", - "Repulsive", - "Revolving", - "Rewarding", - "Rich", - "Rigid", - "Right", - "Ringed", - "Ripe", - "Roasted", - "Robust", - "Rosy", - "Rotating", - "Rotten", - "Rough", - "Round", - "Rowdy", - "Royal", - "Rubbery", - "Rundown", - "Ruddy", - "Rude", - "Runny", - "Rural", - "Rusty", - "Sad", - "Safe", - "Salty", - "Same", - "Sandy", - "Sane", - "Sarcastic", - "Sardonic", - "Satisfied", - "Scaly", - "Scarce", - "Scared", - "Scary", - "Scented", - "Scholarly", - "Scientific", - "Scornful", - "Scratchy", - "Scrawny", - "Second", - "Secondary", - "Second-hand", - "Secret", - "Self-assured", - "Self-reliant", - "Selfish", - "Sentimental", - "Separate", - "Serene", - "Serious", - "Serpentine", - "Several", - "Severe", - "Shabby", - "Shadowy", - "Shady", - "Shallow", - "Shameful", - "Shameless", - "Sharp", - "Shimmering", - "Shiny", - "Shocked", - "Shocking", - "Shoddy", - "Short", - "Short-term", - "Showy", - "Shrill", - "Shy", - "Sick", - "Silent", - "Silky", - "Silly", - "Silver", - "Similar", - "Simple", - "Simplistic", - "Sinful", - "Single", - "Sizzling", - "Skeletal", - "Skinny", - "Sleepy", - "Slight", - "Slim", - "Slimy", - "Slippery", - "Slow", - "Slushy", - "Small", - "Smart", - "Smoggy", - "Smooth", - "Smug", - "Snappy", - "Snarling", - "Sneaky", - "Sniveling", - "Snoopy", - "Sociable", - "Soft", - "Soggy", - "Solid", - "Somber", - "Some", - "Spherical", - "Sophisticated", - "Sore", - "Sorrowful", - "Soulful", - "Soupy", - "Sour", - "Spanish", - "Sparkling", - "Sparse", - "Specific", - "Spectacular", - "Speedy", - "Spicy", - "Spiffy", - "Spirited", - "Spiteful", - "Splendid", - "Spotless", - "Spotted", - "Spry", - "Square", - "Squeaky", - "Squiggly", - "Stable", - "Staid", - "Stained", - "Stale", - "Standard", - "Starchy", - "Stark", - "Starry", - "Steep", - "Sticky", - "Stiff", - "Stimulating", - "Stingy", - "Stormy", - "Straight", - "Strange", - "Steel", - "Strict", - "Strident", - "Striking", - "Striped", - "Strong", - "Studious", - "Stunning", - "Stupendous", - "Stupid", - "Sturdy", - "Stylish", - "Subdued", - "Submissive", - "Substantial", - "Subtle", - "Suburban", - "Sudden", - "Sugary", - "Sunny", - "Super", - "Superb", - "Superficial", - "Superior", - "Supportive", - "Sure-footed", - "Surprised", - "Suspicious", - "Svelte", - "Sweaty", - "Sweet", - "Sweltering", - "Swift", - "Sympathetic", - "Tall", - "Talkative", - "Tame", - "Tan", - "Tangible", - "Tart", - "Tasty", - "Tattered", - "Taut", - "Tedious", - "Teeming", - "Tempting", - "Tender", - "Tense", - "Tepid", - "Terrible", - "Terrific", - "Testy", - "Thankful", - "That", - "These", - "Thick", - "Thin", - "Third", - "Thirsty", - "This", - "Thorough", - "Thorny", - "Those", - "Thoughtful", - "Threadbare", - "Thrifty", - "Thunderous", - "Tidy", - "Tight", - "Timely", - "Tinted", - "Tiny", - "Tired", - "Torn", - "Total", - "Tough", - "Traumatic", - "Treasured", - "Tremendous", - "Tragic", - "Trained", - "Tremendous", - "Triangular", - "Tricky", - "Trifling", - "Trim", - "Trivial", - "Troubled", - "True", - "Trusting", - "Trustworthy", - "Trusty", - "Truthful", - "Tubby", - "Turbulent", - "Twin", - "Ugly", - "Ultimate", - "Unacceptable", - "Unaware", - "Uncomfortable", - "Uncommon", - "Unconscious", - "Understated", - "Unequaled", - "Uneven", - "Unfinished", - "Unfit", - "Unfolded", - "Unfortunate", - "Unhappy", - "Unhealthy", - "Uniform", - "Unimportant", - "Unique", - "United", - "Unkempt", - "Unknown", - "Unlawful", - "Unlined", - "Unlucky", - "Unnatural", - "Unpleasant", - "Unrealistic", - "Unripe", - "Unruly", - "Unselfish", - "Unsightly", - "Unsteady", - "Unsung", - "Untidy", - "Untimely", - "Untried", - "Untrue", - "Unused", - "Unusual", - "Unwelcome", - "Unwieldy", - "Unwilling", - "Unwitting", - "Unwritten", - "Upbeat", - "Upright", - "Upset", - "Urban", - "Usable", - "Used", - "Useful", - "Useless", - "Utilized", - "Utter", - "Vacant", - "Vague", - "Vain", - "Valid", - "Valuable", - "Vapid", - "Variable", - "Vast", - "Velvety", - "Venerated", - "Vengeful", - "Verifiable", - "Vibrant", - "Vicious", - "Victorious", - "Vigilant", - "Vigorous", - "Villainous", - "Violet", - "Violent", - "Virtual", - "Virtuous", - "Visible", - "Vital", - "Vivacious", - "Vivid", - "Voluminous", - "Wan", - "Warlike", - "Warm", - "Warmhearted", - "Warped", - "Wary", - "Wasteful", - "Watchful", - "Waterlogged", - "Watery", - "Wavy", - "Wealthy", - "Weak", - "Weary", - "Webbed", - "Wee", - "Weekly", - "Weepy", - "Weighty", - "Weird", - "Welcome", - "Well-documented", - "Well-groomed", - "Well-informed", - "Well-lit", - "Well-made", - "Well-off", - "Well-to-do", - "Well-worn", - "Wet", - "Which", - "Whimsical", - "Whirlwind", - "Whispered", - "White", - "Whole", - "Whopping", - "Wicked", - "Wide", - "Wide-eyed", - "Wiggly", - "Wild", - "Willing", - "Wilted", - "Winding", - "Windy", - "Winged", - "Wiry", - "Wise", - "Witty", - "Wobbly", - "Woeful", - "Wonderful", - "Wooden", - "Woozy", - "Wordy", - "Worldly", - "Worn", - "Worried", - "Worrisome", - "Worse", - "Worst", - "Worthless", - "Worthwhile", - "Worthy", - "Wrathful", - "Wretched", - "Writhing", - "Wrong", - "Wry", - "Yawning", - "Yearly", - "Yellow", - "Yellowish", - "Young", - "Youthful", - "Yummy", - "Zany", - "Zealous", - "Zesty", - "Zigzag", + "Abandoned", + "Able", + "Absolute", + "Adorable", + "Adventurous", + "Academic", + "Acceptable", + "Acclaimed", + "Accomplished", + "Accurate", + "Aching", + "Acidic", + "Acrobatic", + "Active", + "Actual", + "Adept", + "Admirable", + "Admired", + "Adolescent", + "Adorable", + "Adored", + "Advanced", + "Afraid", + "Affectionate", + "Aged", + "Aggravating", + "Aggressive", + "Agile", + "Agitated", + "Agonizing", + "Agreeable", + "Ajar", + "Alarmed", + "Alarming", + "Alert", + "Alienated", + "Alive", + "All", + "Altruistic", + "Amazing", + "Ambitious", + "Ample", + "Amused", + "Amusing", + "Anchored", + "Ancient", + "Angelic", + "Angry", + "Anguished", + "Animated", + "Annual", + "Another", + "Antique", + "Anxious", + "Any", + "Apprehensive", + "Appropriate", + "Apt", + "Arctic", + "Arid", + "Aromatic", + "Artistic", + "Ashamed", + "Assured", + "Astonishing", + "Athletic", + "Attached", + "Attentive", + "Attractive", + "Austere", + "Authentic", + "Authorized", + "Automatic", + "Avaricious", + "Average", + "Aware", + "Awesome", + "Awful", + "Awkward", + "Babyish", + "Bad", + "Back", + "Baggy", + "Bare", + "Barren", + "Basic", + "Beautiful", + "Belated", + "Beloved", + "Beneficial", + "Better", + "Best", + "Bewitched", + "Big", + "Big-hearted", + "Biodegradable", + "Bite-sized", + "Bitter", + "Black", + "Black-and-white", + "Bland", + "Blank", + "Blaring", + "Bleak", + "Blind", + "Blissful", + "Blond", + "Blue", + "Blushing", + "Bogus", + "Boiling", + "Bold", + "Bony", + "Boring", + "Bossy", + "Both", + "Bouncy", + "Bountiful", + "Bowed", + "Brave", + "Breakable", + "Brief", + "Bright", + "Brilliant", + "Brisk", + "Broken", + "Bronze", + "Brown", + "Bruised", + "Bubbly", + "Bulky", + "Bumpy", + "Buoyant", + "Burdensome", + "Burly", + "Bustling", + "Busy", + "Buttery", + "Buzzing", + "Calculating", + "Calm", + "Candid", + "Canine", + "Capital", + "Carefree", + "Careful", + "Careless", + "Caring", + "Cautious", + "Cavernous", + "Celebrated", + "Charming", + "Cheap", + "Cheerful", + "Cheery", + "Chief", + "Chilly", + "Chubby", + "Circular", + "Classic", + "Clean", + "Clear", + "Clear-cut", + "Clever", + "Close", + "Closed", + "Cloudy", + "Clueless", + "Clumsy", + "Cluttered", + "Coarse", + "Cold", + "Colorful", + "Colorless", + "Colossal", + "Comfortable", + "Common", + "Compassionate", + "Competent", + "Complete", + "Complex", + "Complicated", + "Composed", + "Concerned", + "Concrete", + "Confused", + "Conscious", + "Considerate", + "Constant", + "Content", + "Conventional", + "Cooked", + "Cool", + "Cooperative", + "Coordinated", + "Corny", + "Corrupt", + "Costly", + "Courageous", + "Courteous", + "Crafty", + "Crazy", + "Creamy", + "Creative", + "Creepy", + "Criminal", + "Crisp", + "Critical", + "Crooked", + "Crowded", + "Cruel", + "Crushing", + "Cuddly", + "Cultivated", + "Cultured", + "Cumbersome", + "Curly", + "Curvy", + "Cute", + "Cylindrical", + "Damaged", + "Damp", + "Dangerous", + "Dapper", + "Daring", + "Darling", + "Dark", + "Dazzling", + "Dead", + "Deadly", + "Deafening", + "Dear", + "Dearest", + "Decent", + "Decimal", + "Decisive", + "Deep", + "Defenseless", + "Defensive", + "Defiant", + "Deficient", + "Definite", + "Definitive", + "Delayed", + "Delectable", + "Delicious", + "Delightful", + "Delirious", + "Demanding", + "Dense", + "Dental", + "Dependable", + "Dependent", + "Descriptive", + "Deserted", + "Detailed", + "Determined", + "Devoted", + "Different", + "Difficult", + "Digital", + "Diligent", + "Dim", + "Dimpled", + "Dimwitted", + "Direct", + "Disastrous", + "Discrete", + "Disfigured", + "Disgusting", + "Disloyal", + "Dismal", + "Distant", + "Downright", + "Dreary", + "Dirty", + "Disguised", + "Dishonest", + "Dismal", + "Distant", + "Distinct", + "Distorted", + "Dizzy", + "Dopey", + "Doting", + "Double", + "Downright", + "Drab", + "Drafty", + "Dramatic", + "Dreary", + "Droopy", + "Dry", + "Dual", + "Dull", + "Dutiful", + "Each", + "Eager", + "Earnest", + "Early", + "Easy", + "Easy-going", + "Ecstatic", + "Edible", + "Educated", + "Elaborate", + "Elastic", + "Elated", + "Elderly", + "Electric", + "Elegant", + "Elementary", + "Elliptical", + "Embarrassed", + "Embellished", + "Eminent", + "Emotional", + "Empty", + "Enchanted", + "Enchanting", + "Energetic", + "Enlightened", + "Enormous", + "Enraged", + "Entire", + "Envious", + "Equal", + "Equatorial", + "Essential", + "Esteemed", + "Ethical", + "Euphoric", + "Even", + "Evergreen", + "Everlasting", + "Every", + "Evil", + "Exalted", + "Excellent", + "Exemplary", + "Exhausted", + "Excitable", + "Excited", + "Exciting", + "Exotic", + "Expensive", + "Experienced", + "Expert", + "Extraneous", + "Extroverted", + "Extra-large", + "Extra-small", + "Fabulous", + "Failing", + "Faint", + "Fair", + "Faithful", + "Fake", + "False", + "Familiar", + "Famous", + "Fancy", + "Fantastic", + "Far", + "Faraway", + "Far-flung", + "Far-off", + "Fast", + "Fat", + "Fatal", + "Fatherly", + "Favorable", + "Favorite", + "Fearful", + "Fearless", + "Feisty", + "Feline", + "Female", + "Feminine", + "Few", + "Fickle", + "Filthy", + "Fine", + "Finished", + "Firm", + "First", + "Firsthand", + "Fitting", + "Fixed", + "Flaky", + "Flamboyant", + "Flashy", + "Flat", + "Flawed", + "Flawless", + "Flickering", + "Flimsy", + "Flippant", + "Flowery", + "Fluffy", + "Fluid", + "Flustered", + "Focused", + "Fond", + "Foolhardy", + "Foolish", + "Forceful", + "Forked", + "Formal", + "Forsaken", + "Forthright", + "Fortunate", + "Fragrant", + "Frail", + "Frank", + "Frayed", + "Free", + "French", + "Fresh", + "Frequent", + "Friendly", + "Frightened", + "Frightening", + "Frigid", + "Frilly", + "Frizzy", + "Frivolous", + "Front", + "Frosty", + "Frozen", + "Frugal", + "Fruitful", + "Full", + "Fumbling", + "Functional", + "Funny", + "Fussy", + "Fuzzy", + "Gargantuan", + "Gaseous", + "General", + "Generous", + "Gentle", + "Genuine", + "Giant", + "Giddy", + "Gigantic", + "Gifted", + "Giving", + "Glamorous", + "Glaring", + "Glass", + "Gleaming", + "Gleeful", + "Glistening", + "Glittering", + "Gloomy", + "Glorious", + "Glossy", + "Glum", + "Golden", + "Good", + "Good-natured", + "Gorgeous", + "Graceful", + "Gracious", + "Grand", + "Grandiose", + "Granular", + "Grateful", + "Grave", + "Gray", + "Great", + "Greedy", + "Green", + "Gregarious", + "Grim", + "Grimy", + "Gripping", + "Grizzled", + "Gross", + "Grotesque", + "Grouchy", + "Grounded", + "Growing", + "Growling", + "Grown", + "Grubby", + "Gruesome", + "Grumpy", + "Guilty", + "Gullible", + "Gummy", + "Hairy", + "Half", + "Handmade", + "Handsome", + "Handy", + "Happy", + "Happy-go-lucky", + "Hard", + "Hard-to-find", + "Harmful", + "Harmless", + "Harmonious", + "Harsh", + "Hasty", + "Hateful", + "Haunting", + "Healthy", + "Heartfelt", + "Hearty", + "Heavenly", + "Heavy", + "Hefty", + "Helpful", + "Helpless", + "Hidden", + "Hideous", + "High", + "High-level", + "Hilarious", + "Hoarse", + "Hollow", + "Homely", + "Honest", + "Honorable", + "Honored", + "Hopeful", + "Horrible", + "Hospitable", + "Hot", + "Huge", + "Humble", + "Humiliating", + "Humming", + "Humongous", + "Hungry", + "Hurtful", + "Husky", + "Icky", + "Icy", + "Ideal", + "Idealistic", + "Identical", + "Idle", + "Idiotic", + "Idolized", + "Ignorant", + "Ill", + "Illegal", + "Ill-fated", + "Ill-informed", + "Illiterate", + "Illustrious", + "Imaginary", + "Imaginative", + "Immaculate", + "Immaterial", + "Immediate", + "Immense", + "Impassioned", + "Impeccable", + "Impartial", + "Imperfect", + "Imperturbable", + "Impish", + "Impolite", + "Important", + "Impossible", + "Impractical", + "Impressionable", + "Impressive", + "Improbable", + "Impure", + "Inborn", + "Incomparable", + "Incompatible", + "Incomplete", + "Inconsequential", + "Incredible", + "Indelible", + "Inexperienced", + "Indolent", + "Infamous", + "Infantile", + "Infatuated", + "Inferior", + "Infinite", + "Informal", + "Innocent", + "Insecure", + "Insidious", + "Insignificant", + "Insistent", + "Instructive", + "Insubstantial", + "Intelligent", + "Intent", + "Intentional", + "Interesting", + "Internal", + "International", + "Intrepid", + "Ironclad", + "Irresponsible", + "Irritating", + "Itchy", + "Jaded", + "Jagged", + "Jam-packed", + "Jaunty", + "Jealous", + "Jittery", + "Joint", + "Jolly", + "Jovial", + "Joyful", + "Joyous", + "Jubilant", + "Judicious", + "Juicy", + "Jumbo", + "Junior", + "Jumpy", + "Juvenile", + "Kaleidoscopic", + "Keen", + "Key", + "Kind", + "Kindhearted", + "Kindly", + "Klutzy", + "Knobby", + "Knotty", + "Knowledgeable", + "Knowing", + "Known", + "Kooky", + "Kosher", + "Lame", + "Lanky", + "Large", + "Last", + "Lasting", + "Late", + "Lavish", + "Lawful", + "Lazy", + "Leading", + "Lean", + "Leafy", + "Left", + "Legal", + "Legitimate", + "Light", + "Lighthearted", + "Likable", + "Likely", + "Limited", + "Limp", + "Limping", + "Linear", + "Lined", + "Liquid", + "Little", + "Live", + "Lively", + "Livid", + "Loathsome", + "Lone", + "Lonely", + "Long", + "Long-term", + "Loose", + "Lopsided", + "Lost", + "Loud", + "Lovable", + "Lovely", + "Loving", + "Low", + "Loyal", + "Lucky", + "Lumbering", + "Luminous", + "Lumpy", + "Lustrous", + "Luxurious", + "Mad", + "Made-up", + "Magnificent", + "Majestic", + "Major", + "Male", + "Mammoth", + "Married", + "Marvelous", + "Masculine", + "Massive", + "Mature", + "Meager", + "Mealy", + "Mean", + "Measly", + "Meaty", + "Medical", + "Mediocre", + "Medium", + "Meek", + "Mellow", + "Melodic", + "Memorable", + "Menacing", + "Merry", + "Messy", + "Metallic", + "Mild", + "Milky", + "Mindless", + "Miniature", + "Minor", + "Minty", + "Miserable", + "Miserly", + "Misguided", + "Misty", + "Mixed", + "Modern", + "Modest", + "Moist", + "Monstrous", + "Monthly", + "Monumental", + "Moral", + "Mortified", + "Motherly", + "Motionless", + "Mountainous", + "Muddy", + "Muffled", + "Multicolored", + "Mundane", + "Murky", + "Mushy", + "Musty", + "Muted", + "Mysterious", + "Naive", + "Narrow", + "Nasty", + "Natural", + "Naughty", + "Nautical", + "Near", + "Neat", + "Necessary", + "Needy", + "Negative", + "Neglected", + "Negligible", + "Neighboring", + "Nervous", + "New", + "Next", + "Nice", + "Nifty", + "Nimble", + "Nippy", + "Nocturnal", + "Noisy", + "Nonstop", + "Normal", + "Notable", + "Noted", + "Noteworthy", + "Novel", + "Noxious", + "Numb", + "Nutritious", + "Nutty", + "Obedient", + "Obese", + "Oblong", + "Oily", + "Oblong", + "Obvious", + "Occasional", + "Odd", + "Oddball", + "Offbeat", + "Offensive", + "Official", + "Old", + "Old-fashioned", + "Only", + "Open", + "Optimal", + "Optimistic", + "Opulent", + "Orange", + "Orderly", + "Organic", + "Ornate", + "Ornery", + "Ordinary", + "Original", + "Other", + "Our", + "Outlying", + "Outgoing", + "Outlandish", + "Outrageous", + "Outstanding", + "Oval", + "Overcooked", + "Overdue", + "Overjoyed", + "Overlooked", + "Palatable", + "Pale", + "Paltry", + "Parallel", + "Parched", + "Partial", + "Passionate", + "Past", + "Pastel", + "Peaceful", + "Peppery", + "Perfect", + "Perfumed", + "Periodic", + "Perky", + "Personal", + "Pertinent", + "Pesky", + "Pessimistic", + "Petty", + "Phony", + "Physical", + "Piercing", + "Pink", + "Pitiful", + "Plain", + "Plaintive", + "Plastic", + "Playful", + "Pleasant", + "Pleased", + "Pleasing", + "Plump", + "Plush", + "Polished", + "Polite", + "Political", + "Pointed", + "Pointless", + "Poised", + "Poor", + "Popular", + "Portly", + "Posh", + "Positive", + "Possible", + "Potable", + "Powerful", + "Powerless", + "Practical", + "Precious", + "Present", + "Prestigious", + "Pretty", + "Precious", + "Previous", + "Pricey", + "Prickly", + "Primary", + "Prime", + "Pristine", + "Private", + "Prize", + "Probable", + "Productive", + "Profitable", + "Profuse", + "Proper", + "Proud", + "Prudent", + "Punctual", + "Pungent", + "Puny", + "Pure", + "Purple", + "Pushy", + "Putrid", + "Puzzled", + "Puzzling", + "Quaint", + "Qualified", + "Quarrelsome", + "Quarterly", + "Queasy", + "Querulous", + "Questionable", + "Quick", + "Quick-witted", + "Quiet", + "Quintessential", + "Quirky", + "Quixotic", + "Quizzical", + "Radiant", + "Ragged", + "Rapid", + "Rare", + "Rash", + "Raw", + "Recent", + "Reckless", + "Rectangular", + "Ready", + "Real", + "Realistic", + "Reasonable", + "Red", + "Reflecting", + "Regal", + "Regular", + "Reliable", + "Relieved", + "Remarkable", + "Remorseful", + "Remote", + "Repentant", + "Required", + "Respectful", + "Responsible", + "Repulsive", + "Revolving", + "Rewarding", + "Rich", + "Rigid", + "Right", + "Ringed", + "Ripe", + "Roasted", + "Robust", + "Rosy", + "Rotating", + "Rotten", + "Rough", + "Round", + "Rowdy", + "Royal", + "Rubbery", + "Rundown", + "Ruddy", + "Rude", + "Runny", + "Rural", + "Rusty", + "Sad", + "Safe", + "Salty", + "Same", + "Sandy", + "Sane", + "Sarcastic", + "Sardonic", + "Satisfied", + "Scaly", + "Scarce", + "Scared", + "Scary", + "Scented", + "Scholarly", + "Scientific", + "Scornful", + "Scratchy", + "Scrawny", + "Second", + "Secondary", + "Second-hand", + "Secret", + "Self-assured", + "Self-reliant", + "Selfish", + "Sentimental", + "Separate", + "Serene", + "Serious", + "Serpentine", + "Several", + "Severe", + "Shabby", + "Shadowy", + "Shady", + "Shallow", + "Shameful", + "Shameless", + "Sharp", + "Shimmering", + "Shiny", + "Shocked", + "Shocking", + "Shoddy", + "Short", + "Short-term", + "Showy", + "Shrill", + "Shy", + "Sick", + "Silent", + "Silky", + "Silly", + "Silver", + "Similar", + "Simple", + "Simplistic", + "Sinful", + "Single", + "Sizzling", + "Skeletal", + "Skinny", + "Sleepy", + "Slight", + "Slim", + "Slimy", + "Slippery", + "Slow", + "Slushy", + "Small", + "Smart", + "Smoggy", + "Smooth", + "Smug", + "Snappy", + "Snarling", + "Sneaky", + "Sniveling", + "Snoopy", + "Sociable", + "Soft", + "Soggy", + "Solid", + "Somber", + "Some", + "Spherical", + "Sophisticated", + "Sore", + "Sorrowful", + "Soulful", + "Soupy", + "Sour", + "Spanish", + "Sparkling", + "Sparse", + "Specific", + "Spectacular", + "Speedy", + "Spicy", + "Spiffy", + "Spirited", + "Spiteful", + "Splendid", + "Spotless", + "Spotted", + "Spry", + "Square", + "Squeaky", + "Squiggly", + "Stable", + "Staid", + "Stained", + "Stale", + "Standard", + "Starchy", + "Stark", + "Starry", + "Steep", + "Sticky", + "Stiff", + "Stimulating", + "Stingy", + "Stormy", + "Straight", + "Strange", + "Steel", + "Strict", + "Strident", + "Striking", + "Striped", + "Strong", + "Studious", + "Stunning", + "Stupendous", + "Stupid", + "Sturdy", + "Stylish", + "Subdued", + "Submissive", + "Substantial", + "Subtle", + "Suburban", + "Sudden", + "Sugary", + "Sunny", + "Super", + "Superb", + "Superficial", + "Superior", + "Supportive", + "Sure-footed", + "Surprised", + "Suspicious", + "Svelte", + "Sweaty", + "Sweet", + "Sweltering", + "Swift", + "Sympathetic", + "Tall", + "Talkative", + "Tame", + "Tan", + "Tangible", + "Tart", + "Tasty", + "Tattered", + "Taut", + "Tedious", + "Teeming", + "Tempting", + "Tender", + "Tense", + "Tepid", + "Terrible", + "Terrific", + "Testy", + "Thankful", + "That", + "These", + "Thick", + "Thin", + "Third", + "Thirsty", + "This", + "Thorough", + "Thorny", + "Those", + "Thoughtful", + "Threadbare", + "Thrifty", + "Thunderous", + "Tidy", + "Tight", + "Timely", + "Tinted", + "Tiny", + "Tired", + "Torn", + "Total", + "Tough", + "Traumatic", + "Treasured", + "Tremendous", + "Tragic", + "Trained", + "Tremendous", + "Triangular", + "Tricky", + "Trifling", + "Trim", + "Trivial", + "Troubled", + "True", + "Trusting", + "Trustworthy", + "Trusty", + "Truthful", + "Tubby", + "Turbulent", + "Twin", + "Ugly", + "Ultimate", + "Unacceptable", + "Unaware", + "Uncomfortable", + "Uncommon", + "Unconscious", + "Understated", + "Unequaled", + "Uneven", + "Unfinished", + "Unfit", + "Unfolded", + "Unfortunate", + "Unhappy", + "Unhealthy", + "Uniform", + "Unimportant", + "Unique", + "United", + "Unkempt", + "Unknown", + "Unlawful", + "Unlined", + "Unlucky", + "Unnatural", + "Unpleasant", + "Unrealistic", + "Unripe", + "Unruly", + "Unselfish", + "Unsightly", + "Unsteady", + "Unsung", + "Untidy", + "Untimely", + "Untried", + "Untrue", + "Unused", + "Unusual", + "Unwelcome", + "Unwieldy", + "Unwilling", + "Unwitting", + "Unwritten", + "Upbeat", + "Upright", + "Upset", + "Urban", + "Usable", + "Used", + "Useful", + "Useless", + "Utilized", + "Utter", + "Vacant", + "Vague", + "Vain", + "Valid", + "Valuable", + "Vapid", + "Variable", + "Vast", + "Velvety", + "Venerated", + "Vengeful", + "Verifiable", + "Vibrant", + "Vicious", + "Victorious", + "Vigilant", + "Vigorous", + "Villainous", + "Violet", + "Violent", + "Virtual", + "Virtuous", + "Visible", + "Vital", + "Vivacious", + "Vivid", + "Voluminous", + "Wan", + "Warlike", + "Warm", + "Warmhearted", + "Warped", + "Wary", + "Wasteful", + "Watchful", + "Waterlogged", + "Watery", + "Wavy", + "Wealthy", + "Weak", + "Weary", + "Webbed", + "Wee", + "Weekly", + "Weepy", + "Weighty", + "Weird", + "Welcome", + "Well-documented", + "Well-groomed", + "Well-informed", + "Well-lit", + "Well-made", + "Well-off", + "Well-to-do", + "Well-worn", + "Wet", + "Which", + "Whimsical", + "Whirlwind", + "Whispered", + "White", + "Whole", + "Whopping", + "Wicked", + "Wide", + "Wide-eyed", + "Wiggly", + "Wild", + "Willing", + "Wilted", + "Winding", + "Windy", + "Winged", + "Wiry", + "Wise", + "Witty", + "Wobbly", + "Woeful", + "Wonderful", + "Wooden", + "Woozy", + "Wordy", + "Worldly", + "Worn", + "Worried", + "Worrisome", + "Worse", + "Worst", + "Worthless", + "Worthwhile", + "Worthy", + "Wrathful", + "Wretched", + "Writhing", + "Wrong", + "Wry", + "Yawning", + "Yearly", + "Yellow", + "Yellowish", + "Young", + "Youthful", + "Yummy", + "Zany", + "Zealous", + "Zesty", + "Zigzag", ]; export const nouns = [ - "People", - "History", - "Way", - "Art", - "World", - "Information", - "Map", - "Two", - "Family", - "Government", - "Health", - "System", - "Computer", - "Meat", - "Year", - "Thanks", - "Music", - "Person", - "Reading", - "Method", - "Data", - "Food", - "Understanding", - "Theory", - "Law", - "Bird", - "Literature", - "Problem", - "Software", - "Control", - "Knowledge", - "Power", - "Ability", - "Economics", - "Love", - "Internet", - "Television", - "Science", - "Library", - "Nature", - "Fact", - "Product", - "Idea", - "Temperature", - "Investment", - "Area", - "Society", - "Activity", - "Story", - "Industry", - "Media", - "Thing", - "Oven", - "Community", - "Definition", - "Safety", - "Quality", - "Development", - "Language", - "Management", - "Player", - "Variety", - "Video", - "Week", - "Security", - "Country", - "Exam", - "Movie", - "Organization", - "Equipment", - "Physics", - "Analysis", - "Policy", - "Series", - "Thought", - "Basis", - "Boyfriend", - "Direction", - "Strategy", - "Technology", - "Army", - "Camera", - "Freedom", - "Paper", - "Environment", - "Child", - "Instance", - "Month", - "Truth", - "Marketing", - "University", - "Writing", - "Article", - "Department", - "Difference", - "Goal", - "News", - "Audience", - "Fishing", - "Growth", - "Income", - "Marriage", - "User", - "Combination", - "Failure", - "Meaning", - "Medicine", - "Philosophy", - "Teacher", - "Communication", - "Night", - "Chemistry", - "Disease", - "Disk", - "Energy", - "Nation", - "Road", - "Role", - "Soup", - "Advertising", - "Location", - "Success", - "Addition", - "Apartment", - "Education", - "Math", - "Moment", - "Painting", - "Politics", - "Attention", - "Decision", - "Event", - "Property", - "Shopping", - "Student", - "Wood", - "Competition", - "Distribution", - "Entertainment", - "Office", - "Population", - "President", - "Unit", - "Category", - "Cigarette", - "Context", - "Introduction", - "Opportunity", - "Performance", - "Driver", - "Flight", - "Length", - "Magazine", - "Newspaper", - "Relationship", - "Teaching", - "Cell", - "Dealer", - "Finding", - "Lake", - "Member", - "Message", - "Phone", - "Scene", - "Appearance", - "Association", - "Concept", - "Customer", - "Death", - "Discussion", - "Housing", - "Inflation", - "Insurance", - "Mood", - "Woman", - "Advice", - "Blood", - "Effort", - "Expression", - "Importance", - "Opinion", - "Payment", - "Reality", - "Responsibility", - "Situation", - "Skill", - "Statement", - "Wealth", - "Application", - "City", - "County", - "Depth", - "Estate", - "Foundation", - "Grandmother", - "Heart", - "Perspective", - "Photo", - "Recipe", - "Studio", - "Topic", - "Collection", - "Depression", - "Imagination", - "Passion", - "Percentage", - "Resource", - "Setting", - "Ad", - "Agency", - "College", - "Connection", - "Criticism", - "Debt", - "Description", - "Memory", - "Patience", - "Secretary", - "Solution", - "Administration", - "Aspect", - "Attitude", - "Director", - "Personality", - "Psychology", - "Recommendation", - "Response", - "Selection", - "Storage", - "Version", - "Alcohol", - "Argument", - "Complaint", - "Contract", - "Emphasis", - "Highway", - "Loss", - "Membership", - "Possession", - "Preparation", - "Steak", - "Union", - "Agreement", - "Cancer", - "Currency", - "Employment", - "Engineering", - "Entry", - "Interaction", - "Mixture", - "Preference", - "Region", - "Republic", - "Tradition", - "Virus", - "Actor", - "Classroom", - "Delivery", - "Device", - "Difficulty", - "Drama", - "Election", - "Engine", - "Football", - "Guidance", - "Hotel", - "Owner", - "Priority", - "Protection", - "Suggestion", - "Tension", - "Variation", - "Anxiety", - "Atmosphere", - "Awareness", - "Bath", - "Bread", - "Candidate", - "Climate", - "Comparison", - "Confusion", - "Construction", - "Elevator", - "Emotion", - "Employee", - "Employer", - "Guest", - "Height", - "Leadership", - "Mall", - "Manager", - "Operation", - "Recording", - "Sample", - "Transportation", - "Charity", - "Cousin", - "Disaster", - "Editor", - "Efficiency", - "Excitement", - "Extent", - "Feedback", - "Guitar", - "Homework", - "Leader", - "Mom", - "Outcome", - "Permission", - "Presentation", - "Promotion", - "Reflection", - "Refrigerator", - "Resolution", - "Revenue", - "Session", - "Singer", - "Tennis", - "Basket", - "Bonus", - "Cabinet", - "Childhood", - "Church", - "Clothes", - "Coffee", - "Dinner", - "Drawing", - "Hair", - "Hearing", - "Initiative", - "Judgment", - "Lab", - "Measurement", - "Mode", - "Mud", - "Orange", - "Poetry", - "Police", - "Possibility", - "Procedure", - "Queen", - "Ratio", - "Relation", - "Restaurant", - "Satisfaction", - "Sector", - "Signature", - "Significance", - "Song", - "Tooth", - "Town", - "Vehicle", - "Volume", - "Wife", - "Accident", - "Airport", - "Appointment", - "Arrival", - "Assumption", - "Baseball", - "Chapter", - "Committee", - "Conversation", - "Database", - "Enthusiasm", - "Error", - "Explanation", - "Farmer", - "Gate", - "Girl", - "Hall", - "Historian", - "Hospital", - "Injury", - "Instruction", - "Maintenance", - "Manufacturer", - "Meal", - "Perception", - "Pie", - "Poem", - "Presence", - "Proposal", - "Reception", - "Replacement", - "Revolution", - "River", - "Son", - "Speech", - "Tea", - "Village", - "Warning", - "Winner", - "Worker", - "Writer", - "Assistance", - "Breath", - "Buyer", - "Chest", - "Chocolate", - "Conclusion", - "Contribution", - "Cookie", - "Courage", - "Dad", - "Desk", - "Drawer", - "Establishment", - "Examination", - "Garbage", - "Grocery", - "Honey", - "Impression", - "Improvement", - "Independence", - "Insect", - "Inspection", - "Inspector", - "King", - "Ladder", - "Menu", - "Penalty", - "Piano", - "Potato", - "Profession", - "Professor", - "Quantity", - "Reaction", - "Requirement", - "Salad", - "Sister", - "Supermarket", - "Tongue", - "Weakness", - "Wedding", - "Affair", - "Ambition", - "Analyst", - "Apple", - "Assignment", - "Assistant", - "Bathroom", - "Bedroom", - "Beer", - "Birthday", - "Celebration", - "Championship", - "Cheek", - "Client", - "Consequence", - "Departure", - "Diamond", - "Dirt", - "Ear", - "Fortune", - "Friendship", - "Funeral", - "Gene", - "Girlfriend", - "Hat", - "Indication", - "Intention", - "Lady", - "Midnight", - "Negotiation", - "Obligation", - "Passenger", - "Pizza", - "Platform", - "Poet", - "Pollution", - "Recognition", - "Reputation", - "Shirt", - "Sir", - "Speaker", - "Stranger", - "Surgery", - "Sympathy", - "Tale", - "Throat", - "Trainer", - "Uncle", - "Youth", - "Time", - "Work", - "Film", - "Water", - "Money", - "Example", - "While", - "Business", - "Study", - "Game", - "Life", - "Form", - "Air", - "Day", - "Place", - "Number", - "Part", - "Field", - "Fish", - "Back", - "Process", - "Heat", - "Hand", - "Experience", - "Job", - "Book", - "End", - "Point", - "Type", - "Home", - "Economy", - "Value", - "Body", - "Market", - "Guide", - "Interest", - "State", - "Radio", - "Course", - "Company", - "Price", - "Size", - "Card", - "List", - "Mind", - "Trade", - "Line", - "Care", - "Group", - "Risk", - "Word", - "Fat", - "Force", - "Key", - "Light", - "Training", - "Name", - "School", - "Top", - "Amount", - "Level", - "Order", - "Practice", - "Research", - "Sense", - "Service", - "Piece", - "Web", - "Boss", - "Sport", - "Fun", - "House", - "Page", - "Term", - "Test", - "Answer", - "Sound", - "Focus", - "Matter", - "Kind", - "Soil", - "Board", - "Oil", - "Picture", - "Access", - "Garden", - "Range", - "Rate", - "Reason", - "Future", - "Site", - "Demand", - "Exercise", - "Image", - "Case", - "Cause", - "Coast", - "Action", - "Age", - "Bad", - "Boat", - "Record", - "Result", - "Section", - "Building", - "Mouse", - "Cash", - "Class", - "Nothing", - "Period", - "Plan", - "Store", - "Tax", - "Side", - "Subject", - "Space", - "Rule", - "Stock", - "Weather", - "Chance", - "Figure", - "Man", - "Model", - "Source", - "Beginning", - "Earth", - "Program", - "Chicken", - "Design", - "Feature", - "Head", - "Material", - "Purpose", - "Question", - "Rock", - "Salt", - "Act", - "Birth", - "Car", - "Dog", - "Object", - "Scale", - "Sun", - "Note", - "Profit", - "Rent", - "Speed", - "Style", - "War", - "Bank", - "Craft", - "Half", - "Inside", - "Outside", - "Standard", - "Bus", - "Exchange", - "Eye", - "Fire", - "Position", - "Pressure", - "Stress", - "Advantage", - "Benefit", - "Box", - "Frame", - "Issue", - "Step", - "Cycle", - "Face", - "Item", - "Metal", - "Paint", - "Review", - "Room", - "Screen", - "Structure", - "View", - "Account", - "Ball", - "Discipline", - "Medium", - "Share", - "Balance", - "Bit", - "Black", - "Bottom", - "Choice", - "Gift", - "Impact", - "Machine", - "Shape", - "Tool", - "Wind", - "Address", - "Average", - "Career", - "Culture", - "Morning", - "Pot", - "Sign", - "Table", - "Task", - "Condition", - "Contact", - "Credit", - "Egg", - "Hope", - "Ice", - "Network", - "North", - "Square", - "Attempt", - "Date", - "Effect", - "Link", - "Post", - "Star", - "Voice", - "Capital", - "Challenge", - "Friend", - "Self", - "Shot", - "Brush", - "Couple", - "Debate", - "Exit", - "Front", - "Function", - "Lack", - "Living", - "Plant", - "Plastic", - "Spot", - "Summer", - "Taste", - "Theme", - "Track", - "Wing", - "Brain", - "Button", - "Click", - "Desire", - "Foot", - "Gas", - "Influence", - "Notice", - "Rain", - "Wall", - "Base", - "Damage", - "Distance", - "Feeling", - "Pair", - "Savings", - "Staff", - "Sugar", - "Target", - "Text", - "Animal", - "Author", - "Budget", - "Discount", - "File", - "Ground", - "Lesson", - "Minute", - "Officer", - "Phase", - "Reference", - "Register", - "Sky", - "Stage", - "Stick", - "Title", - "Trouble", - "Bowl", - "Bridge", - "Campaign", - "Character", - "Club", - "Edge", - "Evidence", - "Fan", - "Letter", - "Lock", - "Maximum", - "Novel", - "Option", - "Pack", - "Park", - "Plenty", - "Quarter", - "Skin", - "Sort", - "Weight", - "Baby", - "Background", - "Carry", - "Dish", - "Factor", - "Fruit", - "Glass", - "Joint", - "Master", - "Muscle", - "Red", - "Strength", - "Traffic", - "Trip", - "Vegetable", - "Appeal", - "Chart", - "Gear", - "Ideal", - "Kitchen", - "Land", - "Log", - "Mother", - "Net", - "Party", - "Principle", - "Relative", - "Sale", - "Season", - "Signal", - "Spirit", - "Street", - "Tree", - "Wave", - "Belt", - "Bench", - "Commission", - "Copy", - "Drop", - "Minimum", - "Path", - "Progress", - "Project", - "Sea", - "South", - "Status", - "Stuff", - "Ticket", - "Tour", - "Angle", - "Blue", - "Breakfast", - "Confidence", - "Daughter", - "Degree", - "Doctor", - "Dot", - "Dream", - "Duty", - "Essay", - "Father", - "Fee", - "Finance", - "Hour", - "Juice", - "Limit", - "Luck", - "Milk", - "Mouth", - "Peace", - "Pipe", - "Seat", - "Stable", - "Storm", - "Substance", - "Team", - "Trick", - "Afternoon", - "Bat", - "Beach", - "Blank", - "Catch", - "Chain", - "Consideration", - "Cream", - "Crew", - "Detail", - "Gold", - "Interview", - "Kid", - "Mark", - "Match", - "Mission", - "Pain", - "Pleasure", - "Score", - "Screw", - "Sex", - "Shop", - "Shower", - "Suit", - "Tone", - "Window", - "Agent", - "Band", - "Block", - "Bone", - "Calendar", - "Cap", - "Coat", - "Contest", - "Corner", - "Court", - "Cup", - "District", - "Door", - "East", - "Finger", - "Garage", - "Guarantee", - "Hole", - "Hook", - "Implement", - "Layer", - "Lecture", - "Lie", - "Manner", - "Meeting", - "Nose", - "Parking", - "Partner", - "Profile", - "Respect", - "Rice", - "Routine", - "Schedule", - "Swimming", - "Telephone", - "Tip", - "Winter", - "Airline", - "Bag", - "Battle", - "Bed", - "Bill", - "Bother", - "Cake", - "Code", - "Curve", - "Designer", - "Dimension", - "Dress", - "Ease", - "Emergency", - "Evening", - "Extension", - "Farm", - "Fight", - "Gap", - "Grade", - "Holiday", - "Horror", - "Horse", - "Host", - "Husband", - "Loan", - "Mistake", - "Mountain", - "Nail", - "Noise", - "Occasion", - "Package", - "Patient", - "Pause", - "Phrase", - "Proof", - "Race", - "Relief", - "Sand", - "Sentence", - "Shoulder", - "Smoke", - "Stomach", - "String", - "Tourist", - "Towel", - "Vacation", - "West", - "Wheel", - "Wine", - "Arm", - "Aside", - "Associate", - "Bet", - "Blow", - "Border", - "Branch", - "Breast", - "Brother", - "Buddy", - "Bunch", - "Chip", - "Coach", - "Cross", - "Document", - "Draft", - "Dust", - "Expert", - "Floor", - "God", - "Golf", - "Habit", - "Iron", - "Judge", - "Knife", - "Landscape", - "League", - "Mail", - "Mess", - "Native", - "Opening", - "Parent", - "Pattern", - "Pin", - "Pool", - "Pound", - "Request", - "Salary", - "Shame", - "Shelter", - "Shoe", - "Silver", - "Tackle", - "Tank", - "Trust", - "Assist", - "Bake", - "Bar", - "Bell", - "Bike", - "Blame", - "Boy", - "Brick", - "Chair", - "Closet", - "Clue", - "Collar", - "Comment", - "Conference", - "Devil", - "Diet", - "Fear", - "Fuel", - "Glove", - "Jacket", - "Lunch", - "Monitor", - "Mortgage", - "Nurse", - "Pace", - "Panic", - "Peak", - "Plane", - "Reward", - "Row", - "Sandwich", - "Shock", - "Spite", - "Spray", - "Surprise", - "Till", - "Transition", - "Weekend", - "Welcome", - "Yard", - "Alarm", - "Bend", - "Bicycle", - "Bite", - "Blind", - "Bottle", - "Cable", - "Candle", - "Clerk", - "Cloud", - "Concert", - "Counter", - "Flower", - "Grandfather", - "Harm", - "Knee", - "Lawyer", - "Leather", - "Load", - "Mirror", - "Neck", - "Pension", - "Plate", - "Purple", - "Ruin", - "Ship", - "Skirt", - "Slice", - "Snow", - "Specialist", - "Stroke", - "Switch", - "Trash", - "Tune", - "Zone", - "Anger", - "Award", - "Bid", - "Bitter", - "Boot", - "Bug", - "Camp", - "Candy", - "Carpet", - "Cat", - "Champion", - "Channel", - "Clock", - "Comfort", - "Cow", - "Crack", - "Engineer", - "Entrance", - "Fault", - "Grass", - "Guy", - "Hell", - "Highlight", - "Incident", - "Island", - "Joke", - "Jury", - "Leg", - "Lip", - "Mate", - "Motor", - "Nerve", - "Passage", - "Pen", - "Pride", - "Priest", - "Prize", - "Promise", - "Resident", - "Resort", - "Ring", - "Roof", - "Rope", - "Sail", - "Scheme", - "Script", - "Sock", - "Station", - "Toe", - "Tower", - "Truck", - "Witness", - "A", - "You", - "It", - "Can", - "Will", - "If", - "One", - "Many", - "Most", - "Other", - "Use", - "Make", - "Good", - "Look", - "Help", - "Go", - "Great", - "Being", - "Few", - "Might", - "Still", - "Public", - "Read", - "Keep", - "Start", - "Give", - "Human", - "Local", - "General", - "She", - "Specific", - "Long", - "Play", - "Feel", - "High", - "Tonight", - "Put", - "Common", - "Set", - "Change", - "Simple", - "Past", - "Big", - "Possible", - "Particular", - "Today", - "Major", - "Personal", - "Current", - "National", - "Cut", - "Natural", - "Physical", - "Show", - "Try", - "Check", - "Second", - "Call", - "Move", - "Pay", - "Let", - "Increase", - "Single", - "Individual", - "Turn", - "Ask", - "Buy", - "Guard", - "Hold", - "Main", - "Offer", - "Potential", - "Professional", - "International", - "Travel", - "Cook", - "Alternative", - "Following", - "Special", - "Working", - "Whole", - "Dance", - "Excuse", - "Cold", - "Commercial", - "Low", - "Purchase", - "Deal", - "Primary", - "Worth", - "Fall", - "Necessary", - "Positive", - "Produce", - "Search", - "Present", - "Spend", - "Talk", - "Creative", - "Tell", - "Cost", - "Drive", - "Green", - "Support", - "Glad", - "Remove", - "Return", - "Run", - "Complex", - "Due", - "Effective", - "Middle", - "Regular", - "Reserve", - "Independent", - "Leave", - "Original", - "Reach", - "Rest", - "Serve", - "Watch", - "Beautiful", - "Charge", - "Active", - "Break", - "Negative", - "Safe", - "Stay", - "Visit", - "Visual", - "Affect", - "Cover", - "Report", - "Rise", - "Walk", - "White", - "Beyond", - "Junior", - "Pick", - "Unique", - "Anything", - "Classic", - "Final", - "Lift", - "Mix", - "Private", - "Stop", - "Teach", - "Western", - "Concern", - "Familiar", - "Fly", - "Official", - "Broad", - "Comfortable", - "Gain", - "Maybe", - "Rich", - "Save", - "Stand", - "Young", - "Fail", - "Heavy", - "Hello", - "Lead", - "Listen", - "Valuable", - "Worry", - "Handle", - "Leading", - "Meet", - "Release", - "Sell", - "Finish", - "Normal", - "Press", - "Ride", - "Secret", - "Spread", - "Spring", - "Tough", - "Wait", - "Brown", - "Deep", - "Display", - "Flow", - "Hit", - "Objective", - "Shoot", - "Touch", - "Cancel", - "Chemical", - "Cry", - "Dump", - "Extreme", - "Push", - "Conflict", - "Eat", - "Fill", - "Formal", - "Jump", - "Kick", - "Opposite", - "Pass", - "Pitch", - "Remote", - "Total", - "Treat", - "Vast", - "Abuse", - "Beat", - "Burn", - "Deposit", - "Print", - "Raise", - "Sleep", - "Somewhere", - "Advance", - "Anywhere", - "Consist", - "Dark", - "Double", - "Draw", - "Equal", - "Fix", - "Hire", - "Internal", - "Join", - "Kill", - "Sensitive", - "Tap", - "Win", - "Attack", - "Claim", - "Constant", - "Drag", - "Drink", - "Guess", - "Minor", - "Pull", - "Raw", - "Soft", - "Solid", - "Wear", - "Weird", - "Wonder", - "Annual", - "Count", - "Dead", - "Doubt", - "Feed", - "Forever", - "Impress", - "Nobody", - "Repeat", - "Round", - "Sing", - "Slide", - "Strip", - "Whereas", - "Wish", - "Combine", - "Command", - "Dig", - "Divide", - "Equivalent", - "Hang", - "Hunt", - "Initial", - "March", - "Mention", - "Smell", - "Spiritual", - "Survey", - "Tie", - "Adult", - "Brief", - "Crazy", - "Escape", - "Gather", - "Hate", - "Prior", - "Repair", - "Rough", - "Sad", - "Scratch", - "Sick", - "Strike", - "Employ", - "External", - "Hurt", - "Illegal", - "Laugh", - "Lay", - "Mobile", - "Nasty", - "Ordinary", - "Respond", - "Royal", - "Senior", - "Split", - "Strain", - "Struggle", - "Swim", - "Train", - "Upper", - "Wash", - "Yellow", - "Convert", - "Crash", - "Dependent", - "Fold", - "Funny", - "Grab", - "Hide", - "Miss", - "Permit", - "Quote", - "Recover", - "Resolve", - "Roll", - "Sink", - "Slip", - "Spare", - "Suspect", - "Sweet", - "Swing", - "Twist", - "Upstairs", - "Usual", - "Abroad", - "Brave", - "Calm", - "Concentrate", - "Estimate", - "Grand", - "Male", - "Mine", - "Prompt", - "Quiet", - "Refuse", - "Regret", - "Reveal", - "Rush", - "Shake", - "Shift", - "Shine", - "Steal", - "Suck", - "Surround", - "Anybody", - "Bear", - "Brilliant", - "Dare", - "Dear", - "Delay", - "Drunk", - "Female", - "Hurry", - "Inevitable", - "Invite", - "Kiss", - "Neat", - "Pop", - "Punch", - "Quit", - "Reply", - "Representative", - "Resist", - "Rip", - "Rub", - "Silly", - "Smile", - "Spell", - "Stretch", - "Stupid", - "Tear", - "Temporary", - "Tomorrow", - "Wake", - "Wrap", - "Yesterday", + "People", + "History", + "Way", + "Art", + "World", + "Information", + "Map", + "Two", + "Family", + "Government", + "Health", + "System", + "Computer", + "Meat", + "Year", + "Thanks", + "Music", + "Person", + "Reading", + "Method", + "Data", + "Food", + "Understanding", + "Theory", + "Law", + "Bird", + "Literature", + "Problem", + "Software", + "Control", + "Knowledge", + "Power", + "Ability", + "Economics", + "Love", + "Internet", + "Television", + "Science", + "Library", + "Nature", + "Fact", + "Product", + "Idea", + "Temperature", + "Investment", + "Area", + "Society", + "Activity", + "Story", + "Industry", + "Media", + "Thing", + "Oven", + "Community", + "Definition", + "Safety", + "Quality", + "Development", + "Language", + "Management", + "Player", + "Variety", + "Video", + "Week", + "Security", + "Country", + "Exam", + "Movie", + "Organization", + "Equipment", + "Physics", + "Analysis", + "Policy", + "Series", + "Thought", + "Basis", + "Boyfriend", + "Direction", + "Strategy", + "Technology", + "Army", + "Camera", + "Freedom", + "Paper", + "Environment", + "Child", + "Instance", + "Month", + "Truth", + "Marketing", + "University", + "Writing", + "Article", + "Department", + "Difference", + "Goal", + "News", + "Audience", + "Fishing", + "Growth", + "Income", + "Marriage", + "User", + "Combination", + "Failure", + "Meaning", + "Medicine", + "Philosophy", + "Teacher", + "Communication", + "Night", + "Chemistry", + "Disease", + "Disk", + "Energy", + "Nation", + "Road", + "Role", + "Soup", + "Advertising", + "Location", + "Success", + "Addition", + "Apartment", + "Education", + "Math", + "Moment", + "Painting", + "Politics", + "Attention", + "Decision", + "Event", + "Property", + "Shopping", + "Student", + "Wood", + "Competition", + "Distribution", + "Entertainment", + "Office", + "Population", + "President", + "Unit", + "Category", + "Cigarette", + "Context", + "Introduction", + "Opportunity", + "Performance", + "Driver", + "Flight", + "Length", + "Magazine", + "Newspaper", + "Relationship", + "Teaching", + "Cell", + "Dealer", + "Finding", + "Lake", + "Member", + "Message", + "Phone", + "Scene", + "Appearance", + "Association", + "Concept", + "Customer", + "Death", + "Discussion", + "Housing", + "Inflation", + "Insurance", + "Mood", + "Woman", + "Advice", + "Blood", + "Effort", + "Expression", + "Importance", + "Opinion", + "Payment", + "Reality", + "Responsibility", + "Situation", + "Skill", + "Statement", + "Wealth", + "Application", + "City", + "County", + "Depth", + "Estate", + "Foundation", + "Grandmother", + "Heart", + "Perspective", + "Photo", + "Recipe", + "Studio", + "Topic", + "Collection", + "Depression", + "Imagination", + "Passion", + "Percentage", + "Resource", + "Setting", + "Ad", + "Agency", + "College", + "Connection", + "Criticism", + "Debt", + "Description", + "Memory", + "Patience", + "Secretary", + "Solution", + "Administration", + "Aspect", + "Attitude", + "Director", + "Personality", + "Psychology", + "Recommendation", + "Response", + "Selection", + "Storage", + "Version", + "Alcohol", + "Argument", + "Complaint", + "Contract", + "Emphasis", + "Highway", + "Loss", + "Membership", + "Possession", + "Preparation", + "Steak", + "Union", + "Agreement", + "Cancer", + "Currency", + "Employment", + "Engineering", + "Entry", + "Interaction", + "Mixture", + "Preference", + "Region", + "Republic", + "Tradition", + "Virus", + "Actor", + "Classroom", + "Delivery", + "Device", + "Difficulty", + "Drama", + "Election", + "Engine", + "Football", + "Guidance", + "Hotel", + "Owner", + "Priority", + "Protection", + "Suggestion", + "Tension", + "Variation", + "Anxiety", + "Atmosphere", + "Awareness", + "Bath", + "Bread", + "Candidate", + "Climate", + "Comparison", + "Confusion", + "Construction", + "Elevator", + "Emotion", + "Employee", + "Employer", + "Guest", + "Height", + "Leadership", + "Mall", + "Manager", + "Operation", + "Recording", + "Sample", + "Transportation", + "Charity", + "Cousin", + "Disaster", + "Editor", + "Efficiency", + "Excitement", + "Extent", + "Feedback", + "Guitar", + "Homework", + "Leader", + "Mom", + "Outcome", + "Permission", + "Presentation", + "Promotion", + "Reflection", + "Refrigerator", + "Resolution", + "Revenue", + "Session", + "Singer", + "Tennis", + "Basket", + "Bonus", + "Cabinet", + "Childhood", + "Church", + "Clothes", + "Coffee", + "Dinner", + "Drawing", + "Hair", + "Hearing", + "Initiative", + "Judgment", + "Lab", + "Measurement", + "Mode", + "Mud", + "Orange", + "Poetry", + "Police", + "Possibility", + "Procedure", + "Queen", + "Ratio", + "Relation", + "Restaurant", + "Satisfaction", + "Sector", + "Signature", + "Significance", + "Song", + "Tooth", + "Town", + "Vehicle", + "Volume", + "Wife", + "Accident", + "Airport", + "Appointment", + "Arrival", + "Assumption", + "Baseball", + "Chapter", + "Committee", + "Conversation", + "Database", + "Enthusiasm", + "Error", + "Explanation", + "Farmer", + "Gate", + "Girl", + "Hall", + "Historian", + "Hospital", + "Injury", + "Instruction", + "Maintenance", + "Manufacturer", + "Meal", + "Perception", + "Pie", + "Poem", + "Presence", + "Proposal", + "Reception", + "Replacement", + "Revolution", + "River", + "Son", + "Speech", + "Tea", + "Village", + "Warning", + "Winner", + "Worker", + "Writer", + "Assistance", + "Breath", + "Buyer", + "Chest", + "Chocolate", + "Conclusion", + "Contribution", + "Cookie", + "Courage", + "Dad", + "Desk", + "Drawer", + "Establishment", + "Examination", + "Garbage", + "Grocery", + "Honey", + "Impression", + "Improvement", + "Independence", + "Insect", + "Inspection", + "Inspector", + "King", + "Ladder", + "Menu", + "Penalty", + "Piano", + "Potato", + "Profession", + "Professor", + "Quantity", + "Reaction", + "Requirement", + "Salad", + "Sister", + "Supermarket", + "Tongue", + "Weakness", + "Wedding", + "Affair", + "Ambition", + "Analyst", + "Apple", + "Assignment", + "Assistant", + "Bathroom", + "Bedroom", + "Beer", + "Birthday", + "Celebration", + "Championship", + "Cheek", + "Client", + "Consequence", + "Departure", + "Diamond", + "Dirt", + "Ear", + "Fortune", + "Friendship", + "Funeral", + "Gene", + "Girlfriend", + "Hat", + "Indication", + "Intention", + "Lady", + "Midnight", + "Negotiation", + "Obligation", + "Passenger", + "Pizza", + "Platform", + "Poet", + "Pollution", + "Recognition", + "Reputation", + "Shirt", + "Sir", + "Speaker", + "Stranger", + "Surgery", + "Sympathy", + "Tale", + "Throat", + "Trainer", + "Uncle", + "Youth", + "Time", + "Work", + "Film", + "Water", + "Money", + "Example", + "While", + "Business", + "Study", + "Game", + "Life", + "Form", + "Air", + "Day", + "Place", + "Number", + "Part", + "Field", + "Fish", + "Back", + "Process", + "Heat", + "Hand", + "Experience", + "Job", + "Book", + "End", + "Point", + "Type", + "Home", + "Economy", + "Value", + "Body", + "Market", + "Guide", + "Interest", + "State", + "Radio", + "Course", + "Company", + "Price", + "Size", + "Card", + "List", + "Mind", + "Trade", + "Line", + "Care", + "Group", + "Risk", + "Word", + "Fat", + "Force", + "Key", + "Light", + "Training", + "Name", + "School", + "Top", + "Amount", + "Level", + "Order", + "Practice", + "Research", + "Sense", + "Service", + "Piece", + "Web", + "Boss", + "Sport", + "Fun", + "House", + "Page", + "Term", + "Test", + "Answer", + "Sound", + "Focus", + "Matter", + "Kind", + "Soil", + "Board", + "Oil", + "Picture", + "Access", + "Garden", + "Range", + "Rate", + "Reason", + "Future", + "Site", + "Demand", + "Exercise", + "Image", + "Case", + "Cause", + "Coast", + "Action", + "Age", + "Bad", + "Boat", + "Record", + "Result", + "Section", + "Building", + "Mouse", + "Cash", + "Class", + "Nothing", + "Period", + "Plan", + "Store", + "Tax", + "Side", + "Subject", + "Space", + "Rule", + "Stock", + "Weather", + "Chance", + "Figure", + "Man", + "Model", + "Source", + "Beginning", + "Earth", + "Program", + "Chicken", + "Design", + "Feature", + "Head", + "Material", + "Purpose", + "Question", + "Rock", + "Salt", + "Act", + "Birth", + "Car", + "Dog", + "Object", + "Scale", + "Sun", + "Note", + "Profit", + "Rent", + "Speed", + "Style", + "War", + "Bank", + "Craft", + "Half", + "Inside", + "Outside", + "Standard", + "Bus", + "Exchange", + "Eye", + "Fire", + "Position", + "Pressure", + "Stress", + "Advantage", + "Benefit", + "Box", + "Frame", + "Issue", + "Step", + "Cycle", + "Face", + "Item", + "Metal", + "Paint", + "Review", + "Room", + "Screen", + "Structure", + "View", + "Account", + "Ball", + "Discipline", + "Medium", + "Share", + "Balance", + "Bit", + "Black", + "Bottom", + "Choice", + "Gift", + "Impact", + "Machine", + "Shape", + "Tool", + "Wind", + "Address", + "Average", + "Career", + "Culture", + "Morning", + "Pot", + "Sign", + "Table", + "Task", + "Condition", + "Contact", + "Credit", + "Egg", + "Hope", + "Ice", + "Network", + "North", + "Square", + "Attempt", + "Date", + "Effect", + "Link", + "Post", + "Star", + "Voice", + "Capital", + "Challenge", + "Friend", + "Self", + "Shot", + "Brush", + "Couple", + "Debate", + "Exit", + "Front", + "Function", + "Lack", + "Living", + "Plant", + "Plastic", + "Spot", + "Summer", + "Taste", + "Theme", + "Track", + "Wing", + "Brain", + "Button", + "Click", + "Desire", + "Foot", + "Gas", + "Influence", + "Notice", + "Rain", + "Wall", + "Base", + "Damage", + "Distance", + "Feeling", + "Pair", + "Savings", + "Staff", + "Sugar", + "Target", + "Text", + "Animal", + "Author", + "Budget", + "Discount", + "File", + "Ground", + "Lesson", + "Minute", + "Officer", + "Phase", + "Reference", + "Register", + "Sky", + "Stage", + "Stick", + "Title", + "Trouble", + "Bowl", + "Bridge", + "Campaign", + "Character", + "Club", + "Edge", + "Evidence", + "Fan", + "Letter", + "Lock", + "Maximum", + "Novel", + "Option", + "Pack", + "Park", + "Plenty", + "Quarter", + "Skin", + "Sort", + "Weight", + "Baby", + "Background", + "Carry", + "Dish", + "Factor", + "Fruit", + "Glass", + "Joint", + "Master", + "Muscle", + "Red", + "Strength", + "Traffic", + "Trip", + "Vegetable", + "Appeal", + "Chart", + "Gear", + "Ideal", + "Kitchen", + "Land", + "Log", + "Mother", + "Net", + "Party", + "Principle", + "Relative", + "Sale", + "Season", + "Signal", + "Spirit", + "Street", + "Tree", + "Wave", + "Belt", + "Bench", + "Commission", + "Copy", + "Drop", + "Minimum", + "Path", + "Progress", + "Project", + "Sea", + "South", + "Status", + "Stuff", + "Ticket", + "Tour", + "Angle", + "Blue", + "Breakfast", + "Confidence", + "Daughter", + "Degree", + "Doctor", + "Dot", + "Dream", + "Duty", + "Essay", + "Father", + "Fee", + "Finance", + "Hour", + "Juice", + "Limit", + "Luck", + "Milk", + "Mouth", + "Peace", + "Pipe", + "Seat", + "Stable", + "Storm", + "Substance", + "Team", + "Trick", + "Afternoon", + "Bat", + "Beach", + "Blank", + "Catch", + "Chain", + "Consideration", + "Cream", + "Crew", + "Detail", + "Gold", + "Interview", + "Kid", + "Mark", + "Match", + "Mission", + "Pain", + "Pleasure", + "Score", + "Screw", + "Sex", + "Shop", + "Shower", + "Suit", + "Tone", + "Window", + "Agent", + "Band", + "Block", + "Bone", + "Calendar", + "Cap", + "Coat", + "Contest", + "Corner", + "Court", + "Cup", + "District", + "Door", + "East", + "Finger", + "Garage", + "Guarantee", + "Hole", + "Hook", + "Implement", + "Layer", + "Lecture", + "Lie", + "Manner", + "Meeting", + "Nose", + "Parking", + "Partner", + "Profile", + "Respect", + "Rice", + "Routine", + "Schedule", + "Swimming", + "Telephone", + "Tip", + "Winter", + "Airline", + "Bag", + "Battle", + "Bed", + "Bill", + "Bother", + "Cake", + "Code", + "Curve", + "Designer", + "Dimension", + "Dress", + "Ease", + "Emergency", + "Evening", + "Extension", + "Farm", + "Fight", + "Gap", + "Grade", + "Holiday", + "Horror", + "Horse", + "Host", + "Husband", + "Loan", + "Mistake", + "Mountain", + "Nail", + "Noise", + "Occasion", + "Package", + "Patient", + "Pause", + "Phrase", + "Proof", + "Race", + "Relief", + "Sand", + "Sentence", + "Shoulder", + "Smoke", + "Stomach", + "String", + "Tourist", + "Towel", + "Vacation", + "West", + "Wheel", + "Wine", + "Arm", + "Aside", + "Associate", + "Bet", + "Blow", + "Border", + "Branch", + "Breast", + "Brother", + "Buddy", + "Bunch", + "Chip", + "Coach", + "Cross", + "Document", + "Draft", + "Dust", + "Expert", + "Floor", + "God", + "Golf", + "Habit", + "Iron", + "Judge", + "Knife", + "Landscape", + "League", + "Mail", + "Mess", + "Native", + "Opening", + "Parent", + "Pattern", + "Pin", + "Pool", + "Pound", + "Request", + "Salary", + "Shame", + "Shelter", + "Shoe", + "Silver", + "Tackle", + "Tank", + "Trust", + "Assist", + "Bake", + "Bar", + "Bell", + "Bike", + "Blame", + "Boy", + "Brick", + "Chair", + "Closet", + "Clue", + "Collar", + "Comment", + "Conference", + "Devil", + "Diet", + "Fear", + "Fuel", + "Glove", + "Jacket", + "Lunch", + "Monitor", + "Mortgage", + "Nurse", + "Pace", + "Panic", + "Peak", + "Plane", + "Reward", + "Row", + "Sandwich", + "Shock", + "Spite", + "Spray", + "Surprise", + "Till", + "Transition", + "Weekend", + "Welcome", + "Yard", + "Alarm", + "Bend", + "Bicycle", + "Bite", + "Blind", + "Bottle", + "Cable", + "Candle", + "Clerk", + "Cloud", + "Concert", + "Counter", + "Flower", + "Grandfather", + "Harm", + "Knee", + "Lawyer", + "Leather", + "Load", + "Mirror", + "Neck", + "Pension", + "Plate", + "Purple", + "Ruin", + "Ship", + "Skirt", + "Slice", + "Snow", + "Specialist", + "Stroke", + "Switch", + "Trash", + "Tune", + "Zone", + "Anger", + "Award", + "Bid", + "Bitter", + "Boot", + "Bug", + "Camp", + "Candy", + "Carpet", + "Cat", + "Champion", + "Channel", + "Clock", + "Comfort", + "Cow", + "Crack", + "Engineer", + "Entrance", + "Fault", + "Grass", + "Guy", + "Hell", + "Highlight", + "Incident", + "Island", + "Joke", + "Jury", + "Leg", + "Lip", + "Mate", + "Motor", + "Nerve", + "Passage", + "Pen", + "Pride", + "Priest", + "Prize", + "Promise", + "Resident", + "Resort", + "Ring", + "Roof", + "Rope", + "Sail", + "Scheme", + "Script", + "Sock", + "Station", + "Toe", + "Tower", + "Truck", + "Witness", + "A", + "You", + "It", + "Can", + "Will", + "If", + "One", + "Many", + "Most", + "Other", + "Use", + "Make", + "Good", + "Look", + "Help", + "Go", + "Great", + "Being", + "Few", + "Might", + "Still", + "Public", + "Read", + "Keep", + "Start", + "Give", + "Human", + "Local", + "General", + "She", + "Specific", + "Long", + "Play", + "Feel", + "High", + "Tonight", + "Put", + "Common", + "Set", + "Change", + "Simple", + "Past", + "Big", + "Possible", + "Particular", + "Today", + "Major", + "Personal", + "Current", + "National", + "Cut", + "Natural", + "Physical", + "Show", + "Try", + "Check", + "Second", + "Call", + "Move", + "Pay", + "Let", + "Increase", + "Single", + "Individual", + "Turn", + "Ask", + "Buy", + "Guard", + "Hold", + "Main", + "Offer", + "Potential", + "Professional", + "International", + "Travel", + "Cook", + "Alternative", + "Following", + "Special", + "Working", + "Whole", + "Dance", + "Excuse", + "Cold", + "Commercial", + "Low", + "Purchase", + "Deal", + "Primary", + "Worth", + "Fall", + "Necessary", + "Positive", + "Produce", + "Search", + "Present", + "Spend", + "Talk", + "Creative", + "Tell", + "Cost", + "Drive", + "Green", + "Support", + "Glad", + "Remove", + "Return", + "Run", + "Complex", + "Due", + "Effective", + "Middle", + "Regular", + "Reserve", + "Independent", + "Leave", + "Original", + "Reach", + "Rest", + "Serve", + "Watch", + "Beautiful", + "Charge", + "Active", + "Break", + "Negative", + "Safe", + "Stay", + "Visit", + "Visual", + "Affect", + "Cover", + "Report", + "Rise", + "Walk", + "White", + "Beyond", + "Junior", + "Pick", + "Unique", + "Anything", + "Classic", + "Final", + "Lift", + "Mix", + "Private", + "Stop", + "Teach", + "Western", + "Concern", + "Familiar", + "Fly", + "Official", + "Broad", + "Comfortable", + "Gain", + "Maybe", + "Rich", + "Save", + "Stand", + "Young", + "Fail", + "Heavy", + "Hello", + "Lead", + "Listen", + "Valuable", + "Worry", + "Handle", + "Leading", + "Meet", + "Release", + "Sell", + "Finish", + "Normal", + "Press", + "Ride", + "Secret", + "Spread", + "Spring", + "Tough", + "Wait", + "Brown", + "Deep", + "Display", + "Flow", + "Hit", + "Objective", + "Shoot", + "Touch", + "Cancel", + "Chemical", + "Cry", + "Dump", + "Extreme", + "Push", + "Conflict", + "Eat", + "Fill", + "Formal", + "Jump", + "Kick", + "Opposite", + "Pass", + "Pitch", + "Remote", + "Total", + "Treat", + "Vast", + "Abuse", + "Beat", + "Burn", + "Deposit", + "Print", + "Raise", + "Sleep", + "Somewhere", + "Advance", + "Anywhere", + "Consist", + "Dark", + "Double", + "Draw", + "Equal", + "Fix", + "Hire", + "Internal", + "Join", + "Kill", + "Sensitive", + "Tap", + "Win", + "Attack", + "Claim", + "Constant", + "Drag", + "Drink", + "Guess", + "Minor", + "Pull", + "Raw", + "Soft", + "Solid", + "Wear", + "Weird", + "Wonder", + "Annual", + "Count", + "Dead", + "Doubt", + "Feed", + "Forever", + "Impress", + "Nobody", + "Repeat", + "Round", + "Sing", + "Slide", + "Strip", + "Whereas", + "Wish", + "Combine", + "Command", + "Dig", + "Divide", + "Equivalent", + "Hang", + "Hunt", + "Initial", + "March", + "Mention", + "Smell", + "Spiritual", + "Survey", + "Tie", + "Adult", + "Brief", + "Crazy", + "Escape", + "Gather", + "Hate", + "Prior", + "Repair", + "Rough", + "Sad", + "Scratch", + "Sick", + "Strike", + "Employ", + "External", + "Hurt", + "Illegal", + "Laugh", + "Lay", + "Mobile", + "Nasty", + "Ordinary", + "Respond", + "Royal", + "Senior", + "Split", + "Strain", + "Struggle", + "Swim", + "Train", + "Upper", + "Wash", + "Yellow", + "Convert", + "Crash", + "Dependent", + "Fold", + "Funny", + "Grab", + "Hide", + "Miss", + "Permit", + "Quote", + "Recover", + "Resolve", + "Roll", + "Sink", + "Slip", + "Spare", + "Suspect", + "Sweet", + "Swing", + "Twist", + "Upstairs", + "Usual", + "Abroad", + "Brave", + "Calm", + "Concentrate", + "Estimate", + "Grand", + "Male", + "Mine", + "Prompt", + "Quiet", + "Refuse", + "Regret", + "Reveal", + "Rush", + "Shake", + "Shift", + "Shine", + "Steal", + "Suck", + "Surround", + "Anybody", + "Bear", + "Brilliant", + "Dare", + "Dear", + "Delay", + "Drunk", + "Female", + "Hurry", + "Inevitable", + "Invite", + "Kiss", + "Neat", + "Pop", + "Punch", + "Quit", + "Reply", + "Representative", + "Resist", + "Rip", + "Rub", + "Silly", + "Smile", + "Spell", + "Stretch", + "Stupid", + "Tear", + "Temporary", + "Tomorrow", + "Wake", + "Wrap", + "Yesterday", ]; export function GetRandomNoun() { - return nouns[Math.floor(Math.random() * nouns.length)]; + return nouns[Math.floor(Math.random() * nouns.length)]; } export function GetRandomAdjective() { - return adjectives[Math.floor(Math.random() * adjectives.length)]; + return adjectives[Math.floor(Math.random() * adjectives.length)]; } diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 05e681a09..b544d4f08 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -1,30 +1,30 @@ import { - CounterDocument, - FolderDocument, - GoalDocument, - IIDXBPIData, - IIDXEamusementScoreDocument, - ImportDocument, - TierlistParent, - InviteCodeDocument, - APITokenDocument, - TierlistDataDocument, - MilestoneDocument, - NotificationDocument, - FolderChartLookup, - ImportTimingsDocument, - PrivateUserDocument, - ScoreDocument, - KaiAuthDocument, - SessionDocument, - AnySongDocument, - AnyChartDocument, - UserGameStats, - UserGoalDocument, - PBScoreDocument, - UserMilestoneDocument, - BMSCourseDocument, - ImportLockDocument, + CounterDocument, + FolderDocument, + GoalDocument, + IIDXBPIData, + IIDXEamusementScoreDocument, + ImportDocument, + TierlistParent, + InviteCodeDocument, + APITokenDocument, + TierlistDataDocument, + MilestoneDocument, + NotificationDocument, + FolderChartLookup, + ImportTimingsDocument, + PrivateUserDocument, + ScoreDocument, + KaiAuthDocument, + SessionDocument, + AnySongDocument, + AnyChartDocument, + UserGameStats, + UserGoalDocument, + PBScoreDocument, + UserMilestoneDocument, + BMSCourseDocument, + ImportLockDocument, } from "tachi-common"; import monk from "monk"; import { MONGO_CONNECTION_URL, MONGO_DATABASE_NAME } from "../../lib/setup/config"; @@ -37,98 +37,98 @@ let dbName = MONGO_DATABASE_NAME; /* istanbul ignore next */ if (process.env.NODE_ENV === "test") { - if (process.env.TACHI_PARALLEL_TESTS) { - dbName = `test-ephemeral-${process.pid}`; - } else { - dbName = `testingdb`; - } + if (process.env.TACHI_PARALLEL_TESTS) { + dbName = `test-ephemeral-${process.pid}`; + } else { + dbName = `testingdb`; + } } let dbtime: [number, number] = [0, 0]; /* istanbul ignore next */ if (process.env.NODE_ENV !== "test") { - logger.info(`Connecting to database ${MONGO_CONNECTION_URL}/${dbName}...`); - dbtime = process.hrtime(); + logger.info(`Connecting to database ${MONGO_CONNECTION_URL}/${dbName}...`); + dbtime = process.hrtime(); } export const monkDB = monk(`${MONGO_CONNECTION_URL}/${dbName}`); /* istanbul ignore next */ monkDB - .then(() => { - if (process.env.NODE_ENV !== "test") { - const time = process.hrtime(dbtime); - const elapsed = time[0] + time[1] / 1e6; - logger.info(`Database connection successful: took ${elapsed}ms`); - } - }) - .catch((err) => { - logger.crit(err); - process.exit(1); - }); + .then(() => { + if (process.env.NODE_ENV !== "test") { + const time = process.hrtime(dbtime); + const elapsed = time[0] + time[1] / 1e6; + logger.info(`Database connection successful: took ${elapsed}ms`); + } + }) + .catch((err) => { + logger.crit(err); + process.exit(1); + }); export async function CloseMongoConnection() { - await monkDB.close(); + await monkDB.close(); } const songs = { - bms: monkDB.get(`songs-bms`), - chunithm: monkDB.get(`songs-chunithm`), - ddr: monkDB.get(`songs-ddr`), - gitadora: monkDB.get(`songs-gitadora`), - iidx: monkDB.get(`songs-iidx`), - jubeat: monkDB.get(`songs-jubeat`), - maimai: monkDB.get(`songs-maimai`), - museca: monkDB.get(`songs-museca`), - popn: monkDB.get(`songs-popn`), - sdvx: monkDB.get(`songs-sdvx`), - usc: monkDB.get(`songs-usc`), + bms: monkDB.get(`songs-bms`), + chunithm: monkDB.get(`songs-chunithm`), + ddr: monkDB.get(`songs-ddr`), + gitadora: monkDB.get(`songs-gitadora`), + iidx: monkDB.get(`songs-iidx`), + jubeat: monkDB.get(`songs-jubeat`), + maimai: monkDB.get(`songs-maimai`), + museca: monkDB.get(`songs-museca`), + popn: monkDB.get(`songs-popn`), + sdvx: monkDB.get(`songs-sdvx`), + usc: monkDB.get(`songs-usc`), }; const charts = { - bms: monkDB.get(`charts-bms`), - chunithm: monkDB.get(`charts-chunithm`), - ddr: monkDB.get(`charts-ddr`), - gitadora: monkDB.get(`charts-gitadora`), - iidx: monkDB.get(`charts-iidx`), - jubeat: monkDB.get(`charts-jubeat`), - maimai: monkDB.get(`charts-maimai`), - museca: monkDB.get(`charts-museca`), - popn: monkDB.get(`charts-popn`), - sdvx: monkDB.get(`charts-sdvx`), - usc: monkDB.get(`charts-usc`), + bms: monkDB.get(`charts-bms`), + chunithm: monkDB.get(`charts-chunithm`), + ddr: monkDB.get(`charts-ddr`), + gitadora: monkDB.get(`charts-gitadora`), + iidx: monkDB.get(`charts-iidx`), + jubeat: monkDB.get(`charts-jubeat`), + maimai: monkDB.get(`charts-maimai`), + museca: monkDB.get(`charts-museca`), + popn: monkDB.get(`charts-popn`), + sdvx: monkDB.get(`charts-sdvx`), + usc: monkDB.get(`charts-usc`), }; const db = { - // i have to handwrite this out for TS... :( - // dont worry, it was all macro'd - songs, - charts, - scores: monkDB.get("scores"), - tierlists: monkDB.get("tierlists"), - "tierlist-data": monkDB.get>("tierlist-data"), - "personal-bests": monkDB.get("personal-bests"), - folders: monkDB.get("folders"), - "folder-chart-lookup": monkDB.get("folder-chart-lookup"), - goals: monkDB.get("goals"), - "user-goals": monkDB.get("user-goals"), - milestones: monkDB.get("milestones"), - "user-milestones": monkDB.get("user-milestones"), - users: monkDB.get("users"), - imports: monkDB.get("imports"), - "import-timings": monkDB.get("import-timings"), - notifications: monkDB.get("notifications"), - sessions: monkDB.get("sessions"), - "iidx-bpi-data": monkDB.get("iidx-bpi-data"), - invites: monkDB.get("invites"), - counters: monkDB.get("counters"), - "iidx-eam-scores": monkDB.get("iidx-eam-scores"), - "game-stats": monkDB.get("game-stats"), - "kai-auth-tokens": monkDB.get("kai-auth-tokens"), - "bms-course-lookup": monkDB.get("bms-course-lookup"), - "api-tokens": monkDB.get("api-tokens"), - "orphan-scores": monkDB.get("orphan-scores"), - "import-locks": monkDB.get("import-locks"), + // i have to handwrite this out for TS... :( + // dont worry, it was all macro'd + songs, + charts, + scores: monkDB.get("scores"), + tierlists: monkDB.get("tierlists"), + "tierlist-data": monkDB.get>("tierlist-data"), + "personal-bests": monkDB.get("personal-bests"), + folders: monkDB.get("folders"), + "folder-chart-lookup": monkDB.get("folder-chart-lookup"), + goals: monkDB.get("goals"), + "user-goals": monkDB.get("user-goals"), + milestones: monkDB.get("milestones"), + "user-milestones": monkDB.get("user-milestones"), + users: monkDB.get("users"), + imports: monkDB.get("imports"), + "import-timings": monkDB.get("import-timings"), + notifications: monkDB.get("notifications"), + sessions: monkDB.get("sessions"), + "iidx-bpi-data": monkDB.get("iidx-bpi-data"), + invites: monkDB.get("invites"), + counters: monkDB.get("counters"), + "iidx-eam-scores": monkDB.get("iidx-eam-scores"), + "game-stats": monkDB.get("game-stats"), + "kai-auth-tokens": monkDB.get("kai-auth-tokens"), + "bms-course-lookup": monkDB.get("bms-course-lookup"), + "api-tokens": monkDB.get("api-tokens"), + "orphan-scores": monkDB.get("orphan-scores"), + "import-locks": monkDB.get("import-locks"), }; export default db; diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index 0cc9b0770..23f3de4c9 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -8,132 +8,132 @@ import { CONF_INFO } from "../../lib/setup/config"; const logger = CreateLogCtx(__filename); interface Index { - fields: Record; - options?: IndexOptions; + fields: Record; + options?: IndexOptions; } function index(fields: Record, options?: IndexOptions) { - return { fields, options }; + return { fields, options }; } const UNIQUE = { unique: true }; const staticIndexes: Partial> = { - scores: [index({ scoreID: 1 }, UNIQUE)], - "personal-bests": [ - index({ chartID: 1, userID: 1 }, UNIQUE), - index({ chartID: 1, "scoreData.percent": 1 }), - ], - sessions: [ - // lol - index({ timeStarted: 1, timeEnded: 1, userID: 1, game: 1, playtype: 1 }), - index({ name: "text" }), - ], - "game-stats": [index({ userID: 1, game: 1, playtype: 1 }, UNIQUE)], - "folder-chart-lookup": [index({ chartID: 1, folderID: 1 }, UNIQUE)], - "tierlist-data": [ - index({ tierlistDataID: 1 }, UNIQUE), - index({ chartID: 1, tierlistID: 1 }), - index({ chartID: 1, tierlistID: 1, type: 1 }), - index({ chartID: 1, tierlistID: 1, type: 1, key: 1 }, UNIQUE), - ], - goals: [index({ goalID: 1 }, UNIQUE)], - "user-goals": [index({ goalID: 1, userID: 1 }, UNIQUE), index({ goalID: 1 })], - milestones: [index({ milestoneID: 1 }, UNIQUE), index({ group: 1, game: 1, playtype: 1 })], - "user-milestones": [ - index({ milestoneID: 1, userID: 1 }, UNIQUE), - index({ userID: 1, game: 1, playtype: 1 }), - ], - imports: [index({ importID: 1 }, UNIQUE)], - "import-timings": [ - index({ importID: 1 }, UNIQUE), - index({ timestamp: 1 }), - index({ total: 1 }), - ], - users: [index({ id: 1 }, UNIQUE)], - tierlists: [ - index({ tierlistID: 1 }, UNIQUE), - index({ game: 1, playtype: 1, isDefault: 1 }, UNIQUE), - ], - folders: [ - index({ folderID: 1 }, UNIQUE), - index({ game: 1, playtype: 1 }), - index({ game: 1, playtype: 1, table: 1 }), - index({ game: 1, playtype: 1, table: 1, tableIndex: 1 }), - ], - "kai-auth-tokens": [index({ userID: 1, service: 1 }, UNIQUE)], - "charts-iidx": [ - index( - { "data.arcChartID": 1 }, - { unique: true, partialFilterExpression: { "data.arcChartID": { $type: "string" } } } - ), - index({ "data.hashSHA256": 1 }), - ], - "bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)], - "api-tokens": [index({ token: 1 }, UNIQUE)], + scores: [index({ scoreID: 1 }, UNIQUE)], + "personal-bests": [ + index({ chartID: 1, userID: 1 }, UNIQUE), + index({ chartID: 1, "scoreData.percent": 1 }), + ], + sessions: [ + // lol + index({ timeStarted: 1, timeEnded: 1, userID: 1, game: 1, playtype: 1 }), + index({ name: "text" }), + ], + "game-stats": [index({ userID: 1, game: 1, playtype: 1 }, UNIQUE)], + "folder-chart-lookup": [index({ chartID: 1, folderID: 1 }, UNIQUE)], + "tierlist-data": [ + index({ tierlistDataID: 1 }, UNIQUE), + index({ chartID: 1, tierlistID: 1 }), + index({ chartID: 1, tierlistID: 1, type: 1 }), + index({ chartID: 1, tierlistID: 1, type: 1, key: 1 }, UNIQUE), + ], + goals: [index({ goalID: 1 }, UNIQUE)], + "user-goals": [index({ goalID: 1, userID: 1 }, UNIQUE), index({ goalID: 1 })], + milestones: [index({ milestoneID: 1 }, UNIQUE), index({ group: 1, game: 1, playtype: 1 })], + "user-milestones": [ + index({ milestoneID: 1, userID: 1 }, UNIQUE), + index({ userID: 1, game: 1, playtype: 1 }), + ], + imports: [index({ importID: 1 }, UNIQUE)], + "import-timings": [ + index({ importID: 1 }, UNIQUE), + index({ timestamp: 1 }), + index({ total: 1 }), + ], + users: [index({ id: 1 }, UNIQUE)], + tierlists: [ + index({ tierlistID: 1 }, UNIQUE), + index({ game: 1, playtype: 1, isDefault: 1 }, UNIQUE), + ], + folders: [ + index({ folderID: 1 }, UNIQUE), + index({ game: 1, playtype: 1 }), + index({ game: 1, playtype: 1, table: 1 }), + index({ game: 1, playtype: 1, table: 1, tableIndex: 1 }), + ], + "kai-auth-tokens": [index({ userID: 1, service: 1 }, UNIQUE)], + "charts-iidx": [ + index( + { "data.arcChartID": 1 }, + { unique: true, partialFilterExpression: { "data.arcChartID": { $type: "string" } } } + ), + index({ "data.hashSHA256": 1 }), + ], + "bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)], + "api-tokens": [index({ token: 1 }, UNIQUE)], }; const indexes: Partial> = staticIndexes; for (const game of CONF_INFO.SUPPORTED_GAMES) { - if (indexes[`charts-${game}` as ValidDatabases]) { - indexes[`charts-${game}` as ValidDatabases]!.push( - index({ chartID: 1 }, UNIQUE), - index( - { songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, - { unique: true, partialFilterExpression: { isPrimary: { $eq: true } } } - ) - ); - } else { - indexes[`charts-${game}` as ValidDatabases] = [ - index({ chartID: 1 }, UNIQUE), - index({ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, UNIQUE), - ]; - } + if (indexes[`charts-${game}` as ValidDatabases]) { + indexes[`charts-${game}` as ValidDatabases]!.push( + index({ chartID: 1 }, UNIQUE), + index( + { songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, + { unique: true, partialFilterExpression: { isPrimary: { $eq: true } } } + ) + ); + } else { + indexes[`charts-${game}` as ValidDatabases] = [ + index({ chartID: 1 }, UNIQUE), + index({ songID: 1, difficulty: 1, playtype: 1, isPrimary: 1 }, UNIQUE), + ]; + } - if (indexes[`songs-${game}` as ValidDatabases]) { - indexes[`songs-${game}` as ValidDatabases]!.push( - index({ id: 1 }, UNIQUE), - index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }) - ); - } else { - indexes[`songs-${game}` as ValidDatabases] = [ - index({ id: 1 }, UNIQUE), - index({ title: 1 }), - index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }), - ]; - } + if (indexes[`songs-${game}` as ValidDatabases]) { + indexes[`songs-${game}` as ValidDatabases]!.push( + index({ id: 1 }, UNIQUE), + index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }) + ); + } else { + indexes[`songs-${game}` as ValidDatabases] = [ + index({ id: 1 }, UNIQUE), + index({ title: 1 }), + index({ title: "text", artist: "text", "alt-titles": "text", "search-titles": "text" }), + ]; + } } export async function SetIndexes(mongoURL: string, reset: boolean) { - const db = monk(mongoURL); + const db = monk(mongoURL); - logger.debug(`Starting indexing for ${mongoURL}...`); + logger.debug(`Starting indexing for ${mongoURL}...`); - const collections = (await db.listCollections()).map((e) => e.name); + const collections = (await db.listCollections()).map((e) => e.name); - for (const collection in indexes) { - if (!collections.includes(collection)) { - // this creates a collection, i cant find the createCollection - // call. - await db.get(collection).insert({}); - await db.get(collection).remove({}); - } + for (const collection in indexes) { + if (!collections.includes(collection)) { + // this creates a collection, i cant find the createCollection + // call. + await db.get(collection).insert({}); + await db.get(collection).remove({}); + } - if (reset) { - await db.get(collection).dropIndexes(); - logger.debug(`Reset ${collection}.`); - } + if (reset) { + await db.get(collection).dropIndexes(); + logger.debug(`Reset ${collection}.`); + } - // @ts-expect-error dru(n)kts - for (const index of indexes[collection]) { - const r = await db.get(collection).createIndex(index.fields, index.options); + // @ts-expect-error dru(n)kts + for (const index of indexes[collection]) { + const r = await db.get(collection).createIndex(index.fields, index.options); - logger.debug(r); - } - } + logger.debug(r); + } + } - logger.debug("Done."); + logger.debug("Done."); - await db.close(); + await db.close(); } diff --git a/server/src/external/mongo/schemas.ts b/server/src/external/mongo/schemas.ts index e78b6781c..9c2d44dcb 100644 --- a/server/src/external/mongo/schemas.ts +++ b/server/src/external/mongo/schemas.ts @@ -1,12 +1,12 @@ import deepmerge from "deepmerge"; import { Game, Playtypes } from "tachi-common"; import { - grades, - importTypes, - lamps, - validHitData, - validDifficulties, - validPlaytypes, + grades, + importTypes, + lamps, + validHitData, + validDifficulties, + validPlaytypes, } from "tachi-common/js/config"; import p, { PrudenceSchema, ValidSchemaValue } from "prudence"; import { CONF_INFO } from "../../lib/setup/config"; @@ -14,99 +14,99 @@ import { CONF_INFO } from "../../lib/setup/config"; const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u; export const PRUDENCE_PUBLIC_USER: PrudenceSchema = { - _id: p.any, - username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - usernameLowercase: (self, parent) => self === (parent!.username as string).toLowerCase(), - id: p.isPositiveInteger, - settings: { - nsfwSplashes: "boolean", - invisible: "boolean", - }, - friends: [p.isPositiveInteger], - socialMedia: { - discord: "*?string", - twitter: "*?string", - github: "*?string", - steam: "*?string", - youtube: "*?string", - twitch: "*?string", - }, - about: p.isBoundedString(0, 4000), - customPfp: "boolean", - customBanner: "boolean", - permissions: { - admin: "*boolean", - }, - clan: p.nullable(p.isBoundedString(1, 4)), - lastSeen: p.nullable(p.isPositiveInteger), + _id: p.any, + username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), + usernameLowercase: (self, parent) => self === (parent!.username as string).toLowerCase(), + id: p.isPositiveInteger, + settings: { + nsfwSplashes: "boolean", + invisible: "boolean", + }, + friends: [p.isPositiveInteger], + socialMedia: { + discord: "*?string", + twitter: "*?string", + github: "*?string", + steam: "*?string", + youtube: "*?string", + twitch: "*?string", + }, + about: p.isBoundedString(0, 4000), + customPfp: "boolean", + customBanner: "boolean", + permissions: { + admin: "*boolean", + }, + clan: p.nullable(p.isBoundedString(1, 4)), + lastSeen: p.nullable(p.isPositiveInteger), }; export const PRUDENCE_PRIVATE_USER = Object.assign( - { - password: "string", // could be a tighter fit related to bcrypt? - email: p.regex(LAZY_EMAIL_REGEX), - }, - PRUDENCE_PUBLIC_USER + { + password: "string", // could be a tighter fit related to bcrypt? + email: p.regex(LAZY_EMAIL_REGEX), + }, + PRUDENCE_PUBLIC_USER ); export const PRUDENCE_IIDX_BPI_DATA = { - coef: p.nullable(p.isPositiveNonZero), - kavg: p.isPositiveNonZeroInteger, - wr: p.isPositiveNonZeroInteger, - chartID: "string", - kesd: p.isPositiveNonZero, + coef: p.nullable(p.isPositiveNonZero), + kavg: p.isPositiveNonZeroInteger, + wr: p.isPositiveNonZeroInteger, + chartID: "string", + kesd: p.isPositiveNonZero, }; export const PRUDENCE_COUNTER = { - counterName: "string", - value: p.isPositiveNonZeroInteger, // is nonzero? + counterName: "string", + value: p.isPositiveNonZeroInteger, // is nonzero? }; export const PR_SCORE_GENERIC = { - service: p.isBoundedString(3, 64), - game: "string", - playtype: "string", - difficulty: "string", - userID: p.isPositiveNonZeroInteger, - scoreData: { - score: p.isPositive, - percent: p.isBetween(0, 100), // could be overrode! - lamp: "string", - grade: "string", - lampIndex: p.isPositiveInteger, - gradeIndex: p.isPositiveInteger, - hitData: {}, - hitMeta: {}, - }, - scoreMeta: {}, - calculatedData: { - rating: p.isPositive, - lampRating: p.isPositive, - gameSpecific: {}, - // ranking: p.nullable(p.isPositiveNonZeroInteger), - // outOf: p.nullable( - // p.and( - // p.isPositiveNonZeroInteger, - // (self, parent: Record) => - // (parent.ranking as number) <= (self as number) - // ) - // ), - }, - timeAchieved: p.nullable(p.isPositive), - songID: p.isInteger, - chartID: (self: unknown) => typeof self === "string" && self.length === 40, - highlight: "boolean", - isPrimary: "boolean", - comment: p.nullable(p.isBoundedString(1, 240)), - timeAdded: p.isPositive, - scoreID: "string", // temp - importType: p.nullable(p.isIn(importTypes)), + service: p.isBoundedString(3, 64), + game: "string", + playtype: "string", + difficulty: "string", + userID: p.isPositiveNonZeroInteger, + scoreData: { + score: p.isPositive, + percent: p.isBetween(0, 100), // could be overrode! + lamp: "string", + grade: "string", + lampIndex: p.isPositiveInteger, + gradeIndex: p.isPositiveInteger, + hitData: {}, + hitMeta: {}, + }, + scoreMeta: {}, + calculatedData: { + rating: p.isPositive, + lampRating: p.isPositive, + gameSpecific: {}, + // ranking: p.nullable(p.isPositiveNonZeroInteger), + // outOf: p.nullable( + // p.and( + // p.isPositiveNonZeroInteger, + // (self, parent: Record) => + // (parent.ranking as number) <= (self as number) + // ) + // ), + }, + timeAchieved: p.nullable(p.isPositive), + songID: p.isInteger, + chartID: (self: unknown) => typeof self === "string" && self.length === 40, + highlight: "boolean", + isPrimary: "boolean", + comment: p.nullable(p.isBoundedString(1, 240)), + timeAdded: p.isPositive, + scoreID: "string", // temp + importType: p.nullable(p.isIn(importTypes)), }; type ScoreSchemas = { - [G in Game]: { - [P in Playtypes[G]]: PrudenceSchema; - }; + [G in Game]: { + [P in Playtypes[G]]: PrudenceSchema; + }; }; const optionalPositiveInt = p.optional(p.isPositiveInteger); @@ -116,315 +116,315 @@ const nullableAndOptional = (fn: ValidSchemaValue) => p.optional(p.nullable(fn)) const nullableAndOptionalPosInt = nullableAndOptional(p.isPositiveInteger); const CreateGameScoreData = (game: Game, hitMetaMerge: PrudenceSchema) => ({ - lamp: p.isIn(lamps[game]), - lampIndex: p.and( - p.isPositiveInteger, - (self, parent) => lamps[game][self as number] === parent.lamp - ), - grade: p.isIn(grades[game]), - gradeIndex: p.and( - p.isPositiveInteger, - (self, parent) => grades[game][self as number] === parent.grade - ), - hitData: Object.fromEntries(validHitData[game].map((e) => [e, optionalPositiveInt])), - hitMeta: deepmerge( - { - fast: nullableAndOptionalPosInt, - slow: nullableAndOptionalPosInt, - maxCombo: nullableAndOptionalPosInt, - }, - hitMetaMerge - ), - esd: p.nullable(p.isPositive), + lamp: p.isIn(lamps[game]), + lampIndex: p.and( + p.isPositiveInteger, + (self, parent) => lamps[game][self as number] === parent.lamp + ), + grade: p.isIn(grades[game]), + gradeIndex: p.and( + p.isPositiveInteger, + (self, parent) => grades[game][self as number] === parent.grade + ), + hitData: Object.fromEntries(validHitData[game].map((e) => [e, optionalPositiveInt])), + hitMeta: deepmerge( + { + fast: nullableAndOptionalPosInt, + slow: nullableAndOptionalPosInt, + maxCombo: nullableAndOptionalPosInt, + }, + hitMetaMerge + ), + esd: p.nullable(p.isPositive), }); const PR_SCORE_IIDX_SP: PrudenceSchema = CreatePRScore( - "iidx", - "SP", - { - bp: nullableAndOptional(p.isPositiveInteger), - gauge: nullableAndOptional(p.isBetween(0, 100)), - gaugeHistory: p.optional([nullableAndOptional(p.isBetween(0, 100))]), - comboBreak: optionalPositiveInt, - deadMeasure: nullableAndOptional(p.isPositiveInteger), - deadNote: nullableAndOptional(p.isPositiveInteger), - }, - { - random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])), - gauge: nullableAndOptional(p.isIn(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"])), - assist: nullableAndOptional( - p.isIn(["NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "ASCR + LEGACY"]) - ), - range: nullableAndOptional( - p.isIn(["NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+"]) - ), - pacemaker: "*?string", // lazy - pacemakerName: "*?string", - pacemakerTarget: nullableAndOptional(p.isPositiveInteger), - }, - { - BPI: "?number", - "K%": p.nullable(p.isBetween(0, 100)), - } + "iidx", + "SP", + { + bp: nullableAndOptional(p.isPositiveInteger), + gauge: nullableAndOptional(p.isBetween(0, 100)), + gaugeHistory: p.optional([nullableAndOptional(p.isBetween(0, 100))]), + comboBreak: optionalPositiveInt, + deadMeasure: nullableAndOptional(p.isPositiveInteger), + deadNote: nullableAndOptional(p.isPositiveInteger), + }, + { + random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])), + gauge: nullableAndOptional(p.isIn(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"])), + assist: nullableAndOptional( + p.isIn(["NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "ASCR + LEGACY"]) + ), + range: nullableAndOptional( + p.isIn(["NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+"]) + ), + pacemaker: "*?string", // lazy + pacemakerName: "*?string", + pacemakerTarget: nullableAndOptional(p.isPositiveInteger), + }, + { + BPI: "?number", + "K%": p.nullable(p.isBetween(0, 100)), + } ); const PR_SCORE_BMS_7K: PrudenceSchema = CreatePRScore( - "bms", - "7K", - { - gauge: nullableAndOptional(p.isBetween(0, 100)), - bp: nullableAndOptionalPosInt, - diedAt: nullableAndOptionalPosInt, - lpr: nullableAndOptionalPosInt, - lbd: nullableAndOptionalPosInt, - lgd: nullableAndOptionalPosInt, - lgr: nullableAndOptionalPosInt, - lpg: nullableAndOptionalPosInt, - epr: nullableAndOptionalPosInt, - ebd: nullableAndOptionalPosInt, - egd: nullableAndOptionalPosInt, - egr: nullableAndOptionalPosInt, - epg: nullableAndOptionalPosInt, - }, - { - random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])), - inputDevice: nullableAndOptional(p.isIn(["KEYBOARD", "BM_CONTROLLER", "MIDI"])), - } + "bms", + "7K", + { + gauge: nullableAndOptional(p.isBetween(0, 100)), + bp: nullableAndOptionalPosInt, + diedAt: nullableAndOptionalPosInt, + lpr: nullableAndOptionalPosInt, + lbd: nullableAndOptionalPosInt, + lgd: nullableAndOptionalPosInt, + lgr: nullableAndOptionalPosInt, + lpg: nullableAndOptionalPosInt, + epr: nullableAndOptionalPosInt, + ebd: nullableAndOptionalPosInt, + egd: nullableAndOptionalPosInt, + egr: nullableAndOptionalPosInt, + epg: nullableAndOptionalPosInt, + }, + { + random: nullableAndOptional(p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])), + inputDevice: nullableAndOptional(p.isIn(["KEYBOARD", "BM_CONTROLLER", "MIDI"])), + } ); function CreatePRScore( - game: G, - playtype: Playtypes[G], - mergeHitMeta: PrudenceSchema = {}, - mergeScoreMeta: PrudenceSchema = {}, - mergeGameSpecific: PrudenceSchema = {} + game: G, + playtype: Playtypes[G], + mergeHitMeta: PrudenceSchema = {}, + mergeScoreMeta: PrudenceSchema = {}, + mergeGameSpecific: PrudenceSchema = {} ) { - return deepmerge(PR_SCORE_GENERIC, { - game: p.equalTo(game), - playtype: p.equalTo(playtype), - scoreData: CreateGameScoreData(game, mergeHitMeta), - scoreMeta: mergeScoreMeta, - calculatedData: { - gameSpecific: mergeGameSpecific, - }, - }); + return deepmerge(PR_SCORE_GENERIC, { + game: p.equalTo(game), + playtype: p.equalTo(playtype), + scoreData: CreateGameScoreData(game, mergeHitMeta), + scoreMeta: mergeScoreMeta, + calculatedData: { + gameSpecific: mergeGameSpecific, + }, + }); } function DoublePlayTwinRandomTuple(self: unknown) { - if (!Array.isArray(self)) { - return "Expected an array."; - } + if (!Array.isArray(self)) { + return "Expected an array."; + } - if (self.length !== 2) { - return "Expected exactly 2 elements."; - } + if (self.length !== 2) { + return "Expected exactly 2 elements."; + } - const ls = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[0]); + const ls = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[0]); - if (ls !== true) { - return ls; - } + if (ls !== true) { + return ls; + } - const rs = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[1]); + const rs = p.isIn(["NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"])(self[1]); - return rs; + return rs; } export const PRUDENCE_SCORE_SCHEMAS: ScoreSchemas = { - iidx: { - SP: PR_SCORE_IIDX_SP, - DP: deepmerge(PR_SCORE_IIDX_SP, { - playtype: p.equalTo("DP"), - scoreMeta: { - random: nullableAndOptional(DoublePlayTwinRandomTuple), - }, - calculatedData: { - gameSpecific: { - "K%": "undefined", // remove with override - }, - }, - }), - }, - sdvx: { - Single: CreatePRScore( - "sdvx", - "Single", - { - gauge: p.optional(p.isBetween(0, 100)), - btnRate: p.optional(p.isBetween(0, 100)), - holdRate: p.optional(p.isBetween(0, 100)), - laserRate: p.optional(p.isBetween(0, 100)), - }, - {}, - { - VF4: p.nullable(p.isPositiveInteger), - VF5: p.nullable(p.isBetween(0, 0.5)), // hack - } - ), - }, - bms: { - "7K": PR_SCORE_BMS_7K, - "14K": deepmerge(PR_SCORE_BMS_7K, { - playtype: p.equalTo("14K"), - scoreMeta: { - random: p.optional(DoublePlayTwinRandomTuple), - }, - }), - }, - chunithm: { - Single: deepmerge(CreatePRScore("chunithm", "Single"), { - scoreData: { percent: p.isBetween(0, 101) }, - }), - }, - ddr: { - SP: CreatePRScore( - "ddr", - "SP", - {}, - {}, - { - MFCP: p.nullable(p.isBoundedInteger(1, 25)), - } - ), - DP: CreatePRScore( - "ddr", - "DP", - {}, - {}, - { - MFCP: p.nullable(p.isBoundedInteger(1, 25)), - } - ), - }, - gitadora: { - Gita: CreatePRScore("gitadora", "Gita"), - Dora: CreatePRScore("gitadora", "Dora"), - }, - jubeat: { - Single: CreatePRScore("jubeat", "Single"), - }, - maimai: { - Single: deepmerge(CreatePRScore("maimai", "Single"), { - scoreData: { percent: p.isBetween(0, 150) }, - }), - }, - museca: { - Single: CreatePRScore("museca", "Single"), - }, - popn: { - "9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }), - }, - usc: { - Single: CreatePRScore("usc", "Single", { gauge: p.optional(p.isBetween(0, 100)) }), - }, + iidx: { + SP: PR_SCORE_IIDX_SP, + DP: deepmerge(PR_SCORE_IIDX_SP, { + playtype: p.equalTo("DP"), + scoreMeta: { + random: nullableAndOptional(DoublePlayTwinRandomTuple), + }, + calculatedData: { + gameSpecific: { + "K%": "undefined", // remove with override + }, + }, + }), + }, + sdvx: { + Single: CreatePRScore( + "sdvx", + "Single", + { + gauge: p.optional(p.isBetween(0, 100)), + btnRate: p.optional(p.isBetween(0, 100)), + holdRate: p.optional(p.isBetween(0, 100)), + laserRate: p.optional(p.isBetween(0, 100)), + }, + {}, + { + VF4: p.nullable(p.isPositiveInteger), + VF5: p.nullable(p.isBetween(0, 0.5)), // hack + } + ), + }, + bms: { + "7K": PR_SCORE_BMS_7K, + "14K": deepmerge(PR_SCORE_BMS_7K, { + playtype: p.equalTo("14K"), + scoreMeta: { + random: p.optional(DoublePlayTwinRandomTuple), + }, + }), + }, + chunithm: { + Single: deepmerge(CreatePRScore("chunithm", "Single"), { + scoreData: { percent: p.isBetween(0, 101) }, + }), + }, + ddr: { + SP: CreatePRScore( + "ddr", + "SP", + {}, + {}, + { + MFCP: p.nullable(p.isBoundedInteger(1, 25)), + } + ), + DP: CreatePRScore( + "ddr", + "DP", + {}, + {}, + { + MFCP: p.nullable(p.isBoundedInteger(1, 25)), + } + ), + }, + gitadora: { + Gita: CreatePRScore("gitadora", "Gita"), + Dora: CreatePRScore("gitadora", "Dora"), + }, + jubeat: { + Single: CreatePRScore("jubeat", "Single"), + }, + maimai: { + Single: deepmerge(CreatePRScore("maimai", "Single"), { + scoreData: { percent: p.isBetween(0, 150) }, + }), + }, + museca: { + Single: CreatePRScore("museca", "Single"), + }, + popn: { + "9B": CreatePRScore("popn", "9B", { gauge: p.optional(p.isBetween(0, 100)) }), + }, + usc: { + Single: CreatePRScore("usc", "Single", { gauge: p.optional(p.isBetween(0, 100)) }), + }, }; const PRUDENCE_CHART_BASE: PrudenceSchema = { - chartID: "string", - rgcID: "?string", - songID: p.isPositiveInteger, - level: "string", - levelNum: p.isPositive, - isPrimary: "boolean", - versions: ["string"], + chartID: "string", + rgcID: "?string", + songID: p.isPositiveInteger, + level: "string", + levelNum: p.isPositive, + isPrimary: "boolean", + versions: ["string"], }; function CreatePrChart(game: Game, flags: string[], data: PrudenceSchema) { - return deepmerge(PRUDENCE_CHART_BASE, { - difficulty: p.isIn(validDifficulties[game]), - playtype: p.isIn(validPlaytypes[game]), - flags: Object.fromEntries(flags.map((e) => [e, "boolean"])), - data, - }); + return deepmerge(PRUDENCE_CHART_BASE, { + difficulty: p.isIn(validDifficulties[game]), + playtype: p.isIn(validPlaytypes[game]), + flags: Object.fromEntries(flags.map((e) => [e, "boolean"])), + data, + }); } export const PRUDENCE_CHART_SCHEMAS: Record = { - iidx: CreatePrChart("iidx", ["IN BASE GAME", "OMNIMIX", "N-1"], { - notecount: p.isPositiveInteger, - inGameID: p.isPositiveInteger, - arcChartID: "?string", - }), - bms: CreatePrChart("bms", [], { hashMD5: "string", hashSHA256: "string" }), - chunithm: CreatePrChart("chunithm", ["IN BASE GAME", "OMNIMIX"], { - inGameID: p.isPositiveInteger, - }), - ddr: CreatePrChart("ddr", ["IN BASE GAME", "N-1"], { - songHash: "string", - inGameID: "string", - }), - gitadora: CreatePrChart("gitadora", ["IN BASE GAME", "OMNIMIX", "HOT N-1", "HOT"], { - inGameID: p.isPositiveInteger, - }), - jubeat: CreatePrChart("jubeat", ["IN BASE GAME"], {}), - maimai: CreatePrChart("maimai", ["IN BASE GAME"], { - maxPercent: p.gte(100), - inGameID: "string", - }), - museca: CreatePrChart("museca", ["IN BASE GAME", "OMNIMIX"], {}), - popn: CreatePrChart("popn", ["IN BASE GAME", "OMNIMIX"], {}), - sdvx: CreatePrChart("sdvx", ["IN BASE GAME", "OMNIMIX", "N-1"], { - inGameID: p.isPositiveInteger, - }), - usc: CreatePrChart("usc", ["CUSTOM"], { - hashSHA256: "string", - }), + iidx: CreatePrChart("iidx", ["IN BASE GAME", "OMNIMIX", "N-1"], { + notecount: p.isPositiveInteger, + inGameID: p.isPositiveInteger, + arcChartID: "?string", + }), + bms: CreatePrChart("bms", [], { hashMD5: "string", hashSHA256: "string" }), + chunithm: CreatePrChart("chunithm", ["IN BASE GAME", "OMNIMIX"], { + inGameID: p.isPositiveInteger, + }), + ddr: CreatePrChart("ddr", ["IN BASE GAME", "N-1"], { + songHash: "string", + inGameID: "string", + }), + gitadora: CreatePrChart("gitadora", ["IN BASE GAME", "OMNIMIX", "HOT N-1", "HOT"], { + inGameID: p.isPositiveInteger, + }), + jubeat: CreatePrChart("jubeat", ["IN BASE GAME"], {}), + maimai: CreatePrChart("maimai", ["IN BASE GAME"], { + maxPercent: p.gte(100), + inGameID: "string", + }), + museca: CreatePrChart("museca", ["IN BASE GAME", "OMNIMIX"], {}), + popn: CreatePrChart("popn", ["IN BASE GAME", "OMNIMIX"], {}), + sdvx: CreatePrChart("sdvx", ["IN BASE GAME", "OMNIMIX", "N-1"], { + inGameID: p.isPositiveInteger, + }), + usc: CreatePrChart("usc", ["CUSTOM"], { + hashSHA256: "string", + }), }; const PRUDENCE_SONG_BASE: PrudenceSchema = { - id: p.isPositiveInteger, - title: "string", - artist: "string", - "search-titles": [p.and("string", (self, parent) => self !== parent.title)], - "alt-titles": [p.and("string", (self, parent) => self !== parent.title)], - firstVersion: "string", + id: p.isPositiveInteger, + title: "string", + artist: "string", + "search-titles": [p.and("string", (self, parent) => self !== parent.title)], + "alt-titles": [p.and("string", (self, parent) => self !== parent.title)], + firstVersion: "string", }; function CreatePrSong(data: PrudenceSchema) { - return deepmerge(PRUDENCE_SONG_BASE, { - data, - }); + return deepmerge(PRUDENCE_SONG_BASE, { + data, + }); } const PRUDENCE_SONG_WITH_GENRE = CreatePrSong({ genre: "string" }); export const PRUDENCE_SONG_SCHEMAS: Record = { - iidx: PRUDENCE_SONG_WITH_GENRE, - bms: PRUDENCE_SONG_WITH_GENRE, - chunithm: PRUDENCE_SONG_WITH_GENRE, - ddr: CreatePrSong({}), - gitadora: CreatePrSong({}), - jubeat: CreatePrSong({}), - maimai: CreatePrSong({ titleJP: "string", artistJP: "string", genre: "string" }), - museca: CreatePrSong({ titleJP: "string", artistJP: "string" }), - popn: PRUDENCE_SONG_WITH_GENRE, - sdvx: CreatePrSong({ uscEquiv: p.nullable(p.isPositiveInteger) }), - usc: CreatePrSong({ sdvxEquiv: p.nullable(p.isPositiveInteger) }), + iidx: PRUDENCE_SONG_WITH_GENRE, + bms: PRUDENCE_SONG_WITH_GENRE, + chunithm: PRUDENCE_SONG_WITH_GENRE, + ddr: CreatePrSong({}), + gitadora: CreatePrSong({}), + jubeat: CreatePrSong({}), + maimai: CreatePrSong({ titleJP: "string", artistJP: "string", genre: "string" }), + museca: CreatePrSong({ titleJP: "string", artistJP: "string" }), + popn: PRUDENCE_SONG_WITH_GENRE, + sdvx: CreatePrSong({ uscEquiv: p.nullable(p.isPositiveInteger) }), + usc: CreatePrSong({ sdvxEquiv: p.nullable(p.isPositiveInteger) }), }; const PRUDENCE_FOLDER_CHART_LOOKUP: PrudenceSchema = { - chartID: "string", - folderID: "string", + chartID: "string", + folderID: "string", }; const PRUDENCE_FOLDER: PrudenceSchema = { - title: "string", - game: p.isIn(CONF_INFO.SUPPORTED_GAMES), - playtype: (self, parent) => p.isIn(validPlaytypes[parent.game as Game])(self), - folderID: "string", - table: "string", - tableIndex: "number", - type: p.isIn("songs", "charts", "static"), - data: (self, parent) => { - if (parent.type === "static") { - return Array.isArray(self) && self.every((e) => typeof e === "string"); - } + title: "string", + game: p.isIn(CONF_INFO.SUPPORTED_GAMES), + playtype: (self, parent) => p.isIn(validPlaytypes[parent.game as Game])(self), + folderID: "string", + table: "string", + tableIndex: "number", + type: p.isIn("songs", "charts", "static"), + data: (self, parent) => { + if (parent.type === "static") { + return Array.isArray(self) && self.every((e) => typeof e === "string"); + } - // this is a temp hackjob, it should technically - // check whether this document matches a - // partialised chart/song doc - // but.. can be done later. - return typeof self === "object" && !!self; // whatever - }, + // this is a temp hackjob, it should technically + // check whether this document matches a + // partialised chart/song doc + // but.. can be done later. + return typeof self === "object" && !!self; // whatever + }, }; /** @@ -433,11 +433,11 @@ const PRUDENCE_FOLDER: PrudenceSchema = { * being different depending on the score.game field. */ export const STATIC_SCHEMAS = { - users: PRUDENCE_PRIVATE_USER, - "iidx-bpi-data": PRUDENCE_IIDX_BPI_DATA, - counters: PRUDENCE_COUNTER, - charts: PRUDENCE_CHART_SCHEMAS, - songs: PRUDENCE_SONG_SCHEMAS, - "folder-chart-lookup": PRUDENCE_FOLDER_CHART_LOOKUP, - folders: PRUDENCE_FOLDER, + users: PRUDENCE_PRIVATE_USER, + "iidx-bpi-data": PRUDENCE_IIDX_BPI_DATA, + counters: PRUDENCE_COUNTER, + charts: PRUDENCE_CHART_SCHEMAS, + songs: PRUDENCE_SONG_SCHEMAS, + "folder-chart-lookup": PRUDENCE_FOLDER_CHART_LOOKUP, + folders: PRUDENCE_FOLDER, }; diff --git a/server/src/external/redis/redis-IPC.test.ts b/server/src/external/redis/redis-IPC.test.ts index cca61f296..fa6c47617 100644 --- a/server/src/external/redis/redis-IPC.test.ts +++ b/server/src/external/redis/redis-IPC.test.ts @@ -3,14 +3,14 @@ import t from "tap"; import { CloseRedisPubSub, RedisPub, RedisSub } from "./redis-IPC"; t.test("Basic PUB/SUB testing", (t) => { - t.setTimeout(2000); - RedisSub("test" as any, (d) => { - t.equal(d.userID, 1); - t.pass(); - t.end(); - }); + t.setTimeout(2000); + RedisSub("test" as any, (d) => { + t.equal(d.userID, 1); + t.pass(); + t.end(); + }); - RedisPub("test" as any, { userID: 1 } as any); + RedisPub("test" as any, { userID: 1 } as any); }); t.teardown(CloseRedisPubSub); diff --git a/server/src/external/redis/redis-IPC.ts b/server/src/external/redis/redis-IPC.ts index 05253c4ea..6a99e5c72 100644 --- a/server/src/external/redis/redis-IPC.ts +++ b/server/src/external/redis/redis-IPC.ts @@ -8,7 +8,7 @@ const logger = CreateLogCtx(__filename); type RedisSubCallback = (data: RedisIPCData[T]) => void; type SubCallbacks = { - [K in RedisIPCChannels]: RedisSubCallback[]; + [K in RedisIPCChannels]: RedisSubCallback[]; }; const SubCallbacks: Partial = {}; @@ -22,64 +22,64 @@ const PubClient = redis.createClient(); const PREFIX = CONFIG.TYPE.toUpperCase(); // KTCHI or BTCHI export function RedisPub(channel: T, data: RedisIPCData[T]) { - PubClient.publish(`${PREFIX}-${channel}`, JSON.stringify(data)); + PubClient.publish(`${PREFIX}-${channel}`, JSON.stringify(data)); } export function RedisSub(channel: T, callback: RedisSubCallback) { - if (SubCallbacks[channel]) { - // @ts-expect-error It's complaining that the T in channel might be a different T to the T in callback - // this is obviously nonsense. - SubCallbacks[channel]!.push(callback); - logger.debug(`Pushed callback ${callback.name} to channel ${channel}.`); - } else { - // @ts-expect-error see above. - SubCallbacks[channel] = [callback]; - SubClient.subscribe(`${PREFIX}-${channel}`); - logger.debug(`Added first callback ${callback.name} to channel ${channel}.`); - } + if (SubCallbacks[channel]) { + // @ts-expect-error It's complaining that the T in channel might be a different T to the T in callback + // this is obviously nonsense. + SubCallbacks[channel]!.push(callback); + logger.debug(`Pushed callback ${callback.name} to channel ${channel}.`); + } else { + // @ts-expect-error see above. + SubCallbacks[channel] = [callback]; + SubClient.subscribe(`${PREFIX}-${channel}`); + logger.debug(`Added first callback ${callback.name} to channel ${channel}.`); + } } SubClient.on("message", (channel, strData) => { - if (!channel.startsWith(`${PREFIX}-`)) { - return; // not our business - } + if (!channel.startsWith(`${PREFIX}-`)) { + return; // not our business + } - const ktChannel = channel.slice(`${PREFIX}-`.length) as RedisIPCChannels; + const ktChannel = channel.slice(`${PREFIX}-`.length) as RedisIPCChannels; - if (!Object.prototype.hasOwnProperty.call(SubCallbacks, ktChannel)) { - return; // no callbacks to call - } + if (!Object.prototype.hasOwnProperty.call(SubCallbacks, ktChannel)) { + return; // no callbacks to call + } - const jsData = JSON.parse(strData); + const jsData = JSON.parse(strData); - for (const cb of SubCallbacks[ktChannel]!) { - try { - cb(jsData); - } catch (err) { - logger.error(`Error calling callback ${cb.name} for channel ${ktChannel}`, { err }); - } - } + for (const cb of SubCallbacks[ktChannel]!) { + try { + cb(jsData); + } catch (err) { + logger.error(`Error calling callback ${cb.name} for channel ${ktChannel}`, { err }); + } + } }); // Awful... // Function is near-impossible to test. /* istanbul ignore next */ export function CloseRedisPubSub() { - return new Promise((resolve, reject) => { - PubClient.quit((err) => { - if (err) { - logger.crit(`PubClient QUIT error: ${err}`, { err }); - reject(err); - } + return new Promise((resolve, reject) => { + PubClient.quit((err) => { + if (err) { + logger.crit(`PubClient QUIT error: ${err}`, { err }); + reject(err); + } - SubClient.quit((err) => { - if (err) { - logger.crit(`SubClient QUIT error: ${err}`, { err }); - reject(err); - } + SubClient.quit((err) => { + if (err) { + logger.crit(`SubClient QUIT error: ${err}`, { err }); + reject(err); + } - resolve(); - }); - }); - }); + resolve(); + }); + }); + }); } diff --git a/server/src/external/redis/redis.ts b/server/src/external/redis/redis.ts index 4898933d2..0e549e9c1 100644 --- a/server/src/external/redis/redis.ts +++ b/server/src/external/redis/redis.ts @@ -9,20 +9,20 @@ export const RedisClient = redis.createClient(); logger.verbose("Instantiated Redis Client"); function EmitCritical() { - /* istanbul ignore next */ - if (!RedisClient.connected) { - logger.crit(`Could not connect to redis in time. No more information is available.`); - } + /* istanbul ignore next */ + if (!RedisClient.connected) { + logger.crit(`Could not connect to redis in time. No more information is available.`); + } } const ref = setTimeout(EmitCritical, 10000); RedisClient.on("connect", () => { - logger.verbose(`Connected to Redis.`); + logger.verbose(`Connected to Redis.`); - clearTimeout(ref); + clearTimeout(ref); }); export function CloseRedisConnection() { - return RedisClient.quit(); + return RedisClient.quit(); } diff --git a/server/src/lib/achievables/goals.test.ts b/server/src/lib/achievables/goals.test.ts index 5b56004a7..e3c889d55 100644 --- a/server/src/lib/achievables/goals.test.ts +++ b/server/src/lib/achievables/goals.test.ts @@ -6,10 +6,10 @@ import { EvaluateGoalForUser, HumaniseGoalProgress } from "./goals"; import { GoalDocument } from "tachi-common"; import deepmerge from "deepmerge"; import { - HC511Goal, - Testing511SPA, - TestingIIDXFolderSP10, - TestingIIDXSPScorePB, + HC511Goal, + Testing511SPA, + TestingIIDXFolderSP10, + TestingIIDXSPScorePB, } from "../../test-utils/test-data"; import { CreateFolderChartLookup } from "../../utils/folder"; import { CloseAllConnections } from "../../test-utils/close-connections"; @@ -17,292 +17,292 @@ import { CloseAllConnections } from "../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); t.test("#EvaluateGoalForUser", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should correctly evaluate against single goals.", (t) => { - t.beforeEach(async () => { - await db["personal-bests"].insert(TestingIIDXSPScorePB); - delete TestingIIDXSPScorePB._id; - }); + t.test("Should correctly evaluate against single goals.", (t) => { + t.beforeEach(async () => { + await db["personal-bests"].insert(TestingIIDXSPScorePB); + delete TestingIIDXSPScorePB._id; + }); - t.test("Should correctly evaluate goals if user succeeds.", async (t) => { - const res = await EvaluateGoalForUser(HC511Goal, 1, logger); + t.test("Should correctly evaluate goals if user succeeds.", async (t) => { + const res = await EvaluateGoalForUser(HC511Goal, 1, logger); - t.strictSame( - res, - { - achieved: true, - outOf: 5, - progress: 6, - outOfHuman: "HARD CLEAR", - progressHuman: "EX HARD CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user succeeds." - ); + t.strictSame( + res, + { + achieved: true, + outOf: 5, + progress: 6, + outOfHuman: "HARD CLEAR", + progressHuman: "EX HARD CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user succeeds." + ); - t.end(); - }); + t.end(); + }); - t.test("Should correctly evaluate goals if user fails.", async (t) => { - await db["personal-bests"].update( - { userID: 1, chartID: Testing511SPA.chartID }, - { - $set: { - "scoreData.lamp": "CLEAR", - "scoreData.lampIndex": 4, - }, - } - ); + t.test("Should correctly evaluate goals if user fails.", async (t) => { + await db["personal-bests"].update( + { userID: 1, chartID: Testing511SPA.chartID }, + { + $set: { + "scoreData.lamp": "CLEAR", + "scoreData.lampIndex": 4, + }, + } + ); - const res = await EvaluateGoalForUser(HC511Goal, 1, logger); + const res = await EvaluateGoalForUser(HC511Goal, 1, logger); - t.strictSame( - res, - { - achieved: false, - outOf: 5, - progress: 4, - outOfHuman: "HARD CLEAR", - progressHuman: "CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user fails." - ); + t.strictSame( + res, + { + achieved: false, + outOf: 5, + progress: 4, + outOfHuman: "HARD CLEAR", + progressHuman: "CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user fails." + ); - t.end(); - }); + t.end(); + }); - t.test("Should correctly evaluate goals when user has no score.", async (t) => { - await db["personal-bests"].remove({}); + t.test("Should correctly evaluate goals when user has no score.", async (t) => { + await db["personal-bests"].remove({}); - const res = await EvaluateGoalForUser(HC511Goal, 1, logger); + const res = await EvaluateGoalForUser(HC511Goal, 1, logger); - t.strictSame( - res, - { - achieved: false, - outOf: 5, - progress: null, - outOfHuman: "HARD CLEAR", - progressHuman: "NO DATA", - }, - "Should correctly evaluate the goal if the user has no score." - ); + t.strictSame( + res, + { + achieved: false, + outOf: 5, + progress: null, + outOfHuman: "HARD CLEAR", + progressHuman: "NO DATA", + }, + "Should correctly evaluate the goal if the user has no score." + ); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Should correctly evaluate against multi goals.", (t) => { - // @ts-expect-error ??? - // this goal is effectively "HC any of the charts" - const multiGoal: GoalDocument = deepmerge(HC511Goal, { - charts: { - type: "multi", - data: [Testing511SPA.chartID, "fake_other_chart_id"], - }, - }); + t.test("Should correctly evaluate against multi goals.", (t) => { + // @ts-expect-error ??? + // this goal is effectively "HC any of the charts" + const multiGoal: GoalDocument = deepmerge(HC511Goal, { + charts: { + type: "multi", + data: [Testing511SPA.chartID, "fake_other_chart_id"], + }, + }); - t.beforeEach(async () => { - await db["personal-bests"].insert(TestingIIDXSPScorePB); - delete TestingIIDXSPScorePB._id; + t.beforeEach(async () => { + await db["personal-bests"].insert(TestingIIDXSPScorePB); + delete TestingIIDXSPScorePB._id; - await db["personal-bests"].insert( - deepmerge(TestingIIDXSPScorePB, { - scoreData: { lamp: "CLEAR", lampIndex: 4 }, - chartID: "fake_other_chart_id", - }) - ); - }); + await db["personal-bests"].insert( + deepmerge(TestingIIDXSPScorePB, { + scoreData: { lamp: "CLEAR", lampIndex: 4 }, + chartID: "fake_other_chart_id", + }) + ); + }); - t.test("Should work if 511 is >= HARD CLEAR", async (t) => { - const res = await EvaluateGoalForUser(multiGoal, 1, logger); + t.test("Should work if 511 is >= HARD CLEAR", async (t) => { + const res = await EvaluateGoalForUser(multiGoal, 1, logger); - t.strictSame( - res, - { - achieved: true, - outOf: 5, - progress: 6, - outOfHuman: "HARD CLEAR", - progressHuman: "EX HARD CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user succeeds." - ); + t.strictSame( + res, + { + achieved: true, + outOf: 5, + progress: 6, + outOfHuman: "HARD CLEAR", + progressHuman: "EX HARD CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user succeeds." + ); - t.end(); - }); + t.end(); + }); - t.test("Should work if other chart is >= HARD CLEAR", async (t) => { - await db["personal-bests"].update( - { userID: 1, chartID: Testing511SPA.chartID }, - { $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } } - ); - await db["personal-bests"].update( - { userID: 1, chartID: "fake_other_chart_id" }, - { $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } } - ); + t.test("Should work if other chart is >= HARD CLEAR", async (t) => { + await db["personal-bests"].update( + { userID: 1, chartID: Testing511SPA.chartID }, + { $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } } + ); + await db["personal-bests"].update( + { userID: 1, chartID: "fake_other_chart_id" }, + { $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } } + ); - const res = await EvaluateGoalForUser(multiGoal, 1, logger); + const res = await EvaluateGoalForUser(multiGoal, 1, logger); - t.strictSame( - res, - { - achieved: true, - outOf: 5, - progress: 5, - outOfHuman: "HARD CLEAR", - progressHuman: "HARD CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user succeeds." - ); + t.strictSame( + res, + { + achieved: true, + outOf: 5, + progress: 5, + outOfHuman: "HARD CLEAR", + progressHuman: "HARD CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user succeeds." + ); - t.end(); - }); + t.end(); + }); - t.test("Should work if user has no scores", async (t) => { - await db["personal-bests"].remove({}); + t.test("Should work if user has no scores", async (t) => { + await db["personal-bests"].remove({}); - const res = await EvaluateGoalForUser(multiGoal, 1, logger); + const res = await EvaluateGoalForUser(multiGoal, 1, logger); - t.strictSame( - res, - { - achieved: false, - outOf: 5, - progress: null, - outOfHuman: "HARD CLEAR", - progressHuman: "NO DATA", - }, - "Should correctly evaluate the goal if the user has no data." - ); + t.strictSame( + res, + { + achieved: false, + outOf: 5, + progress: null, + outOfHuman: "HARD CLEAR", + progressHuman: "NO DATA", + }, + "Should correctly evaluate the goal if the user has no data." + ); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Should correctly evaluate against folder goals.", (t) => { - // @ts-expect-error ??? - // this goal is effectively "HC any of the sp10s" - const folderGoal: GoalDocument = deepmerge(HC511Goal, { - charts: { - type: "folder", - data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", - }, - }); + t.test("Should correctly evaluate against folder goals.", (t) => { + // @ts-expect-error ??? + // this goal is effectively "HC any of the sp10s" + const folderGoal: GoalDocument = deepmerge(HC511Goal, { + charts: { + type: "folder", + data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", + }, + }); - t.beforeEach(async () => { - await db["personal-bests"].insert(TestingIIDXSPScorePB); - delete TestingIIDXSPScorePB._id; + t.beforeEach(async () => { + await db["personal-bests"].insert(TestingIIDXSPScorePB); + delete TestingIIDXSPScorePB._id; - await db["personal-bests"].insert( - deepmerge(TestingIIDXSPScorePB, { - scoreData: { lamp: "CLEAR", lampIndex: 4 }, - chartID: "other_sp10", - }) - ); + await db["personal-bests"].insert( + deepmerge(TestingIIDXSPScorePB, { + scoreData: { lamp: "CLEAR", lampIndex: 4 }, + chartID: "other_sp10", + }) + ); - delete Testing511SPA._id; + delete Testing511SPA._id; - await db.charts.iidx.insert([ - // @ts-expect-error ??? - deepmerge(Testing511SPA, { - songID: 123, - level: "9", - chartID: "not_sp10", - }), - // @ts-expect-error ??? - deepmerge(Testing511SPA, { - songID: 124, - level: "10", - chartID: "other_sp10", - }), - ]); + await db.charts.iidx.insert([ + // @ts-expect-error ??? + deepmerge(Testing511SPA, { + songID: 123, + level: "9", + chartID: "not_sp10", + }), + // @ts-expect-error ??? + deepmerge(Testing511SPA, { + songID: 124, + level: "10", + chartID: "other_sp10", + }), + ]); - await CreateFolderChartLookup(TestingIIDXFolderSP10); - }); + await CreateFolderChartLookup(TestingIIDXFolderSP10); + }); - t.test("Should work if 511 is >= HARD CLEAR", async (t) => { - const res = await EvaluateGoalForUser(folderGoal, 1, logger); + t.test("Should work if 511 is >= HARD CLEAR", async (t) => { + const res = await EvaluateGoalForUser(folderGoal, 1, logger); - t.strictSame( - res, - { - achieved: true, - outOf: 5, - progress: 6, - outOfHuman: "HARD CLEAR", - progressHuman: "EX HARD CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user succeeds." - ); + t.strictSame( + res, + { + achieved: true, + outOf: 5, + progress: 6, + outOfHuman: "HARD CLEAR", + progressHuman: "EX HARD CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user succeeds." + ); - t.end(); - }); + t.end(); + }); - t.test("Should work if other chart is >= HARD CLEAR", async (t) => { - await db["personal-bests"].update( - { userID: 1, chartID: Testing511SPA.chartID }, - { $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } } - ); + t.test("Should work if other chart is >= HARD CLEAR", async (t) => { + await db["personal-bests"].update( + { userID: 1, chartID: Testing511SPA.chartID }, + { $set: { "scoreData.lamp": "CLEAR", "scoreData.lampIndex": 4 } } + ); - await db["personal-bests"].update( - { userID: 1, chartID: "other_sp10" }, - { $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } } - ); + await db["personal-bests"].update( + { userID: 1, chartID: "other_sp10" }, + { $set: { "scoreData.lamp": "HARD CLEAR", "scoreData.lampIndex": 5 } } + ); - const res = await EvaluateGoalForUser(folderGoal, 1, logger); + const res = await EvaluateGoalForUser(folderGoal, 1, logger); - t.strictSame( - res, - { - achieved: true, - outOf: 5, - progress: 5, - outOfHuman: "HARD CLEAR", - progressHuman: "HARD CLEAR (BP: 2)", - }, - "Should correctly evaluate the goal if the user succeeds." - ); + t.strictSame( + res, + { + achieved: true, + outOf: 5, + progress: 5, + outOfHuman: "HARD CLEAR", + progressHuman: "HARD CLEAR (BP: 2)", + }, + "Should correctly evaluate the goal if the user succeeds." + ); - t.end(); - }); + t.end(); + }); - t.test("Should work if user has no scores", async (t) => { - await db["personal-bests"].remove({}); + t.test("Should work if user has no scores", async (t) => { + await db["personal-bests"].remove({}); - const res = await EvaluateGoalForUser(folderGoal, 1, logger); + const res = await EvaluateGoalForUser(folderGoal, 1, logger); - t.strictSame( - res, - { - achieved: false, - outOf: 5, - progress: null, - outOfHuman: "HARD CLEAR", - progressHuman: "NO DATA", - }, - "Should correctly evaluate the goal if the user has no data." - ); + t.strictSame( + res, + { + achieved: false, + outOf: 5, + progress: null, + outOfHuman: "HARD CLEAR", + progressHuman: "NO DATA", + }, + "Should correctly evaluate the goal if the user has no data." + ); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#HumaniseGoalProgress", (t) => { - t.equal(HumaniseGoalProgress("iidx", "scoreData.gradeIndex", 4, null), "B"); - t.equal(HumaniseGoalProgress("iidx", "scoreData.lampIndex", 4, null), "CLEAR"); - t.equal(HumaniseGoalProgress("iidx", "scoreData.percent", 90.1142, null), "90.11%"); - t.equal(HumaniseGoalProgress("iidx", "scoreData.score", 2240, null), "2240"); + t.equal(HumaniseGoalProgress("iidx", "scoreData.gradeIndex", 4, null), "B"); + t.equal(HumaniseGoalProgress("iidx", "scoreData.lampIndex", 4, null), "CLEAR"); + t.equal(HumaniseGoalProgress("iidx", "scoreData.percent", 90.1142, null), "90.11%"); + t.equal(HumaniseGoalProgress("iidx", "scoreData.score", 2240, null), "2240"); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/achievables/goals.ts b/server/src/lib/achievables/goals.ts index fa0bbe2d1..7dbdf6151 100644 --- a/server/src/lib/achievables/goals.ts +++ b/server/src/lib/achievables/goals.ts @@ -7,11 +7,11 @@ import { FilterQuery } from "mongodb"; import fjsh from "fast-json-stable-hash"; export interface EvaluatedGoalReturn { - achieved: boolean; - progress: number | null; - outOf: number; - progressHuman: string; - outOfHuman: string; + achieved: boolean; + progress: number | null; + outOf: number; + progressHuman: string; + outOfHuman: string; } /** @@ -26,146 +26,146 @@ export interface EvaluatedGoalReturn { * to multiple games. */ export function CreateGoalID(charts: GoalDocument["charts"], criteria: GoalDocument["criteria"]) { - return fjsh.hash({ charts, criteria }, "sha256"); + return fjsh.hash({ charts, criteria }, "sha256"); } export async function EvaluateGoalForUser( - goal: GoalDocument, - userID: integer, - logger: KtLogger + goal: GoalDocument, + userID: integer, + logger: KtLogger ): Promise { - // First, we need to resolve the set of charts this - // goal involves. - const chartIDs = await ResolveGoalCharts(goal); + // First, we need to resolve the set of charts this + // goal involves. + const chartIDs = await ResolveGoalCharts(goal); - if (chartIDs === undefined) { - logger.error( - `Invalid goal ${goal.goalID} - has nonsense chartsType of ${goal.charts.type}, ignoring.` - ); - return null; - } + if (chartIDs === undefined) { + logger.error( + `Invalid goal ${goal.goalID} - has nonsense chartsType of ${goal.charts.type}, ignoring.` + ); + return null; + } - // lets configure a "base" query for our requests. - const scoreQuery: FilterQuery = { - userID, - // normally, this would be a VERY WORRYING line of code, but goal.criteria.key is guaranteed to be - // within a specific set of fields. - [goal.criteria.key]: { $gte: goal.criteria.value }, - }; + // lets configure a "base" query for our requests. + const scoreQuery: FilterQuery = { + userID, + // normally, this would be a VERY WORRYING line of code, but goal.criteria.key is guaranteed to be + // within a specific set of fields. + [goal.criteria.key]: { $gte: goal.criteria.value }, + }; - if (chartIDs) { - scoreQuery.chartID = { $in: chartIDs }; - } + if (chartIDs) { + scoreQuery.chartID = { $in: chartIDs }; + } - // Next, we need to figure out our criteria. - if (goal.criteria.mode === "single") { - const res = await db["personal-bests"].findOne(scoreQuery); - // hack, but guaranteed to work. - const scoreDataKey = goal.criteria.key.split(".")[1] as - | "lampIndex" - | "gradeIndex" - | "score" - | "percent"; + // Next, we need to figure out our criteria. + if (goal.criteria.mode === "single") { + const res = await db["personal-bests"].findOne(scoreQuery); + // hack, but guaranteed to work. + const scoreDataKey = goal.criteria.key.split(".")[1] as + | "lampIndex" + | "gradeIndex" + | "score" + | "percent"; - const outOfHuman = HumaniseGoalProgress( - goal.game, - goal.criteria.key, - goal.criteria.value, - null - ); + const outOfHuman = HumaniseGoalProgress( + goal.game, + goal.criteria.key, + goal.criteria.value, + null + ); - if (res) { - return { - achieved: true, - outOf: goal.criteria.value, - progress: res.scoreData[scoreDataKey], - outOfHuman, - progressHuman: HumaniseGoalProgress( - goal.game, - goal.criteria.key, - res.scoreData[scoreDataKey], - res - ), - }; - } + if (res) { + return { + achieved: true, + outOf: goal.criteria.value, + progress: res.scoreData[scoreDataKey], + outOfHuman, + progressHuman: HumaniseGoalProgress( + goal.game, + goal.criteria.key, + res.scoreData[scoreDataKey], + res + ), + }; + } - // if we weren't successful, we have to get the users next best score and put it up here - // this is made infinitely easier by the existance of personal-bests. - const nextBestQuery: FilterQuery = { - userID, - }; + // if we weren't successful, we have to get the users next best score and put it up here + // this is made infinitely easier by the existance of personal-bests. + const nextBestQuery: FilterQuery = { + userID, + }; - if (chartIDs) { - nextBestQuery.chartID = { $in: chartIDs }; - } + if (chartIDs) { + nextBestQuery.chartID = { $in: chartIDs }; + } - const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, { - sort: { [goal.criteria.key]: -1 }, - }); + const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, { + sort: { [goal.criteria.key]: -1 }, + }); - if (!nextBestScore) { - return { - achieved: false, - outOf: goal.criteria.value, - progress: null, - outOfHuman, - progressHuman: "NO DATA", - }; - } + if (!nextBestScore) { + return { + achieved: false, + outOf: goal.criteria.value, + progress: null, + outOfHuman, + progressHuman: "NO DATA", + }; + } - return { - achieved: false, - outOf: goal.criteria.value, - outOfHuman, - progress: nextBestScore.scoreData[scoreDataKey], - progressHuman: HumaniseGoalProgress( - goal.game, - goal.criteria.key, - nextBestScore.scoreData[scoreDataKey], - nextBestScore - ), - }; - } else if (goal.criteria.mode === "abs" || goal.criteria.mode === "proportion") { - let count; + return { + achieved: false, + outOf: goal.criteria.value, + outOfHuman, + progress: nextBestScore.scoreData[scoreDataKey], + progressHuman: HumaniseGoalProgress( + goal.game, + goal.criteria.key, + nextBestScore.scoreData[scoreDataKey], + nextBestScore + ), + }; + } else if (goal.criteria.mode === "abs" || goal.criteria.mode === "proportion") { + let count; - // abs -> Absolute mode, such as clear 10 charts. - if (goal.criteria.mode === "abs") { - count = goal.criteria.countNum; - } else { - // proportion -> Proportional mode, the value - // is a multiplier for the amount of charts - // available -- i.e. 0.1 * charts. + // abs -> Absolute mode, such as clear 10 charts. + if (goal.criteria.mode === "abs") { + count = goal.criteria.countNum; + } else { + // proportion -> Proportional mode, the value + // is a multiplier for the amount of charts + // available -- i.e. 0.1 * charts. - let totalChartCount; + let totalChartCount; - if (chartIDs === null) { - // edge case: proportion goals on "any" - // charts (i.e. clear 20% of charts) need to - // know how many charts the game has! - totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype }); - } else { - totalChartCount = chartIDs.length; - } + if (chartIDs === null) { + // edge case: proportion goals on "any" + // charts (i.e. clear 20% of charts) need to + // know how many charts the game has! + totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype }); + } else { + totalChartCount = chartIDs.length; + } - count = Math.floor(goal.criteria.countNum * totalChartCount); - } + count = Math.floor(goal.criteria.countNum * totalChartCount); + } - const userCount = await db["personal-bests"].count(scoreQuery); + const userCount = await db["personal-bests"].count(scoreQuery); - return { - achieved: userCount >= count, - progress: userCount, - outOf: count, - progressHuman: userCount.toString(), - outOfHuman: userCount.toString(), - }; - } + return { + achieved: userCount >= count, + progress: userCount, + outOf: count, + progressHuman: userCount.toString(), + outOfHuman: userCount.toString(), + }; + } - logger.error( - `Invalid goal: ${goal.goalID}, unknown criteria.mode ${goal.criteria.mode}, ignoring.` - ); + logger.error( + `Invalid goal: ${goal.goalID}, unknown criteria.mode ${goal.criteria.mode}, ignoring.` + ); - return null; + return null; } /** @@ -174,15 +174,15 @@ export async function EvaluateGoalForUser( * @returns An array of chartIDs, except if the goal chart type is "any", in which case, it returns null. */ function ResolveGoalCharts(goal: GoalDocument): Promise | string[] | null | undefined { - if (goal.charts.type === "single") { - return [goal.charts.data]; - } else if (goal.charts.type === "multi") { - return goal.charts.data; - } else if (goal.charts.type === "folder") { - return GetFolderChartIDs(goal.charts.data); - } else if (goal.charts.type === "any") { - return null; // special case. - } + if (goal.charts.type === "single") { + return [goal.charts.data]; + } else if (goal.charts.type === "multi") { + return goal.charts.data; + } else if (goal.charts.type === "folder") { + return GetFolderChartIDs(goal.charts.data); + } else if (goal.charts.type === "any") { + return null; // special case. + } } type GoalKeys = GoalDocument["criteria"]["key"]; @@ -191,26 +191,26 @@ type IIDXOrBMSPB = PBScoreDocument<"iidx:SP" | "iidx:DP" | "bms:7K" | "bms:14K"> // @todo, #100 improve this (add things like BP for iidx, maybe, percents for scores?) export function HumaniseGoalProgress( - game: Game, - key: GoalKeys, - value: number, - userPB: PBScoreDocument | null + game: Game, + key: GoalKeys, + value: number, + userPB: PBScoreDocument | null ): string { - switch (key) { - case "scoreData.gradeIndex": - return grades[game][value]; - case "scoreData.lampIndex": - if (userPB && (game === "iidx" || game === "bms")) { - return `${lamps[game][value]} (BP: ${ - (userPB as IIDXOrBMSPB).scoreData.hitMeta.bp ?? "N/A" - })`; - } - return lamps[game][value]; - case "scoreData.percent": - return `${value.toFixed(2)}%`; - case "scoreData.score": - return value.toString(); - default: - throw new Error(`Broken goal - invalid key ${key}.`); - } + switch (key) { + case "scoreData.gradeIndex": + return grades[game][value]; + case "scoreData.lampIndex": + if (userPB && (game === "iidx" || game === "bms")) { + return `${lamps[game][value]} (BP: ${ + (userPB as IIDXOrBMSPB).scoreData.hitMeta.bp ?? "N/A" + })`; + } + return lamps[game][value]; + case "scoreData.percent": + return `${value.toFixed(2)}%`; + case "scoreData.score": + return value.toString(); + default: + throw new Error(`Broken goal - invalid key ${key}.`); + } } diff --git a/server/src/lib/cdn/cdn.ts b/server/src/lib/cdn/cdn.ts index 7d785ad06..841ea978a 100644 --- a/server/src/lib/cdn/cdn.ts +++ b/server/src/lib/cdn/cdn.ts @@ -14,16 +14,16 @@ const logger = CreateLogCtx(__filename); * Joins a file location against the location of the CDN static store. */ function CDNRoot(fileLoc: string) { - return path.join(KTCDN_ROOT, fileLoc); + return path.join(KTCDN_ROOT, fileLoc); } /** * Retrieves the data of the file at the given CDN location. */ export function RetrieveCDN(fileLoc: string) { - logger.debug(`Retrieving path ${fileLoc}.`); + logger.debug(`Retrieving path ${fileLoc}.`); - return readFilePromise(CDNRoot(fileLoc)); + return readFilePromise(CDNRoot(fileLoc)); } /** @@ -36,12 +36,12 @@ const WRITE_NO_OVERWRITE = "wx"; * @returns Nothing on success. Throws on error. */ export async function StoreCDN(fileLoc: string, data: Buffer | string) { - logger.debug(`Storing path ${fileLoc}.`); + logger.debug(`Storing path ${fileLoc}.`); - const loc = CDNRoot(fileLoc); + const loc = CDNRoot(fileLoc); - // make the parent folders if they dont exist. else, mkdirp is a no-op. - await mkdirp(path.dirname(loc)); + // make the parent folders if they dont exist. else, mkdirp is a no-op. + await mkdirp(path.dirname(loc)); - return writeFilePromise(loc, data, { flag: WRITE_NO_OVERWRITE }); + return writeFilePromise(loc, data, { flag: WRITE_NO_OVERWRITE }); } diff --git a/server/src/lib/constants/classes.ts b/server/src/lib/constants/classes.ts index 0f08443a9..1c9f3161a 100644 --- a/server/src/lib/constants/classes.ts +++ b/server/src/lib/constants/classes.ts @@ -1,161 +1,161 @@ export enum IIDXClasses { - KYU_7, - KYU_6, - KYU_5, - KYU_4, - KYU_3, - KYU_2, - KYU_1, + KYU_7, + KYU_6, + KYU_5, + KYU_4, + KYU_3, + KYU_2, + KYU_1, - DAN_1, - DAN_2, - DAN_3, - DAN_4, - DAN_5, - DAN_6, - DAN_7, - DAN_8, - DAN_9, - DAN_10, + DAN_1, + DAN_2, + DAN_3, + DAN_4, + DAN_5, + DAN_6, + DAN_7, + DAN_8, + DAN_9, + DAN_10, - CHUUDEN, - KAIDEN, + CHUUDEN, + KAIDEN, } export enum GitadoraColours { - WHITE, + WHITE, - ORANGE, - ORANGE_GRADIENT, - YELLOW, - YELLOW_GRADIENT, - GREEN, - GREEN_GRADIENT, - BLUE, - BLUE_GRADIENT, - PURPLE, - PURPLE_GRADIENT, - RED, - RED_GRADIENT, + ORANGE, + ORANGE_GRADIENT, + YELLOW, + YELLOW_GRADIENT, + GREEN, + GREEN_GRADIENT, + BLUE, + BLUE_GRADIENT, + PURPLE, + PURPLE_GRADIENT, + RED, + RED_GRADIENT, - BRONZE, - SILVER, - GOLD, - RAINBOW, + BRONZE, + SILVER, + GOLD, + RAINBOW, } export enum BMSGenocideDans { - NORMAL_1, - NORMAL_2, - NORMAL_3, - NORMAL_4, - NORMAL_5, - NORMAL_6, - NORMAL_7, - NORMAL_8, - NORMAL_9, - NORMAL_10, + NORMAL_1, + NORMAL_2, + NORMAL_3, + NORMAL_4, + NORMAL_5, + NORMAL_6, + NORMAL_7, + NORMAL_8, + NORMAL_9, + NORMAL_10, - INSANE_1, - INSANE_2, - INSANE_3, - INSANE_4, - INSANE_5, - INSANE_6, - INSANE_7, - INSANE_8, - INSANE_9, - INSANE_10, - INSANE_KAIDEN, + INSANE_1, + INSANE_2, + INSANE_3, + INSANE_4, + INSANE_5, + INSANE_6, + INSANE_7, + INSANE_8, + INSANE_9, + INSANE_10, + INSANE_KAIDEN, - OVERJOY, + OVERJOY, } export enum BMSStSlDans { - SL_0, - SL_1, - SL_2, - SL_3, - SL_4, - SL_5, - SL_6, - SL_7, - SL_8, - SL_9, - SL_10, - SL_11, - SL_12, + SL_0, + SL_1, + SL_2, + SL_3, + SL_4, + SL_5, + SL_6, + SL_7, + SL_8, + SL_9, + SL_10, + SL_11, + SL_12, - ST_0, - ST_1, - ST_2, - ST_3, - ST_4, - ST_5, - ST_6, - ST_7, - ST_8, - ST_9, - ST_10, - ST_11, - ST_12, + ST_0, + ST_1, + ST_2, + ST_3, + ST_4, + ST_5, + ST_6, + ST_7, + ST_8, + ST_9, + ST_10, + ST_11, + ST_12, } export enum SDVXDans { - DAN_1, - DAN_2, - DAN_3, - DAN_4, - DAN_5, - DAN_6, - DAN_7, - DAN_8, - DAN_9, - DAN_10, - DAN_11, - INF, + DAN_1, + DAN_2, + DAN_3, + DAN_4, + DAN_5, + DAN_6, + DAN_7, + DAN_8, + DAN_9, + DAN_10, + DAN_11, + INF, } // ewww export enum SDVXVFClasses { - SIENNA_I, - SIENNA_II, - SIENNA_III, - SIENNA_IV, - COBALT_I, - COBALT_II, - COBALT_III, - COBALT_IV, - DANDELION_I, - DANDELION_II, - DANDELION_III, - DANDELION_IV, - CYAN_I, - CYAN_II, - CYAN_III, - CYAN_IV, - SCARLET_I, - SCARLET_II, - SCARLET_III, - SCARLET_IV, - CORAL_I, - CORAL_II, - CORAL_III, - CORAL_IV, - ARGENTO_I, - ARGENTO_II, - ARGENTO_III, - ARGENTO_IV, - ELDORA_I, - ELDORA_II, - ELDORA_III, - ELDORA_IV, - CRIMSON_I, - CRIMSON_II, - CRIMSON_III, - CRIMSON_IV, - IMPERIAL_I, - IMPERIAL_II, - IMPERIAL_III, - IMPERIAL_IV, + SIENNA_I, + SIENNA_II, + SIENNA_III, + SIENNA_IV, + COBALT_I, + COBALT_II, + COBALT_III, + COBALT_IV, + DANDELION_I, + DANDELION_II, + DANDELION_III, + DANDELION_IV, + CYAN_I, + CYAN_II, + CYAN_III, + CYAN_IV, + SCARLET_I, + SCARLET_II, + SCARLET_III, + SCARLET_IV, + CORAL_I, + CORAL_II, + CORAL_III, + CORAL_IV, + ARGENTO_I, + ARGENTO_II, + ARGENTO_III, + ARGENTO_IV, + ELDORA_I, + ELDORA_II, + ELDORA_III, + ELDORA_IV, + CRIMSON_I, + CRIMSON_II, + CRIMSON_III, + CRIMSON_IV, + IMPERIAL_I, + IMPERIAL_II, + IMPERIAL_III, + IMPERIAL_IV, } diff --git a/server/src/lib/constants/version.ts b/server/src/lib/constants/version.ts index 508d1efdd..f3382bc1f 100644 --- a/server/src/lib/constants/version.ts +++ b/server/src/lib/constants/version.ts @@ -11,29 +11,29 @@ const PATCH = 0; // The fact that this album is very similar to Mezzanine // - the album used for Kamaitachi v1 - is deliberate. :) - zkldi const PORTISHEAD_DUMMY = [ - "Mysterons", // v2.0.0 - "Sour Times", // v2.1.0 - "Strangers", // v2.2.0 - "It Could Be Sweet", // v2.3.0 - "Wandering Star", // v2.4.0 - "It's a Fire", // v2.5.0 - "Numb", // v2.6.0 - "Roads", // v2.7.0 - "Pedestal", // v2.8.0 - "Biscuit", // v2.9.0 - "Glory Box", // v2.10.0 - // v2.11+ onwards are not expected to occur, but if they do, we'll figure something out. + "Mysterons", // v2.0.0 + "Sour Times", // v2.1.0 + "Strangers", // v2.2.0 + "It Could Be Sweet", // v2.3.0 + "Wandering Star", // v2.4.0 + "It's a Fire", // v2.5.0 + "Numb", // v2.6.0 + "Roads", // v2.7.0 + "Pedestal", // v2.8.0 + "Biscuit", // v2.9.0 + "Glory Box", // v2.10.0 + // v2.11+ onwards are not expected to occur, but if they do, we'll figure something out. ]; export const VERSION_INFO = { - major: MAJOR, - minor: MINOR, - patch: PATCH, - name: PORTISHEAD_DUMMY[MINOR], + major: MAJOR, + minor: MINOR, + patch: PATCH, + name: PORTISHEAD_DUMMY[MINOR], }; export const VERSION_STR = `${MAJOR}.${MINOR}.${PATCH}`; export function FormatVersion() { - return `v${VERSION_STR} (${VERSION_INFO.name})`; + return `v${VERSION_STR} (${VERSION_INFO.name})`; } diff --git a/server/src/lib/logger/logger.test.ts b/server/src/lib/logger/logger.test.ts index 0943e9009..5903430e5 100644 --- a/server/src/lib/logger/logger.test.ts +++ b/server/src/lib/logger/logger.test.ts @@ -3,23 +3,23 @@ import { CloseAllConnections } from "../../test-utils/close-connections"; import CreateLogCtx, { Transports } from "./logger"; t.test("Logger Tests", (t) => { - const logger = CreateLogCtx(__filename); + const logger = CreateLogCtx(__filename); - Transports[0].level = "debug"; // lol + Transports[0].level = "debug"; // lol - logger.debug("Debug Message Test"); - logger.verbose("Verbose Message Test"); - logger.info("Info Message Test"); - logger.warn("Warning Message Test"); - logger.error("Error Message Test"); - logger.severe("Severe Message Test"); - logger.crit("Critical Message Test"); + logger.debug("Debug Message Test"); + logger.verbose("Verbose Message Test"); + logger.info("Info Message Test"); + logger.warn("Warning Message Test"); + logger.error("Error Message Test"); + logger.severe("Severe Message Test"); + logger.crit("Critical Message Test"); - Transports[0].level = process.env.LOG_LEVEL ?? "info"; + Transports[0].level = process.env.LOG_LEVEL ?? "info"; - logger.debug("This message shouldn't appear."); + logger.debug("This message shouldn't appear."); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/logger/logger.ts b/server/src/lib/logger/logger.ts index ac693f116..646a0f37f 100644 --- a/server/src/lib/logger/logger.ts +++ b/server/src/lib/logger/logger.ts @@ -10,150 +10,150 @@ const level = LOG_LEVEL ?? "info"; const IN_TESTING = process.env.NODE_ENV === "test"; const formatExcessProperties = (meta: Record) => { - let i = 0; - for (const key in meta) { - const val = meta[key]; + let i = 0; + for (const key in meta) { + const val = meta[key]; - if (val instanceof Error) { - meta[key] = { message: val.message, stack: val.stack }; - } - i++; - } + if (val instanceof Error) { + meta[key] = { message: val.message, stack: val.stack }; + } + i++; + } - if (!i) { - return ""; - } + if (!i) { + return ""; + } - return ` ${JSON.stringify(meta)}`; + return ` ${JSON.stringify(meta)}`; }; const formatExcessPropertiesNoStack = (meta: Record, omitKeys: string[] = []) => { - let i = 0; - const realMeta: Record = {}; + let i = 0; + const realMeta: Record = {}; - for (const key in meta) { - if (omitKeys.includes(key)) { - continue; - } + for (const key in meta) { + if (omitKeys.includes(key)) { + continue; + } - const val = meta[key]; + const val = meta[key]; - if (val instanceof Error) { - realMeta[key] = { message: val.message }; - } else { - realMeta[key] = val; - } - i++; - } + if (val instanceof Error) { + realMeta[key] = { message: val.message }; + } else { + realMeta[key] = val; + } + i++; + } - if (!i) { - return ""; - } + if (!i) { + return ""; + } - return ` ${JSON.stringify(realMeta)}`; + return ` ${JSON.stringify(realMeta)}`; }; const ktblackPrintf = format.printf( - ({ level, message, context = "ktblack-root", timestamp, ...meta }) => - `${timestamp} [${ - Array.isArray(context) ? context.join(" | ") : context - }] ${level}: ${message}${formatExcessProperties(meta)}` + ({ level, message, context = "ktblack-root", timestamp, ...meta }) => + `${timestamp} [${ + Array.isArray(context) ? context.join(" | ") : context + }] ${level}: ${message}${formatExcessProperties(meta)}` ); const ktblackConsolePrintf = format.printf( - ({ level, message, context = "ktblack-root", timestamp, hideFromConsole, ...meta }) => - `${timestamp} [${ - Array.isArray(context) ? context.join(" | ") : context - }] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole)}` + ({ level, message, context = "ktblack-root", timestamp, hideFromConsole, ...meta }) => + `${timestamp} [${ + Array.isArray(context) ? context.join(" | ") : context + }] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole)}` ); winston.addColors({ - crit: ["bgRed", "black"], - severe: ["bgBrightRed", "black"], - error: ["red"], - warn: ["yellow"], - info: ["blue"], - verbose: ["cyan"], - debug: ["white"], + crit: ["bgRed", "black"], + severe: ["bgBrightRed", "black"], + error: ["red"], + warn: ["yellow"], + info: ["blue"], + verbose: ["cyan"], + debug: ["white"], }); const baseFormatRoute = format.combine( - format.timestamp({ - format: "YYYY-MM-DD HH:mm:ss", - }) + format.timestamp({ + format: "YYYY-MM-DD HH:mm:ss", + }) ); const defaultFormatRoute = format.combine( - baseFormatRoute, - format.errors({ stack: false }), - ktblackPrintf + baseFormatRoute, + format.errors({ stack: false }), + ktblackPrintf ); const consoleFormatRoute = format.combine( - baseFormatRoute, - format.errors({ stack: false }), - ktblackConsolePrintf, - format.colorize({ - all: true, - }) + baseFormatRoute, + format.errors({ stack: false }), + ktblackConsolePrintf, + format.colorize({ + all: true, + }) ); let tports = []; /* istanbul ignore next */ if (IN_TESTING) { - tports = [ - new transports.Console({ - format: consoleFormatRoute, - }), - ]; + tports = [ + new transports.Console({ + format: consoleFormatRoute, + }), + ]; } else { - tports = [ - new transports.File({ - filename: "logs/ktblack-error.log", - level: "error", - format: defaultFormatRoute, - }), - new transports.File({ filename: "logs/ktblack.log", format: defaultFormatRoute }), - new transports.Console({ - format: consoleFormatRoute, - }), - ]; + tports = [ + new transports.File({ + filename: "logs/ktblack-error.log", + level: "error", + format: defaultFormatRoute, + }), + new transports.File({ filename: "logs/ktblack.log", format: defaultFormatRoute }), + new transports.Console({ + format: consoleFormatRoute, + }), + ]; } export const rootLogger = winston.createLogger({ - levels: { - crit: 0, // entire process termination is necessary - severe: 1, // something is wrong, and more than one function is affected (such as a failed assertion that is definitely expected to be true). - error: 2, // function call (or related process) has failed unexpectedly - warn: 3, // function call has hit something it didn't want, but can recover - info: 4, // something has happened that is expected, but worth logging - verbose: 5, // something has happened - debug: 6, // glorified console.log debugging - }, - level, - format: defaultFormatRoute, - transports: tports, + levels: { + crit: 0, // entire process termination is necessary + severe: 1, // something is wrong, and more than one function is affected (such as a failed assertion that is definitely expected to be true). + error: 2, // function call (or related process) has failed unexpectedly + warn: 3, // function call has hit something it didn't want, but can recover + info: 4, // something has happened that is expected, but worth logging + verbose: 5, // something has happened + debug: 6, // glorified console.log debugging + }, + level, + format: defaultFormatRoute, + transports: tports, }); function CreateLogCtx(filename: string, lg = rootLogger): KtLogger { - const replacedFilename = filename.replace( - new RegExp(`^${EscapeStringRegexp(process.cwd())}/((js|src)/)?`, "u"), - "" - ); + const replacedFilename = filename.replace( + new RegExp(`^${EscapeStringRegexp(process.cwd())}/((js|src)/)?`, "u"), + "" + ); - const logger = lg.child({ - context: [replacedFilename], - }) as KtLogger; + const logger = lg.child({ + context: [replacedFilename], + }) as KtLogger; - logger.defaultMeta = { context: [replacedFilename] }; - return logger; + logger.defaultMeta = { context: [replacedFilename] }; + return logger; } export function AppendLogCtx(context: string, lg: KtLogger): KtLogger { - const newContext = [...lg.defaultMeta.context, context]; + const newContext = [...lg.defaultMeta.context, context]; - return lg.child({ context: newContext }) as KtLogger; + return lg.child({ context: newContext }) as KtLogger; } export const Transports = tports; diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts index 99eb8bb2f..859154bbe 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts @@ -4,11 +4,11 @@ import db from "../../../../external/mongo/db"; import CreateLogCtx from "../../../logger/logger"; import { prAssert } from "../../../../test-utils/asserts"; import { - Testing511SPA, - TestingDoraChart, - TestingGITADORADoraDryScore, - TestingIIDXSPDryScore, - TestingSDVXSingleDryScore, + Testing511SPA, + TestingDoraChart, + TestingGITADORADoraDryScore, + TestingIIDXSPDryScore, + TestingSDVXSingleDryScore, } from "../../../../test-utils/test-data"; import { CreateCalculatedData, CalculateDataForGamePT } from "./calculated-data"; import deepmerge from "deepmerge"; @@ -18,374 +18,374 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); t.test("#CreateCalculatedData", async (t) => { - const res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, 30, logger); + const res = await CreateCalculatedData(TestingIIDXSPDryScore, Testing511SPA, 30, logger); - prAssert( - res, - { - ktRating: p.aprx(2.65), - ktLampRating: p.equalTo(10), - BPI: "?number", - "K%": "?number", - }, - "Should correctly produce calculatedData" - ); + prAssert( + res, + { + ktRating: p.aprx(2.65), + ktLampRating: p.equalTo(10), + BPI: "?number", + "K%": "?number", + }, + "Should correctly produce calculatedData" + ); - const gitadoraRes = await CreateCalculatedData( - TestingGITADORADoraDryScore, - TestingDoraChart, - 30, - logger - ); + const gitadoraRes = await CreateCalculatedData( + TestingGITADORADoraDryScore, + TestingDoraChart, + 30, + logger + ); - prAssert( - gitadoraRes, - { - skill: p.isPositiveNonZero, - }, - "Should correctly call rating function overrides for different games" - ); + prAssert( + gitadoraRes, + { + skill: p.isPositiveNonZero, + }, + "Should correctly call rating function overrides for different games" + ); - t.end(); + t.end(); }); /** * These tests only check that the right properties are assigned. */ t.test("#CalculateDataForGamePT", (t) => { - const defaultTierlistID = undefined; - t.test("IIDX:SP", async (t) => { - const res = await CalculateDataForGamePT( - "iidx", - "SP", - Testing511SPA, - TestingIIDXSPDryScore, - 30, - defaultTierlistID, - logger - ); + const defaultTierlistID = undefined; + t.test("IIDX:SP", async (t) => { + const res = await CalculateDataForGamePT( + "iidx", + "SP", + Testing511SPA, + TestingIIDXSPDryScore, + 30, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktRating: "?number", - ktLampRating: "?number", - BPI: "?number", - "K%": "?number", - }, - "Response should contain keys for IIDX:SP" - ); + prAssert( + res, + { + ktRating: "?number", + ktLampRating: "?number", + BPI: "?number", + "K%": "?number", + }, + "Response should contain keys for IIDX:SP" + ); - t.end(); - }); + t.end(); + }); - t.test("IIDX:DP", async (t) => { - const res = await CalculateDataForGamePT( - "iidx", - "DP", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an SP score. but we're testing - 30, - defaultTierlistID, - logger - ); + t.test("IIDX:DP", async (t) => { + const res = await CalculateDataForGamePT( + "iidx", + "DP", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an SP score. but we're testing + 30, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktRating: "?number", - ktLampRating: "?number", - BPI: "?number", - }, - "Response should contain keys for IIDX:DP" - ); + prAssert( + res, + { + ktRating: "?number", + ktLampRating: "?number", + BPI: "?number", + }, + "Response should contain keys for IIDX:DP" + ); - t.end(); - }); + t.end(); + }); - t.test("SDVX:Single", async (t) => { - const res = await CalculateDataForGamePT( - "sdvx", - "Single", - Testing511SPA, - TestingSDVXSingleDryScore, - null, - defaultTierlistID, - logger - ); + t.test("SDVX:Single", async (t) => { + const res = await CalculateDataForGamePT( + "sdvx", + "Single", + Testing511SPA, + TestingSDVXSingleDryScore, + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - VF6: p.nullable(p.isPositive), - }, - "Response should contain keys for SDVX:Single" - ); + prAssert( + res, + { + VF6: p.nullable(p.isPositive), + }, + "Response should contain keys for SDVX:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("DDR:SP", async (t) => { - const res = await CalculateDataForGamePT( - "ddr", - "SP", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("DDR:SP", async (t) => { + const res = await CalculateDataForGamePT( + "ddr", + "SP", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - MFCP: p.nullable(p.isPositiveInteger), - ktRating: "?number", - }, - "Response should contain keys for DDR:SP" - ); + prAssert( + res, + { + MFCP: p.nullable(p.isPositiveInteger), + ktRating: "?number", + }, + "Response should contain keys for DDR:SP" + ); - t.end(); - }); + t.end(); + }); - t.test("DDR:DP", async (t) => { - const res = await CalculateDataForGamePT( - "ddr", - "DP", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("DDR:DP", async (t) => { + const res = await CalculateDataForGamePT( + "ddr", + "DP", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - MFCP: "null", - ktRating: "?number", - }, - "Response should contain nulled keys for DDR:DP" - ); + prAssert( + res, + { + MFCP: "null", + ktRating: "?number", + }, + "Response should contain nulled keys for DDR:DP" + ); - t.end(); - }); + t.end(); + }); - t.test("chunithm:Single", async (t) => { - const res = await CalculateDataForGamePT( - "chunithm", - "Single", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("chunithm:Single", async (t) => { + const res = await CalculateDataForGamePT( + "chunithm", + "Single", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - rating: "?number", - }, - "Response should contain nulled keys for chunithm:Single" - ); + prAssert( + res, + { + rating: "?number", + }, + "Response should contain nulled keys for chunithm:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("maimai:Single", async (t) => { - const res = await CalculateDataForGamePT( - "maimai", - "Single", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("maimai:Single", async (t) => { + const res = await CalculateDataForGamePT( + "maimai", + "Single", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktRating: "?number", - }, - "Response should contain nulled keys for maimai:Single" - ); + prAssert( + res, + { + ktRating: "?number", + }, + "Response should contain nulled keys for maimai:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("museca:Single", async (t) => { - const res = await CalculateDataForGamePT( - "museca", - "Single", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("museca:Single", async (t) => { + const res = await CalculateDataForGamePT( + "museca", + "Single", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktRating: "?number", - }, - "Response should contain nulled keys for museca:Single" - ); + prAssert( + res, + { + ktRating: "?number", + }, + "Response should contain nulled keys for museca:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("bms:7K", async (t) => { - const res = await CalculateDataForGamePT( - "bms", - "7K", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("bms:7K", async (t) => { + const res = await CalculateDataForGamePT( + "bms", + "7K", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktLampRating: "?number", - }, - "Response should contain nulled keys for bms:7K" - ); + prAssert( + res, + { + ktLampRating: "?number", + }, + "Response should contain nulled keys for bms:7K" + ); - t.end(); - }); + t.end(); + }); - t.test("bms:14K", async (t) => { - const res = await CalculateDataForGamePT( - "bms", - "14K", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("bms:14K", async (t) => { + const res = await CalculateDataForGamePT( + "bms", + "14K", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - ktLampRating: "?number", - }, - "Response should contain nulled keys for bms:14K" - ); + prAssert( + res, + { + ktLampRating: "?number", + }, + "Response should contain nulled keys for bms:14K" + ); - t.end(); - }); + t.end(); + }); - t.test("gitadora:Gita", async (t) => { - const res = await CalculateDataForGamePT( - "gitadora", - "Gita", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("gitadora:Gita", async (t) => { + const res = await CalculateDataForGamePT( + "gitadora", + "Gita", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - skill: "?number", - }, - "Response should contain nulled keys for gitadora:Gita" - ); + prAssert( + res, + { + skill: "?number", + }, + "Response should contain nulled keys for gitadora:Gita" + ); - t.end(); - }); + t.end(); + }); - t.test("gitadora:Dora", async (t) => { - const res = await CalculateDataForGamePT( - "gitadora", - "Dora", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("gitadora:Dora", async (t) => { + const res = await CalculateDataForGamePT( + "gitadora", + "Dora", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - skill: "?number", - }, - "Response should contain nulled keys for gitadora:Dora" - ); + prAssert( + res, + { + skill: "?number", + }, + "Response should contain nulled keys for gitadora:Dora" + ); - t.end(); - }); + t.end(); + }); - t.test("usc:Single", async (t) => { - const res = await CalculateDataForGamePT( - "usc", - "Single", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("usc:Single", async (t) => { + const res = await CalculateDataForGamePT( + "usc", + "Single", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - VF6: "?number", - }, - "Response should contain nulled keys for usc:Single" - ); + prAssert( + res, + { + VF6: "?number", + }, + "Response should contain nulled keys for usc:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("jubeat:Single", async (t) => { - const res = await CalculateDataForGamePT( - "jubeat", - "Single", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("jubeat:Single", async (t) => { + const res = await CalculateDataForGamePT( + "jubeat", + "Single", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert( - res, - { - jubility: "?number", - }, - "Response should contain nulled keys for jubeat:Single" - ); + prAssert( + res, + { + jubility: "?number", + }, + "Response should contain nulled keys for jubeat:Single" + ); - t.end(); - }); + t.end(); + }); - t.test("popn:9B", async (t) => { - const res = await CalculateDataForGamePT( - "popn", - "9B", - Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing - null, - defaultTierlistID, - logger - ); + t.test("popn:9B", async (t) => { + const res = await CalculateDataForGamePT( + "popn", + "9B", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + defaultTierlistID, + logger + ); - prAssert(res, {}, "Response should contain nulled keys for popn:9B"); + prAssert(res, {}, "Response should contain nulled keys for popn:9B"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts index 600df1d18..bd177ba1a 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts @@ -1,12 +1,12 @@ import { - AnyChartDocument, - Game, - Playtypes, - ScoreDocument, - ChartDocument, - Grades, - Lamps, - IDStrings, + AnyChartDocument, + Game, + Playtypes, + ScoreDocument, + ChartDocument, + Grades, + Lamps, + IDStrings, } from "tachi-common"; import db from "../../../../external/mongo/db"; import { HasOwnProperty } from "../../../../utils/misc"; @@ -15,308 +15,308 @@ import { GetDefaultTierlist } from "../../../../utils/tierlist"; import { KtLogger } from "../../../logger/logger"; import { DryScore } from "../common/types"; import { - CalculateCHUNITHMRating, - CalculateGITADORASkill, - CalculateBPI, - CalculateMFCP, - KaidenPercentile, - CalculateKTRating, - CalculateKTLampRating, - CalculateVF6, + CalculateCHUNITHMRating, + CalculateGITADORASkill, + CalculateBPI, + CalculateMFCP, + KaidenPercentile, + CalculateKTRating, + CalculateKTLampRating, + CalculateVF6, } from "./stats"; export async function CreateCalculatedData( - dryScore: DryScore, - chart: AnyChartDocument, - esd: number | null, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + esd: number | null, + logger: KtLogger ): Promise { - const game = dryScore.game; - const playtype = chart.playtype; + const game = dryScore.game; + const playtype = chart.playtype; - const defaultTierlist = await GetDefaultTierlist(game, playtype); - const defaultTierlistID = defaultTierlist?.tierlistID; // tierlistID | undefined + const defaultTierlist = await GetDefaultTierlist(game, playtype); + const defaultTierlistID = defaultTierlist?.tierlistID; // tierlistID | undefined - const calculatedData = await CalculateDataForGamePT( - game, - playtype, - chart, - dryScore, - esd, - defaultTierlistID, - logger - ); + const calculatedData = await CalculateDataForGamePT( + game, + playtype, + chart, + dryScore, + esd, + defaultTierlistID, + logger + ); - return calculatedData; + return calculatedData; } type CalculatedDataFunctions = { - [G in Game]: { - [P in Playtypes[G]]: ( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string, - logger: KtLogger - ) => Promise | ScoreDocument["calculatedData"]; - }; + [G in Game]: { + [P in Playtypes[G]]: ( + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string, + logger: KtLogger + ) => Promise | ScoreDocument["calculatedData"]; + }; }; const CalculatedDataFunctions: CalculatedDataFunctions = { - iidx: { - SP: CalculateDataIIDXSP, - DP: CalculateDataIIDXDP, - }, - sdvx: { - Single: CalculateDataSDVXorUSC, - }, - popn: { - "9B": () => ({}), - }, - museca: { - Single: CalculateDataMuseca, - }, - chunithm: { - Single: CalculateDataCHUNITHM, - }, - maimai: { - Single: CalculateDataMaimai, - }, - gitadora: { - Gita: CalculateDataGitadora, - Dora: CalculateDataGitadora, - }, - bms: { - "7K": CalculateDataBMS7K, - "14K": CalculateDataBMS14K, - }, - ddr: { - SP: CalculateDataDDR, - DP: CalculateDataDDR, - }, - jubeat: { - Single: CalculateDataJubeat, - }, - usc: { - Single: CalculateDataSDVXorUSC, - }, + iidx: { + SP: CalculateDataIIDXSP, + DP: CalculateDataIIDXDP, + }, + sdvx: { + Single: CalculateDataSDVXorUSC, + }, + popn: { + "9B": () => ({}), + }, + museca: { + Single: CalculateDataMuseca, + }, + chunithm: { + Single: CalculateDataCHUNITHM, + }, + maimai: { + Single: CalculateDataMaimai, + }, + gitadora: { + Gita: CalculateDataGitadora, + Dora: CalculateDataGitadora, + }, + bms: { + "7K": CalculateDataBMS7K, + "14K": CalculateDataBMS14K, + }, + ddr: { + SP: CalculateDataDDR, + DP: CalculateDataDDR, + }, + jubeat: { + Single: CalculateDataJubeat, + }, + usc: { + Single: CalculateDataSDVXorUSC, + }, }; // Creates Game-Specific calculatedData for the provided game & playtype. export function CalculateDataForGamePT( - game: G, - playtype: Playtypes[G], - chart: AnyChartDocument, - dryScore: DryScore, - // ESD gets specially passed through because it's not part of the DryScore, but - // can be used for statistics anyway. - esd: number | null, - defaultTierlistID: string | undefined, - logger: KtLogger + game: G, + playtype: Playtypes[G], + chart: AnyChartDocument, + dryScore: DryScore, + // ESD gets specially passed through because it's not part of the DryScore, but + // can be used for statistics anyway. + esd: number | null, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise { - const GameRatingFns = CalculatedDataFunctions[game]; + const GameRatingFns = CalculatedDataFunctions[game]; - if (!HasOwnProperty(GameRatingFns, playtype)) { - logger.error( - `Invalid playtype of ${playtype} given for game ${game} in CalculateDataForGamePT, returning an empty object.` - ); - return new Promise((resolve) => resolve({})); - } + if (!HasOwnProperty(GameRatingFns, playtype)) { + logger.error( + `Invalid playtype of ${playtype} given for game ${game} in CalculateDataForGamePT, returning an empty object.` + ); + return new Promise((resolve) => resolve({})); + } - // @ts-expect-error standard game->pt stuff. - return GameRatingFns[playtype](dryScore, chart, defaultTierlistID, logger); + // @ts-expect-error standard game->pt stuff. + return GameRatingFns[playtype](dryScore, chart, defaultTierlistID, logger); } type CalculatedData = Required["calculatedData"]>; async function CalculateDataIIDXSP( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - const BPIData = await db["iidx-bpi-data"].findOne({ - chartID: chart.chartID, - }); + const BPIData = await db["iidx-bpi-data"].findOne({ + chartID: chart.chartID, + }); - let bpi; - const kPercent = await KaidenPercentile(dryScore, chart); + let bpi; + const kPercent = await KaidenPercentile(dryScore, chart); - if (BPIData) { - bpi = CalculateBPI( - BPIData.kavg, - BPIData.wr, - dryScore.scoreData.score, - (chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2, - BPIData.coef - ); + if (BPIData) { + bpi = CalculateBPI( + BPIData.kavg, + BPIData.wr, + dryScore.scoreData.score, + (chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2, + BPIData.coef + ); - // kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled - } else { - bpi = null; - } + // kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled + } else { + bpi = null; + } - return { - BPI: bpi, - "K%": kPercent, - ktRating: await CalculateKTRating(dryScore, "iidx", "SP", chart, logger, defaultTierlistID), - ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "SP", chart, defaultTierlistID), - }; + return { + BPI: bpi, + "K%": kPercent, + ktRating: await CalculateKTRating(dryScore, "iidx", "SP", chart, logger, defaultTierlistID), + ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "SP", chart, defaultTierlistID), + }; } async function CalculateDataIIDXDP( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - const BPIData = await db["iidx-bpi-data"].findOne({ - chartID: chart.chartID, - }); + const BPIData = await db["iidx-bpi-data"].findOne({ + chartID: chart.chartID, + }); - let bpi; + let bpi; - if (BPIData) { - bpi = CalculateBPI( - BPIData.kavg, - BPIData.wr, - dryScore.scoreData.score, - (chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2, - BPIData.coef - ); + if (BPIData) { + bpi = CalculateBPI( + BPIData.kavg, + BPIData.wr, + dryScore.scoreData.score, + (chart as ChartDocument<"iidx:DP" | "iidx:SP">).data.notecount * 2, + BPIData.coef + ); - // kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled - } else { - bpi = null; - } + // kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled + } else { + bpi = null; + } - return { - BPI: bpi, - ktRating: await CalculateKTRating(dryScore, "iidx", "DP", chart, logger, defaultTierlistID), - ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "DP", chart, defaultTierlistID), - }; + return { + BPI: bpi, + ktRating: await CalculateKTRating(dryScore, "iidx", "DP", chart, logger, defaultTierlistID), + ktLampRating: await CalculateKTLampRating(dryScore, "iidx", "DP", chart, defaultTierlistID), + }; } function CalculateDataSDVXorUSC( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): CalculatedData<"sdvx:Single" | "usc:Single"> { - const VF6 = CalculateVF6( - dryScore.scoreData.grade as Grades["sdvx:Single"], - dryScore.scoreData.lamp as Lamps["sdvx:Single"], - dryScore.scoreData.percent, - chart.levelNum, - logger - ); + const VF6 = CalculateVF6( + dryScore.scoreData.grade as Grades["sdvx:Single"], + dryScore.scoreData.lamp as Lamps["sdvx:Single"], + dryScore.scoreData.percent, + chart.levelNum, + logger + ); - return { - VF6, - }; + return { + VF6, + }; } async function CalculateDataMuseca( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - ktRating: await CalculateKTRating( - dryScore, - "museca", - "Single", - chart, - logger, - defaultTierlistID - ), - }; + return { + ktRating: await CalculateKTRating( + dryScore, + "museca", + "Single", + chart, + logger, + defaultTierlistID + ), + }; } function CalculateDataCHUNITHM( - dryScore: DryScore, - chart: AnyChartDocument + dryScore: DryScore, + chart: AnyChartDocument ): CalculatedData<"chunithm:Single"> { - return { - rating: CalculateCHUNITHMRating(dryScore, chart), - }; + return { + rating: CalculateCHUNITHMRating(dryScore, chart), + }; } async function CalculateDataMaimai( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - ktRating: await CalculateKTRating( - dryScore, - "maimai", - "Single", - chart, - logger, - defaultTierlistID - ), - }; + return { + ktRating: await CalculateKTRating( + dryScore, + "maimai", + "Single", + chart, + logger, + defaultTierlistID + ), + }; } function CalculateDataGitadora( - dryScore: DryScore, - chart: AnyChartDocument + dryScore: DryScore, + chart: AnyChartDocument ): CalculatedData<"gitadora:Gita" | "gitadora:Dora"> { - return { - skill: CalculateGITADORASkill(dryScore, chart), - }; + return { + skill: CalculateGITADORASkill(dryScore, chart), + }; } async function CalculateDataDDR( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - MFCP: CalculateMFCP(dryScore, chart, logger), - ktRating: await CalculateKTRating( - dryScore, - "museca", - "Single", - chart, - logger, - defaultTierlistID - ), - }; + return { + MFCP: CalculateMFCP(dryScore, chart, logger), + ktRating: await CalculateKTRating( + dryScore, + "museca", + "Single", + chart, + logger, + defaultTierlistID + ), + }; } async function CalculateDataBMS14K( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - ktLampRating: await CalculateKTLampRating(dryScore, "bms", "14K", chart, defaultTierlistID), - }; + return { + ktLampRating: await CalculateKTLampRating(dryScore, "bms", "14K", chart, defaultTierlistID), + }; } async function CalculateDataBMS7K( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - ktLampRating: await CalculateKTLampRating(dryScore, "bms", "7K", chart, defaultTierlistID), - }; + return { + ktLampRating: await CalculateKTLampRating(dryScore, "bms", "7K", chart, defaultTierlistID), + }; } async function CalculateDataJubeat( - dryScore: DryScore, - chart: AnyChartDocument, - defaultTierlistID: string | undefined, - logger: KtLogger + dryScore: DryScore, + chart: AnyChartDocument, + defaultTierlistID: string | undefined, + logger: KtLogger ): Promise> { - return { - jubility: 0, // @todo #163 Jubeat Jubility - }; + return { + jubility: 0, // @todo #163 Jubeat Jubility + }; } diff --git a/server/src/lib/score-import/framework/calculated-data/stats.test.ts b/server/src/lib/score-import/framework/calculated-data/stats.test.ts index 345aaafdb..77d0fc35d 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.test.ts @@ -2,546 +2,546 @@ import t from "tap"; import db from "../../../../external/mongo/db"; import { AnyChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common"; import { - CalculateBPI, - CalculateCHUNITHMRating, - CalculateGITADORASkill, - CalculateKESDC, - CalculateMFCP, - KaidenPercentile, - CalculateKTLampRating, - CalculateKTRating, - CalculateVF6, + CalculateBPI, + CalculateCHUNITHMRating, + CalculateGITADORASkill, + CalculateKESDC, + CalculateMFCP, + KaidenPercentile, + CalculateKTLampRating, + CalculateKTRating, + CalculateVF6, } from "./stats"; import CreateLogCtx from "../../../logger/logger"; import { isApproximately } from "../../../../test-utils/asserts"; import { DryScore } from "../common/types"; import { - GetKTDataJSON, - Testing511SPA, - TestingIIDXSPDryScore, + GetKTDataJSON, + Testing511SPA, + TestingIIDXSPDryScore, } from "../../../../test-utils/test-data"; import { CloseAllConnections } from "../../../../test-utils/close-connections"; import deepmerge from "deepmerge"; import ResetDBState from "../../../../test-utils/resets"; t.test("#CalculateBPI", (t) => { - t.test("AA BPI tests", (t) => { - // data accurate as of 14/04/2021 - const WR_AA = 3650; - const KAVG_AA = 3204; - const MAX_AA = 1834 * 2; - const COEF_AA = 1.25945; + t.test("AA BPI tests", (t) => { + // data accurate as of 14/04/2021 + const WR_AA = 3650; + const KAVG_AA = 3204; + const MAX_AA = 1834 * 2; + const COEF_AA = 1.25945; - // AA, the song. - function AA_BPI(yourEx: number) { - return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA); - } + // AA, the song. + function AA_BPI(yourEx: number) { + return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA); + } - t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI"); - isApproximately(AA_BPI(3393), 10.02, "A score with 3393 should be approximately 10.02BPI"); - isApproximately(AA_BPI(3481), 20.09, "A score with 3481 should be approximately 20.09BPI"); - isApproximately(AA_BPI(3535), 30.01, "A score with 3535 should be approximately 30.01BPI"); - isApproximately(AA_BPI(3572), 40.18, "A score with 3572 should be approximately 40.18BPI"); - isApproximately(AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI"); - isApproximately(AA_BPI(3615), 60.12, "A score with 3615 should be approximately 60.12BPI"); - isApproximately(AA_BPI(3628), 70.11, "A score with 3628 should be approximately 70.11BPI"); - isApproximately(AA_BPI(3638), 80.62, "A score with 3638 should be approximately 80.62BPI"); - isApproximately(AA_BPI(3645), 90.59, "A score with 3645 should be approximately 90.59BPI"); - t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI"); + t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI"); + isApproximately(AA_BPI(3393), 10.02, "A score with 3393 should be approximately 10.02BPI"); + isApproximately(AA_BPI(3481), 20.09, "A score with 3481 should be approximately 20.09BPI"); + isApproximately(AA_BPI(3535), 30.01, "A score with 3535 should be approximately 30.01BPI"); + isApproximately(AA_BPI(3572), 40.18, "A score with 3572 should be approximately 40.18BPI"); + isApproximately(AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI"); + isApproximately(AA_BPI(3615), 60.12, "A score with 3615 should be approximately 60.12BPI"); + isApproximately(AA_BPI(3628), 70.11, "A score with 3628 should be approximately 70.11BPI"); + isApproximately(AA_BPI(3638), 80.62, "A score with 3638 should be approximately 80.62BPI"); + isApproximately(AA_BPI(3645), 90.59, "A score with 3645 should be approximately 90.59BPI"); + t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI"); - isApproximately(AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI"); - isApproximately(AA_BPI(2886), -9.99, "A score with 3638 should be approximately -9.99BPI"); + isApproximately(AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI"); + isApproximately(AA_BPI(2886), -9.99, "A score with 3638 should be approximately -9.99BPI"); - t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); + t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); - isApproximately(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI"); + isApproximately(AA_BPI(MAX_AA), 244.56, "A score of MAX should be approximately 244.56BPI"); - t.end(); - }); + t.end(); + }); - // This song has both no co-efficient and is an sp11, so it serves - // as another interesting test. - t.test("Afterimage d'automne BPI tests", (t) => { - // data accurate as of 14/04/2021 - const WR_AFT = 2891; - const KAVG_AFT = 2497; - const MAX_AFT = 1480 * 2; - const COEF_AFT = null; + // This song has both no co-efficient and is an sp11, so it serves + // as another interesting test. + t.test("Afterimage d'automne BPI tests", (t) => { + // data accurate as of 14/04/2021 + const WR_AFT = 2891; + const KAVG_AFT = 2497; + const MAX_AFT = 1480 * 2; + const COEF_AFT = null; - function AFT_BPI(yourEx: number) { - return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT); - } + function AFT_BPI(yourEx: number) { + return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT); + } - t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI"); - isApproximately(AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI"); - isApproximately(AFT_BPI(2675), 20.07, "A score with 2675 should be approximately 20.07BPI"); - isApproximately(AFT_BPI(2727), 30.18, "A score with 2727 should be approximately 30.18BPI"); - isApproximately(AFT_BPI(2767), 40.12, "A score with 2767 should be approximately 40.12BPI"); - isApproximately(AFT_BPI(2799), 50.06, "A score with 2799 should be approximately 50.06BPI"); - isApproximately(AFT_BPI(2826), 60.43, "A score with 2826 should be approximately 60.43BPI"); - isApproximately(AFT_BPI(2847), 70.3, "A score with 2847 should be approximately 70.3BPI"); - isApproximately(AFT_BPI(2865), 80.57, "A score with 2865 should be approximately 80.57BPI"); - isApproximately(AFT_BPI(2879), 90.18, "A score with 2879 should be approximately 90.18BPI"); - t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI"); + t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI"); + isApproximately(AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI"); + isApproximately(AFT_BPI(2675), 20.07, "A score with 2675 should be approximately 20.07BPI"); + isApproximately(AFT_BPI(2727), 30.18, "A score with 2727 should be approximately 30.18BPI"); + isApproximately(AFT_BPI(2767), 40.12, "A score with 2767 should be approximately 40.12BPI"); + isApproximately(AFT_BPI(2799), 50.06, "A score with 2799 should be approximately 50.06BPI"); + isApproximately(AFT_BPI(2826), 60.43, "A score with 2826 should be approximately 60.43BPI"); + isApproximately(AFT_BPI(2847), 70.3, "A score with 2847 should be approximately 70.3BPI"); + isApproximately(AFT_BPI(2865), 80.57, "A score with 2865 should be approximately 80.57BPI"); + isApproximately(AFT_BPI(2879), 90.18, "A score with 2879 should be approximately 90.18BPI"); + t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI"); - isApproximately(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI"); - isApproximately( - AFT_BPI(2354), - -10.04, - "A score with 2354 should be approximately -10.04BPI" - ); + isApproximately(AFT_BPI(2423), -4.99, "A score with 2423 should be approximately -4.99BPI"); + isApproximately( + AFT_BPI(2354), + -10.04, + "A score with 2354 should be approximately -10.04BPI" + ); - t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); + t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); - isApproximately( - AFT_BPI(MAX_AFT), - 431.57, - "A score of MAX should be approximately 431.57BPI" - ); + isApproximately( + AFT_BPI(MAX_AFT), + 431.57, + "A score of MAX should be approximately 431.57BPI" + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#CalculateGITADORARating", (t) => { - function TestGitadoraRating(percent: number, levelNum: number) { - return CalculateGITADORASkill( - { scoreData: { percent } } as DryScore, - { levelNum } as AnyChartDocument - ); - } + function TestGitadoraRating(percent: number, levelNum: number) { + return CalculateGITADORASkill( + { scoreData: { percent } } as DryScore, + { levelNum } as AnyChartDocument + ); + } - t.equal( - TestGitadoraRating(89.48, 3.4), - 60.84, - "Test GITADORA Rating function aligns with game (1)" - ); + t.equal( + TestGitadoraRating(89.48, 3.4), + 60.84, + "Test GITADORA Rating function aligns with game (1)" + ); - t.equal( - TestGitadoraRating(70.76, 5.8), - 82.08, - "Test GITADORA Rating function aligns with game (2)" - ); + t.equal( + TestGitadoraRating(70.76, 5.8), + 82.08, + "Test GITADORA Rating function aligns with game (2)" + ); - t.end(); + t.end(); }); // this metric is ground to be removed, possibly. t.test("#CalculateKESDC", (t) => { - t.equal(CalculateKESDC(10, 4), 37.18460876390943, "Should perform ESDC on two numbers."); - t.equal(CalculateKESDC(null, 4), null, "Should return null if kaidenESD is null."); + t.equal(CalculateKESDC(10, 4), 37.18460876390943, "Should perform ESDC on two numbers."); + t.equal(CalculateKESDC(null, 4), null, "Should return null if kaidenESD is null."); - t.end(); + t.end(); }); const logger = CreateLogCtx(__filename); t.test("#CalculateMFCP", (t) => { - function TestMFCP( - lamp: Lamps["ddr:SP" | "ddr:DP"], - levelNum: number, - difficulty: Difficulties["ddr:DP" | "ddr:SP"] - ) { - return CalculateMFCP( - { - scoreData: { - lamp, - }, - } as ScoreDocument, - { - difficulty, - levelNum, - } as AnyChartDocument, - logger - ); - } + function TestMFCP( + lamp: Lamps["ddr:SP" | "ddr:DP"], + levelNum: number, + difficulty: Difficulties["ddr:DP" | "ddr:SP"] + ) { + return CalculateMFCP( + { + scoreData: { + lamp, + }, + } as ScoreDocument, + { + difficulty, + levelNum, + } as AnyChartDocument, + logger + ); + } - t.equal(TestMFCP("FAILED", 10, "EXPERT"), null, "Should return null for non-mfcs"); + t.equal(TestMFCP("FAILED", 10, "EXPERT"), null, "Should return null for non-mfcs"); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 10, "BASIC"), - null, - "Should reject charts on BASIC difficulty." - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 10, "BASIC"), + null, + "Should reject charts on BASIC difficulty." + ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 10, "BEGINNER"), - null, - "Should reject charts on BEGINNER difficulty." - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 10, "BEGINNER"), + null, + "Should reject charts on BEGINNER difficulty." + ); - t.test("Should return null for charts with level less than 8", (t) => { - for (let i = 1; i <= 7; i++) { - t.equal( - TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"), - null, - `Should return null for charts with level ${i}` - ); - } + t.test("Should return null for charts with level less than 8", (t) => { + for (let i = 1; i <= 7; i++) { + t.equal( + TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"), + null, + `Should return null for charts with level ${i}` + ); + } - t.end(); - }); + t.end(); + }); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 8, "EXPERT"), - 1, - "Should return 1 for charts with level 8" - ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 9, "EXPERT"), - 1, - "Should return 1 for charts with level 9" - ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 10, "EXPERT"), - 1, - "Should return 1 for charts with level 10" - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 8, "EXPERT"), + 1, + "Should return 1 for charts with level 8" + ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 9, "EXPERT"), + 1, + "Should return 1 for charts with level 9" + ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 10, "EXPERT"), + 1, + "Should return 1 for charts with level 10" + ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 11, "EXPERT"), - 2, - "Should return 2 for charts with level 11" - ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 12, "EXPERT"), - 2, - "Should return 2 for charts with level 12" - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 11, "EXPERT"), + 2, + "Should return 2 for charts with level 11" + ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 12, "EXPERT"), + 2, + "Should return 2 for charts with level 12" + ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 13, "EXPERT"), - 4, - "Should return 4 for charts with level 13" - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 13, "EXPERT"), + 4, + "Should return 4 for charts with level 13" + ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 14, "EXPERT"), - 8, - "Should return 8 for charts with level 14" - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 14, "EXPERT"), + 8, + "Should return 8 for charts with level 14" + ); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", 15, "EXPERT"), - 15, - "Should return 15 for charts with level 15" - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", 15, "EXPERT"), + 15, + "Should return 15 for charts with level 15" + ); - t.test("Should return 25 for charts with level 16-20", (t) => { - for (let i = 16; i <= 20; i++) { - t.equal( - TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"), - 25, - `Should return 25 for charts with level ${i}` - ); - } - t.end(); - }); + t.test("Should return 25 for charts with level 16-20", (t) => { + for (let i = 16; i <= 20; i++) { + t.equal( + TestMFCP("MARVELOUS FULL COMBO", i, "EXPERT"), + 25, + `Should return 25 for charts with level ${i}` + ); + } + t.end(); + }); - t.equal( - TestMFCP("MARVELOUS FULL COMBO", NaN, "EXPERT"), - null, - "Invalid level triggers failsafe." - ); + t.equal( + TestMFCP("MARVELOUS FULL COMBO", NaN, "EXPERT"), + null, + "Invalid level triggers failsafe." + ); - t.end(); + t.end(); }); const bbkk = GetKTDataJSON("./tachi/chunithm-bbkk-chart.json"); // unit testing a mathematical function is a square-round-hole problem. t.test("#CalculateCHUNITHMRating", (t) => { - // cutoffs - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, bbkk), 5.0); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, bbkk), 4.5); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, bbkk), 4); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, bbkk), 3); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, bbkk), 0); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, bbkk), 0); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, bbkk), 0); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, bbkk), 0); + // cutoffs + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, bbkk), 5.0); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, bbkk), 4.5); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, bbkk), 4); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, bbkk), 3); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, bbkk), 0); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, bbkk), 0); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, bbkk), 0); + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, bbkk), 0); - // inbetweens - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, bbkk), 3.48); + // inbetweens + t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, bbkk), 3.48); - t.end(); + t.end(); }); // Random assertions plucked from bemaniwiki. t.test("#CalculateVF6", (t) => { - t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332); + t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332); - t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356); + t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462); + t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369); + t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392); + t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415); + t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438); + t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462); - t.end(); + t.end(); }); t.test("#KaidenPercentile", async (t) => { - await db["iidx-eam-scores"].insert( - [100, 200, 300, 400, 1000, 2000, 3000, 4000].map((e) => ({ - chartID: Testing511SPA.chartID, - score: e, - lamp: "EASY CLEAR", - ranking: 0, // ignore - })) - ); + await db["iidx-eam-scores"].insert( + [100, 200, 300, 400, 1000, 2000, 3000, 4000].map((e) => ({ + chartID: Testing511SPA.chartID, + score: e, + lamp: "EASY CLEAR", + ranking: 0, // ignore + })) + ); - const res = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA); - t.equal(res, 50); + const res = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA); + t.equal(res, 50); - await db["iidx-eam-scores"].remove({}); - const res2 = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA); + await db["iidx-eam-scores"].remove({}); + const res2 = await KaidenPercentile(TestingIIDXSPDryScore, Testing511SPA); - t.equal(res2, null); + t.equal(res2, null); - t.end(); + t.end(); }); t.test("#CalculateRating", (t) => { - t.test("Should call the success calculator if percent > pivotPercent", async (t) => { - const r = await CalculateKTRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 80 } }), - "iidx", - "SP", - Testing511SPA, - logger - ); + t.test("Should call the success calculator if percent > pivotPercent", async (t) => { + const r = await CalculateKTRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 80 } }), + "iidx", + "SP", + Testing511SPA, + logger + ); - t.ok(r > 10, "Should return rating greater than the levelNum of the chart."); + t.ok(r > 10, "Should return rating greater than the levelNum of the chart."); - t.end(); - }); + t.end(); + }); - t.test("Should call the fail calculator if percent > pivotPercent", async (t) => { - const r = await CalculateKTRating( - TestingIIDXSPDryScore, - "iidx", - "SP", - Testing511SPA, - logger - ); + t.test("Should call the fail calculator if percent > pivotPercent", async (t) => { + const r = await CalculateKTRating( + TestingIIDXSPDryScore, + "iidx", + "SP", + Testing511SPA, + logger + ); - t.ok(r < 10, "Should return rating less than the levelNum of the chart."); + t.ok(r < 10, "Should return rating less than the levelNum of the chart."); - t.end(); - }); + t.end(); + }); - t.test("Should call levelNum if percent === pivotPercent", async (t) => { - const r = await CalculateKTRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 77.7777 } }), - "iidx", - "SP", - Testing511SPA, - logger - ); + t.test("Should call levelNum if percent === pivotPercent", async (t) => { + const r = await CalculateKTRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 77.7777 } }), + "iidx", + "SP", + Testing511SPA, + logger + ); - t.equal( - // hack for approximate tests - parseFloat(r.toFixed(2)), - 10, - "Should return rating exactly that of the levelNum of the chart." - ); + t.equal( + // hack for approximate tests + parseFloat(r.toFixed(2)), + 10, + "Should return rating exactly that of the levelNum of the chart." + ); - t.end(); - }); + t.end(); + }); - t.test( - "Should trigger safety if completely invalid percent somehow gets through", - async (t) => { - let r = await CalculateKTRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 1000000000 } }), - "iidx", - "SP", - Testing511SPA, - logger - ); + t.test( + "Should trigger safety if completely invalid percent somehow gets through", + async (t) => { + let r = await CalculateKTRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 1000000000 } }), + "iidx", + "SP", + Testing511SPA, + logger + ); - t.equal(r, 0, "Should safely return 0 and log a warning."); + t.equal(r, 0, "Should safely return 0 and log a warning."); - r = await CalculateKTRating( - // not high enough to be non-finite but high enough to be > 1000 - deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 200 } }), - "iidx", - "SP", - Testing511SPA, - logger - ); + r = await CalculateKTRating( + // not high enough to be non-finite but high enough to be > 1000 + deepmerge(TestingIIDXSPDryScore, { scoreData: { percent: 200 } }), + "iidx", + "SP", + Testing511SPA, + logger + ); - t.equal(r, 0, "Should safely return 0 and log a warning."); + t.equal(r, 0, "Should safely return 0 and log a warning."); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); t.test("#CalculateLampRating", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const defaultTierlist = { - createdAt: 1620150338858, - tierlistID: "ee9b756e50cff8282091102257b01f423ef855f2", - createdBy: 1, - description: "The official Kamaitachi Tierlist for beatmania IIDX (SP).", - game: "iidx", - playtype: "SP", - isDefault: true, - name: "Kamaitachi IIDX SP Official", - lastUpdated: 1620150338858, - permissions: { - anyPlayer: { - edit: 0, - submit: 1, - vote: 1, - }, - }, - config: { - autoHumanise: false, - flags: ["Individual Difference"], - requireState: "clear", - }, - }; + const defaultTierlist = { + createdAt: 1620150338858, + tierlistID: "ee9b756e50cff8282091102257b01f423ef855f2", + createdBy: 1, + description: "The official Kamaitachi Tierlist for beatmania IIDX (SP).", + game: "iidx", + playtype: "SP", + isDefault: true, + name: "Kamaitachi IIDX SP Official", + lastUpdated: 1620150338858, + permissions: { + anyPlayer: { + edit: 0, + submit: 1, + vote: 1, + }, + }, + config: { + autoHumanise: false, + flags: ["Individual Difference"], + requireState: "clear", + }, + }; - t.test("TierlistData", async (t) => { - const lampRating = await CalculateKTLampRating( - TestingIIDXSPDryScore, - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + t.test("TierlistData", async (t) => { + const lampRating = await CalculateKTLampRating( + TestingIIDXSPDryScore, + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRating, 10, "Should equal the levelNum of the chart."); + t.equal(lampRating, 10, "Should equal the levelNum of the chart."); - const lampRatingFail = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingFail = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); + t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); - const lampRatingHC = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingHC = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRatingHC, 10.6, "Should equal the tierlist value for HC on this chart."); + t.equal(lampRatingHC, 10.6, "Should equal the tierlist value for HC on this chart."); - const lampRatingEXHC = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingEXHC = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - // deliberate - there is no EXHC data here - t.equal(lampRatingEXHC, 10.6, "Should equal the tierlist value for HC on this chart."); + // deliberate - there is no EXHC data here + t.equal(lampRatingEXHC, 10.6, "Should equal the tierlist value for HC on this chart."); - t.end(); - }); + t.end(); + }); - t.test("TierlistData edge cases", async (t) => { - // mock document that implies 5.1.1. SPA is worth 11.9 to NC and 10.6 to HC. - await db["tierlist-data"].insert({ - chartID: Testing511SPA.chartID, - type: "lamp", - key: "CLEAR", - tierlistDataID: "asdf", - tierlistID: defaultTierlist!.tierlistID, - data: { - flags: {}, - humanised: "a", - value: 11.9, - }, - }); + t.test("TierlistData edge cases", async (t) => { + // mock document that implies 5.1.1. SPA is worth 11.9 to NC and 10.6 to HC. + await db["tierlist-data"].insert({ + chartID: Testing511SPA.chartID, + type: "lamp", + key: "CLEAR", + tierlistDataID: "asdf", + tierlistID: defaultTierlist!.tierlistID, + data: { + flags: {}, + humanised: "a", + value: 11.9, + }, + }); - const lampRating = await CalculateKTLampRating( - TestingIIDXSPDryScore, - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRating = await CalculateKTLampRating( + TestingIIDXSPDryScore, + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRating, 11.9, "Should equal the NC value of the chart."); + t.equal(lampRating, 11.9, "Should equal the NC value of the chart."); - const lampRatingFail = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingFail = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); + t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); - const lampRatingHC = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingHC = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "HARD CLEAR" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal( - lampRatingHC, - 11.9, - "Should equal the tierlist value for NC, as it is higher than HC on this chart." - ); + t.equal( + lampRatingHC, + 11.9, + "Should equal the tierlist value for NC, as it is higher than HC on this chart." + ); - const lampRatingEXHC = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }), - "iidx", - "SP", - Testing511SPA, - defaultTierlist!.tierlistID - ); + const lampRatingEXHC = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "EX HARD CLEAR" } }), + "iidx", + "SP", + Testing511SPA, + defaultTierlist!.tierlistID + ); - t.equal(lampRatingEXHC, 11.9, "Should equal the tierlist value for NC on this chart."); + t.equal(lampRatingEXHC, 11.9, "Should equal the tierlist value for NC on this chart."); - t.end(); - }); + t.end(); + }); - t.test("No TierlistData", async (t) => { - await db["tierlist-data"].remove({}); - const lampRating = await CalculateKTLampRating( - TestingIIDXSPDryScore, - "iidx", - "SP", - Testing511SPA - ); + t.test("No TierlistData", async (t) => { + await db["tierlist-data"].remove({}); + const lampRating = await CalculateKTLampRating( + TestingIIDXSPDryScore, + "iidx", + "SP", + Testing511SPA + ); - t.equal(lampRating, 10, "Should equal the levelNum of the chart."); + t.equal(lampRating, 10, "Should equal the levelNum of the chart."); - const lampRatingFail = await CalculateKTLampRating( - deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), - "iidx", - "SP", - Testing511SPA - ); + const lampRatingFail = await CalculateKTLampRating( + deepmerge(TestingIIDXSPDryScore, { scoreData: { lamp: "FAILED" } }), + "iidx", + "SP", + Testing511SPA + ); - t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); + t.equal(lampRatingFail, 0, "Should equal 0, if the score is not a clear."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/calculated-data/stats.ts b/server/src/lib/score-import/framework/calculated-data/stats.ts index aab1cec05..b17312872 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.ts @@ -9,37 +9,37 @@ import { lamps, clearLamp, ratingParameters } from "tachi-common/js/config"; * Calculates the in-game CHUNITHM rating for a score. */ export function CalculateCHUNITHMRating(dryScore: DryScore, chartData: AnyChartDocument) { - const score = dryScore.scoreData.score; - const levelBase = chartData.levelNum * 100; + const score = dryScore.scoreData.score; + const levelBase = chartData.levelNum * 100; - let val = 0; + let val = 0; - if (score >= 1_007_500) { - val = levelBase + 200; - } else if (score >= 1_005_000) { - val = levelBase + 150 + ((score - 1_005_000) * 10) / 500; - } else if (score >= 1_000_000) { - val = levelBase + 100 + ((score - 1_000_000) * 5) / 100; - } else if (score >= 975_000) { - val = levelBase + ((score - 975_000) * 2) / 500; - } else if (score >= 925_000) { - val = levelBase - 300 + ((score - 925_000) * 3) / 500; - } else if (score >= 900_000) { - val = levelBase - 500 + ((score - 900_000) * 4) / 500; - } else if (score >= 800_000) { - val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000; - } + if (score >= 1_007_500) { + val = levelBase + 200; + } else if (score >= 1_005_000) { + val = levelBase + 150 + ((score - 1_005_000) * 10) / 500; + } else if (score >= 1_000_000) { + val = levelBase + 100 + ((score - 1_000_000) * 5) / 100; + } else if (score >= 975_000) { + val = levelBase + ((score - 975_000) * 2) / 500; + } else if (score >= 925_000) { + val = levelBase - 300 + ((score - 925_000) * 3) / 500; + } else if (score >= 900_000) { + val = levelBase - 500 + ((score - 900_000) * 4) / 500; + } else if (score >= 800_000) { + val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000; + } - return Math.max(Math.floor(val) / 100, 0); + return Math.max(Math.floor(val) / 100, 0); } /** * Calculates the in-game GITADORA rating for a score. */ export function CalculateGITADORASkill(dryScore: DryScore, chartData: AnyChartDocument) { - const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20; - const flooredRating = Math.floor(trueRating * 100) / 100; - return flooredRating; + const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20; + const flooredRating = Math.floor(trueRating * 100) / 100; + return flooredRating; } /** @@ -47,7 +47,7 @@ export function CalculateGITADORASkill(dryScore: DryScore, chartData: AnyChartDo * @returns */ function BPIPikaGreatFn(score: integer, max: integer) { - return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max); + return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max); } /** @@ -66,35 +66,35 @@ function BPIPikaGreatFn(score: integer, max: integer) { * returns can be above 100. */ export function CalculateBPI( - kaidenEx: integer, - wrEx: integer, - yourEx: integer, - max: integer, - pc: number | null + kaidenEx: integer, + wrEx: integer, + yourEx: integer, + max: integer, + pc: number | null ) { - const powCoef = pc ?? 1.175; - const yourPGF = BPIPikaGreatFn(yourEx, max); - const kaidenPGF = BPIPikaGreatFn(kaidenEx, max); - const wrPGF = BPIPikaGreatFn(wrEx, max); + const powCoef = pc ?? 1.175; + const yourPGF = BPIPikaGreatFn(yourEx, max); + const kaidenPGF = BPIPikaGreatFn(kaidenEx, max); + const wrPGF = BPIPikaGreatFn(wrEx, max); - // no idea what these var names are - const _s_ = yourPGF / kaidenPGF, - _z_ = wrPGF / kaidenPGF; + // no idea what these var names are + const _s_ = yourPGF / kaidenPGF, + _z_ = wrPGF / kaidenPGF; - const isBetterThanKavg = yourEx >= kaidenEx; + const isBetterThanKavg = yourEx >= kaidenEx; - // this line of code isn't mine, and that's why it's *really* bad here. - return Math.max( - -15, - Math.round( - (isBetterThanKavg ? 100 : -100) * - Math.pow( - (isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_), - powCoef - ) * - 100 - ) / 100 - ); + // this line of code isn't mine, and that's why it's *really* bad here. + return Math.max( + -15, + Math.round( + (isBetterThanKavg ? 100 : -100) * + Math.pow( + (isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_), + powCoef + ) * + 100 + ) / 100 + ); } /** @@ -102,20 +102,20 @@ export function CalculateBPI( * @returns Null, if this chart has no kaidens, a percent between 0 and 100, if there are. */ export async function KaidenPercentile(scoreObj: DryScore, chartData: AnyChartDocument) { - const scoreCount = await db["iidx-eam-scores"].count({ - chartID: chartData.chartID, - }); + const scoreCount = await db["iidx-eam-scores"].count({ + chartID: chartData.chartID, + }); - if (!scoreCount) { - return null; - } + if (!scoreCount) { + return null; + } - const worseScores = await db["iidx-eam-scores"].count({ - chartID: chartData.chartID, - score: { $lt: scoreObj.scoreData.score }, - }); + const worseScores = await db["iidx-eam-scores"].count({ + chartID: chartData.chartID, + score: { $lt: scoreObj.scoreData.score }, + }); - return (100 * worseScores) / scoreCount; + return (100 * worseScores) / scoreCount; } /** @@ -123,10 +123,10 @@ export async function KaidenPercentile(scoreObj: DryScore, chartData: AnyChartDo * For those who know what ESDC is (me), this is just ESDC(kesd, your esd). */ export function CalculateKESDC(kaidenESD: number | null, yourESD: number) { - if (!kaidenESD) { - return null; - } - return ESDCore.ESDCompare(kaidenESD, yourESD); + if (!kaidenESD) { + return null; + } + return ESDCore.ESDCompare(kaidenESD, yourESD); } /** @@ -136,105 +136,105 @@ export function CalculateKESDC(kaidenESD: number | null, yourESD: number) { * @returns Null if this score was not eligible, a number otherwise. */ export function CalculateMFCP(dryScore: DryScore, chartData: AnyChartDocument, logger: KtLogger) { - if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") { - return null; - } + if (dryScore.scoreData.lamp !== "MARVELOUS FULL COMBO") { + return null; + } - // Beginner and BASIC scores are explicitly excluded. - if (chartData.difficulty === "BEGINNER" || chartData.difficulty === "BASIC") { - return null; - } + // Beginner and BASIC scores are explicitly excluded. + if (chartData.difficulty === "BEGINNER" || chartData.difficulty === "BASIC") { + return null; + } - if (chartData.levelNum < 8) { - return null; - } else if (chartData.levelNum <= 10) { - return 1; - } else if (chartData.levelNum <= 12) { - return 2; - } else if (chartData.levelNum === 13) { - return 4; - } else if (chartData.levelNum === 14) { - return 8; - } else if (chartData.levelNum === 15) { - return 15; - } else if (chartData.levelNum >= 16) { - return 25; - } + if (chartData.levelNum < 8) { + return null; + } else if (chartData.levelNum <= 10) { + return 1; + } else if (chartData.levelNum <= 12) { + return 2; + } else if (chartData.levelNum === 13) { + return 4; + } else if (chartData.levelNum === 14) { + return 8; + } else if (chartData.levelNum === 15) { + return 15; + } else if (chartData.levelNum >= 16) { + return 25; + } - logger.warn( - `Invalid levelNum passed to MFCP ${chartData.levelNum}. ChartID ${chartData.chartID}.` - ); + logger.warn( + `Invalid levelNum passed to MFCP ${chartData.levelNum}. ChartID ${chartData.chartID}.` + ); - // failsafe - return null; + // failsafe + return null; } const VF4GradeCoefficients = { - S: 1.0, - "AAA+": 0.99, - AAA: 0.98, - "AA+": 0.97, - AA: 0.96, - "A+": 0.95, - A: 0.94, - B: 0.93, - C: 0.92, - D: 0.91, + S: 1.0, + "AAA+": 0.99, + AAA: 0.98, + "AA+": 0.97, + AA: 0.96, + "A+": 0.95, + A: 0.94, + B: 0.93, + C: 0.92, + D: 0.91, }; const VF5GradeCoefficients = { - S: 1.05, - "AAA+": 1.02, - AAA: 1.0, - "AA+": 0.97, - AA: 0.94, - "A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki. - A: 0.88, - B: 0.85, - C: 0.82, - D: 0.8, + S: 1.05, + "AAA+": 1.02, + AAA: 1.0, + "AA+": 0.97, + AA: 0.94, + "A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki. + A: 0.88, + B: 0.85, + C: 0.82, + D: 0.8, }; const VF5LampCoefficients = { - "PERFECT ULTIMATE CHAIN": 1.1, - "ULTIMATE CHAIN": 1.05, - "EXCESSIVE CLEAR": 1.02, - CLEAR: 1.0, - FAILED: 0.5, + "PERFECT ULTIMATE CHAIN": 1.1, + "ULTIMATE CHAIN": 1.05, + "EXCESSIVE CLEAR": 1.02, + CLEAR: 1.0, + FAILED: 0.5, }; function FloorToNDP(number: number, dp: integer) { - const mul = 10 ** dp; - return Math.floor(number * mul) / mul; + const mul = 10 ** dp; + return Math.floor(number * mul) / mul; } export function CalculateVF6( - grade: Grades["sdvx:Single"], - lamp: Lamps["sdvx:Single"], - per: number, - levelNum: number, - logger: KtLogger + grade: Grades["sdvx:Single"], + lamp: Lamps["sdvx:Single"], + per: number, + levelNum: number, + logger: KtLogger ) { - const gradeCoefficient = VF5GradeCoefficients[grade]; - const lampCoefficient = VF5LampCoefficients[lamp]; + const gradeCoefficient = VF5GradeCoefficients[grade]; + const lampCoefficient = VF5LampCoefficients[lamp]; - if (!lampCoefficient) { - logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`); - return null; - } - if (!gradeCoefficient) { - logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`); - return null; - } + if (!lampCoefficient) { + logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`); + return null; + } + if (!gradeCoefficient) { + logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`); + return null; + } - const percent = per / 100; - if (!levelNum || !percent) { - return 0; - } + const percent = per / 100; + if (!levelNum || !percent) { + return 0; + } - const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100; + const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100; - return FloorToNDP(realVF6, 3); + return FloorToNDP(realVF6, 3); } // function CalculateJubility( @@ -246,151 +246,151 @@ export function CalculateVF6( // } interface RatingParameters { - failHarshnessMultiplier: number; - pivotPercent: number; - clearExpMultiplier: number; + failHarshnessMultiplier: number; + pivotPercent: number; + clearExpMultiplier: number; } // Generic Rating Calc that is guaranteed to work for everything. This is unspecialised, and not great. function KTRatingCalcV1( - percent: number, - levelNum: number, - parameters: RatingParameters, - logger: KtLogger + percent: number, + levelNum: number, + parameters: RatingParameters, + logger: KtLogger ) { - const percentDiv100 = percent / 100; + const percentDiv100 = percent / 100; - if (percentDiv100 < parameters.pivotPercent) { - return RatingCalcV0Fail(percentDiv100, levelNum, parameters); - } + if (percentDiv100 < parameters.pivotPercent) { + return RatingCalcV0Fail(percentDiv100, levelNum, parameters); + } - return RatingCalcV1Clear(percentDiv100, levelNum, parameters, logger); + return RatingCalcV1Clear(percentDiv100, levelNum, parameters, logger); } function RatingCalcV1Clear( - percentDiv100: number, - levelNum: number, - parameters: RatingParameters, - logger: KtLogger + percentDiv100: number, + levelNum: number, + parameters: RatingParameters, + logger: KtLogger ) { - // https://www.desmos.com/calculator/hn7uxjmjkc + // https://www.desmos.com/calculator/hn7uxjmjkc - const rating = - Math.cosh( - parameters.clearExpMultiplier * levelNum * (percentDiv100 - parameters.pivotPercent) - ) + - (levelNum - 1); + const rating = + Math.cosh( + parameters.clearExpMultiplier * levelNum * (percentDiv100 - parameters.pivotPercent) + ) + + (levelNum - 1); - // checks for Infinity or NaN. I'm not sure how this would happen, but it's a failsafe. - if (!Number.isFinite(rating)) { - logger.warn( - `Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid. Defaulting to 0.` - ); - return 0; - } else if (rating > 1000) { - logger.warn( - `Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid (> 1000). Defaulting to 0.` - ); - return 0; - } + // checks for Infinity or NaN. I'm not sure how this would happen, but it's a failsafe. + if (!Number.isFinite(rating)) { + logger.warn( + `Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid. Defaulting to 0.` + ); + return 0; + } else if (rating > 1000) { + logger.warn( + `Percent: ${percentDiv100}, Level: ${levelNum} resulted in rating of ${rating}, which is invalid (> 1000). Defaulting to 0.` + ); + return 0; + } - return rating; + return rating; } function RatingCalcV0Fail(percentDiv100: number, levelNum: number, parameters: RatingParameters) { - // https://www.desmos.com/calculator/ngjie5elto - return ( - percentDiv100 ** (parameters.failHarshnessMultiplier * levelNum) * - (levelNum / parameters.pivotPercent ** (parameters.failHarshnessMultiplier * levelNum)) - ); + // https://www.desmos.com/calculator/ngjie5elto + return ( + percentDiv100 ** (parameters.failHarshnessMultiplier * levelNum) * + (levelNum / parameters.pivotPercent ** (parameters.failHarshnessMultiplier * levelNum)) + ); } export async function CalculateKTRating( - dryScore: DryScore, - game: Game, - playtype: Playtypes[Game], - chart: AnyChartDocument, - logger: KtLogger, - defaultTierlistID?: string + dryScore: DryScore, + game: Game, + playtype: Playtypes[Game], + chart: AnyChartDocument, + logger: KtLogger, + defaultTierlistID?: string ) { - // If this game doesn't have a specific rating function declared, fall back to the default "generic" rating function. - // This is just a function that is guaranteed to work for all input - and therefore not result in lower-skilled users - // always seeing 0. - const parameters = ratingParameters[dryScore.game]; + // If this game doesn't have a specific rating function declared, fall back to the default "generic" rating function. + // This is just a function that is guaranteed to work for all input - and therefore not result in lower-skilled users + // always seeing 0. + const parameters = ratingParameters[dryScore.game]; - let levelNum = chart.levelNum; + let levelNum = chart.levelNum; - if (defaultTierlistID) { - const tierlistData = await GetOneTierlistData( - game, - chart, - "score", - null, - defaultTierlistID - ); + if (defaultTierlistID) { + const tierlistData = await GetOneTierlistData( + game, + chart, + "score", + null, + defaultTierlistID + ); - if (tierlistData) { - levelNum = tierlistData.data.value; - } - } + if (tierlistData) { + levelNum = tierlistData.data.value; + } + } - return KTRatingCalcV1(dryScore.scoreData.percent, levelNum, parameters, logger); + return KTRatingCalcV1(dryScore.scoreData.percent, levelNum, parameters, logger); } export async function CalculateKTLampRating( - dryScore: DryScore, - game: Game, - playtype: Playtypes[Game], - chart: AnyChartDocument, - defaultTierlistID?: string + dryScore: DryScore, + game: Game, + playtype: Playtypes[Game], + chart: AnyChartDocument, + defaultTierlistID?: string ) { - // if no tierlist data - if (!defaultTierlistID) { - return LampRatingNoTierlistInfo(dryScore, game, chart); - } + // if no tierlist data + if (!defaultTierlistID) { + return LampRatingNoTierlistInfo(dryScore, game, chart); + } - const lampTierlistInfo = await GetAllTierlistDataOfType(game, chart, "lamp", defaultTierlistID); + const lampTierlistInfo = await GetAllTierlistDataOfType(game, chart, "lamp", defaultTierlistID); - // if no tierlist info - if (lampTierlistInfo.length === 0) { - return LampRatingNoTierlistInfo(dryScore, game, chart); - } + // if no tierlist info + if (lampTierlistInfo.length === 0) { + return LampRatingNoTierlistInfo(dryScore, game, chart); + } - const userLampIndex = lamps[game].indexOf(dryScore.scoreData.lamp); + const userLampIndex = lamps[game].indexOf(dryScore.scoreData.lamp); - // if this score is a clear, the lowest we should go is the levelNum of the chart. - // Else, the lowest we can go is 0. - let lampRating = userLampIndex >= lamps[game].indexOf(clearLamp[game]) ? chart.levelNum : 0; + // if this score is a clear, the lowest we should go is the levelNum of the chart. + // Else, the lowest we can go is 0. + let lampRating = userLampIndex >= lamps[game].indexOf(clearLamp[game]) ? chart.levelNum : 0; - // why is this like this and not a lookup table? - // Some charts have higher values for *lower* lamps, such as - // one more lovely in IIDX being harder to NC than it is to HC! - // this means we have to iterate over all tierlist data, in general. - for (const tierlistData of lampTierlistInfo) { - if ( - // this tierlistData has higher value than the current lampRating - tierlistData.data.value > lampRating && - // and your clear is better than the lamp its for - lamps[game].indexOf(tierlistData.key!) <= userLampIndex - ) { - lampRating = tierlistData.data.value; - } - } + // why is this like this and not a lookup table? + // Some charts have higher values for *lower* lamps, such as + // one more lovely in IIDX being harder to NC than it is to HC! + // this means we have to iterate over all tierlist data, in general. + for (const tierlistData of lampTierlistInfo) { + if ( + // this tierlistData has higher value than the current lampRating + tierlistData.data.value > lampRating && + // and your clear is better than the lamp its for + lamps[game].indexOf(tierlistData.key!) <= userLampIndex + ) { + lampRating = tierlistData.data.value; + } + } - return lampRating; + return lampRating; } function LampRatingNoTierlistInfo(dryScore: DryScore, game: Game, chart: AnyChartDocument) { - const CLEAR_LAMP_INDEX = lamps[game].indexOf(clearLamp[game]); + const CLEAR_LAMP_INDEX = lamps[game].indexOf(clearLamp[game]); - // if this is a clear - if (lamps[game].indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) { - // return this chart's numeric level as the lamp rating - return chart.levelNum; - } + // if this is a clear + if (lamps[game].indexOf(dryScore.scoreData.lamp) >= CLEAR_LAMP_INDEX) { + // return this chart's numeric level as the lamp rating + return chart.levelNum; + } - // else, this score is worth 0. - return 0; + // else, this score is worth 0. + return 0; } // deprecated calcs diff --git a/server/src/lib/score-import/framework/common/converter-failures.ts b/server/src/lib/score-import/framework/common/converter-failures.ts index 81815d66a..a74124de6 100644 --- a/server/src/lib/score-import/framework/common/converter-failures.ts +++ b/server/src/lib/score-import/framework/common/converter-failures.ts @@ -5,11 +5,11 @@ import { ImportTypes } from "tachi-common"; import { ImportTypeDataMap, ImportTypeContextMap } from "../../import-types/common/types"; export class ConverterFailure { - message: string | null; + message: string | null; - constructor(message: string | null) { - this.message = message; - } + constructor(message: string | null) { + this.message = message; + } } /** @@ -25,22 +25,22 @@ export class SkipScoreFailure extends ConverterFailure {} * Alongside any other fields used in the query. */ export class KTDataNotFoundFailure extends ConverterFailure { - data: ImportTypeDataMap[T]; - converterContext: ImportTypeContextMap[T]; - importType: T; + data: ImportTypeDataMap[T]; + converterContext: ImportTypeContextMap[T]; + importType: T; - constructor( - message: string | null, - importType: T, - data: ImportTypeDataMap[T], - context: ImportTypeContextMap[T] - ) { - super(message); + constructor( + message: string | null, + importType: T, + data: ImportTypeDataMap[T], + context: ImportTypeContextMap[T] + ) { + super(message); - this.importType = importType; - this.data = data; - this.converterContext = context; - } + this.importType = importType; + this.data = data; + this.converterContext = context; + } } /** diff --git a/server/src/lib/score-import/framework/common/import-logger.test.ts b/server/src/lib/score-import/framework/common/import-logger.test.ts index 67bcc3def..b75776376 100644 --- a/server/src/lib/score-import/framework/common/import-logger.test.ts +++ b/server/src/lib/score-import/framework/common/import-logger.test.ts @@ -4,19 +4,19 @@ import { PublicUserDocument } from "tachi-common"; import { CloseAllConnections } from "../../../../test-utils/close-connections"; t.test("#CreateImportLoggerAndID", (t) => { - const { importID, logger } = CreateImportLoggerAndID( - { username: "foo", id: 1 } as PublicUserDocument, - "file/eamusement-iidx-csv" - ); + const { importID, logger } = CreateImportLoggerAndID( + { username: "foo", id: 1 } as PublicUserDocument, + "file/eamusement-iidx-csv" + ); - t.match(importID, /^[a-f0-9]{40}$/u, "Should return a 40 character importID."); + t.match(importID, /^[a-f0-9]{40}$/u, "Should return a 40 character importID."); - // not possible to automate this, probably. - logger.info( - "Namespaced log test - if this does not have 'file/eamusement-iidx-csv | foo (#1)' next to it, it's broke." - ); + // not possible to automate this, probably. + logger.info( + "Namespaced log test - if this does not have 'file/eamusement-iidx-csv | foo (#1)' next to it, it's broke." + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/common/import-logger.ts b/server/src/lib/score-import/framework/common/import-logger.ts index ac10b89ae..be3376931 100644 --- a/server/src/lib/score-import/framework/common/import-logger.ts +++ b/server/src/lib/score-import/framework/common/import-logger.ts @@ -4,24 +4,24 @@ import { FormatUserDoc } from "../../../../utils/user"; import { rootLogger, KtLogger } from "../../../logger/logger"; export function CreateImportLoggerAndID(userDoc: PublicUserDocument, importType: ImportTypes) { - const importID = crypto.randomBytes(20).toString("hex"); - return { logger: CreateScoreLogger(userDoc, importID, importType), importID }; + const importID = crypto.randomBytes(20).toString("hex"); + return { logger: CreateScoreLogger(userDoc, importID, importType), importID }; } export function CreateScoreLogger( - user: PublicUserDocument, - importID: string, - importType: ImportTypes + user: PublicUserDocument, + importID: string, + importType: ImportTypes ): KtLogger { - const meta = { - context: ["Score Import", importType, FormatUserDoc(user)], - importID, - }; + const meta = { + context: ["Score Import", importType, FormatUserDoc(user)], + importID, + }; - // used so appendLogCtx works - const childLogger = rootLogger.child(meta); + // used so appendLogCtx works + const childLogger = rootLogger.child(meta); - childLogger.defaultMeta = meta; + childLogger.defaultMeta = meta; - return childLogger as KtLogger; + return childLogger as KtLogger; } diff --git a/server/src/lib/score-import/framework/common/score-utils.ts b/server/src/lib/score-import/framework/common/score-utils.ts index 725ff59c0..3e0d40fc8 100644 --- a/server/src/lib/score-import/framework/common/score-utils.ts +++ b/server/src/lib/score-import/framework/common/score-utils.ts @@ -1,13 +1,13 @@ import { - config, - Game, - Grades, - IDStrings, - AnyChartDocument, - ChartDocument, - GameToIDStrings, - ESDCore, - Playtypes, + config, + Game, + Grades, + IDStrings, + AnyChartDocument, + ChartDocument, + GameToIDStrings, + ESDCore, + Playtypes, } from "tachi-common"; import { judgementWindows, gamePercentMax } from "tachi-common/js/config"; import CreateLogCtx from "../../../logger/logger"; @@ -19,22 +19,22 @@ const logger = CreateLogCtx(__filename); * Util for getting a games' grade for a given percent. */ export function GetGradeFromPercent( - game: Game, - percent: number + game: Game, + percent: number ): Grades[I] { - // @todo #102 update config to use game->pt - const boundaries = config.gradeBoundaries[game]; - const grades = config.grades[game]; + // @todo #102 update config to use game->pt + const boundaries = config.gradeBoundaries[game]; + const grades = config.grades[game]; - // eslint doesn't like backwards for loops (hey, this for loop is backwards!) - for (let i = boundaries.length; i >= 0; i--) { - if (percent >= boundaries[i]) { - return grades[i] as Grades[I]; - } - } + // eslint doesn't like backwards for loops (hey, this for loop is backwards!) + for (let i = boundaries.length; i >= 0; i--) { + if (percent >= boundaries[i]) { + return grades[i] as Grades[I]; + } + } - logger.error(`Could not resolve grade for percent ${percent} on game ${game}`); - throw new InternalFailure(`Could not resolve grade for percent ${percent} on game ${game}`); + logger.error(`Could not resolve grade for percent ${percent} on game ${game}`); + throw new InternalFailure(`Could not resolve grade for percent ${percent} on game ${game}`); } /** @@ -42,44 +42,44 @@ export function GetGradeFromPercent( * a given game. */ export function GenericCalculatePercent( - game: Game, - score: number, - chart?: AnyChartDocument + game: Game, + score: number, + chart?: AnyChartDocument ): number { - switch (game) { - case "ddr": - case "museca": - case "jubeat": - case "chunithm": - return (score / 1_000_000) * 100; - case "sdvx": - case "usc": - return (score / 10_000_000) * 100; - case "popn": - return (score / 100_000) * 100; - case "gitadora": - case "maimai": - return score; - case "bms": - case "iidx": - if (!chart) { - logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms."); - throw new InternalFailure( - "No Chart passed to GenericCalcPercent but game was iidx/bms." - ); - } + switch (game) { + case "ddr": + case "museca": + case "jubeat": + case "chunithm": + return (score / 1_000_000) * 100; + case "sdvx": + case "usc": + return (score / 10_000_000) * 100; + case "popn": + return (score / 100_000) * 100; + case "gitadora": + case "maimai": + return score; + case "bms": + case "iidx": + if (!chart) { + logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms."); + throw new InternalFailure( + "No Chart passed to GenericCalcPercent but game was iidx/bms." + ); + } - // Yeah, we declare it like this so the below return is actually clear. - // eslint-disable-next-line no-case-declarations - const MAX = - (chart as ChartDocument<"iidx:SP" | "bms:7K" | "bms:14K" | "iidx:DP">).data - .notecount * 2; + // Yeah, we declare it like this so the below return is actually clear. + // eslint-disable-next-line no-case-declarations + const MAX = + (chart as ChartDocument<"iidx:SP" | "bms:7K" | "bms:14K" | "iidx:DP">).data + .notecount * 2; - return (100 * score) / MAX; - default: - logger.severe(`Invalid game passed of ${game} to GenericCalcPercent.`); - throw new InternalFailure(`Invalid game passed of ${game} to GenericCalcPercent.`); - } + return (100 * score) / MAX; + default: + logger.severe(`Invalid game passed of ${game} to GenericCalcPercent.`); + throw new InternalFailure(`Invalid game passed of ${game} to GenericCalcPercent.`); + } } /** @@ -89,21 +89,21 @@ export function GenericCalculatePercent( * This exists to support maimai, as it has a dynamic "max percent". */ export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocument) { - // i love needing a helper function for *ONE* game. - if (game === "maimai") { - const mmChart = chart as ChartDocument<"maimai:Single">; - if (percent > mmChart.data.maxPercent) { - throw new InvalidScoreFailure( - `Invalid percent - expected less than ${mmChart.data.maxPercent}.` - ); - } - } + // i love needing a helper function for *ONE* game. + if (game === "maimai") { + const mmChart = chart as ChartDocument<"maimai:Single">; + if (percent > mmChart.data.maxPercent) { + throw new InvalidScoreFailure( + `Invalid percent - expected less than ${mmChart.data.maxPercent}.` + ); + } + } - if (percent > gamePercentMax[game]) { - throw new InvalidScoreFailure( - `Invalid percent of ${percent} - expected a value less than ${gamePercentMax[game]}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).` - ); - } + if (percent > gamePercentMax[game]) { + throw new InvalidScoreFailure( + `Invalid percent of ${percent} - expected a value less than ${gamePercentMax[game]}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).` + ); + } } /** @@ -112,17 +112,17 @@ export function ValidatePercent(game: Game, percent: number, chart: AnyChartDocu * or if the grade is invalid. */ export function GenericGetGradeAndPercent( - game: G, - score: number, - chart: AnyChartDocument + game: G, + score: number, + chart: AnyChartDocument ) { - const percent = GenericCalculatePercent(game, score, chart); + const percent = GenericCalculatePercent(game, score, chart); - ValidatePercent(game, percent, chart); + ValidatePercent(game, percent, chart); - const grade = GetGradeFromPercent(game, percent) as Grades[GameToIDStrings[G]]; + const grade = GetGradeFromPercent(game, percent) as Grades[GameToIDStrings[G]]; - return { percent, grade }; + return { percent, grade }; } /** @@ -130,24 +130,24 @@ export function GenericGetGradeAndPercent( * null if the game does not support support ESD. */ export function CalculateESDForGame( - game: Game, - playtype: Playtypes[Game], - percent: number + game: Game, + playtype: Playtypes[Game], + percent: number ): number | null { - if ( - Object.prototype.hasOwnProperty.call(judgementWindows, game) && - // @ts-expect-error i know better - Object.prototype.hasOwnProperty.call(judgementWindows[game], playtype) - ) { - if (percent > 0.1) { - // @ts-expect-error i know better - return ESDCore.CalculateESD(judgementWindows[game][playtype], percent); - } else { - return 200; - } - } + if ( + Object.prototype.hasOwnProperty.call(judgementWindows, game) && + // @ts-expect-error i know better + Object.prototype.hasOwnProperty.call(judgementWindows[game], playtype) + ) { + if (percent > 0.1) { + // @ts-expect-error i know better + return ESDCore.CalculateESD(judgementWindows[game][playtype], percent); + } else { + return 200; + } + } - return null; + return null; } /** @@ -155,15 +155,15 @@ export function CalculateESDForGame( * @returns Miliseconds from the unix epoch, or null if the initial argument was null or undefined. */ export function ParseDateFromString(str: string | undefined | null): number | null { - if (!str) { - return null; - } + if (!str) { + return null; + } - const date = Date.parse(str); + const date = Date.parse(str); - if (Number.isNaN(date)) { - throw new InvalidScoreFailure(`Invalid/Unparsable score timestamp of ${str}.`); - } + if (Number.isNaN(date)) { + throw new InvalidScoreFailure(`Invalid/Unparsable score timestamp of ${str}.`); + } - return date; + return date; } diff --git a/server/src/lib/score-import/framework/common/string-asserts.test.ts b/server/src/lib/score-import/framework/common/string-asserts.test.ts index 7e10f539b..342aad78e 100644 --- a/server/src/lib/score-import/framework/common/string-asserts.test.ts +++ b/server/src/lib/score-import/framework/common/string-asserts.test.ts @@ -3,177 +3,177 @@ import { InvalidScoreFailure } from "./converter-failures"; import { AssertStrAsPositiveInt, AssertStrAsPositiveNonZeroInt } from "./string-asserts"; function astr(v: string) { - try { - const r = AssertStrAsPositiveInt(v, "err"); + try { + const r = AssertStrAsPositiveInt(v, "err"); - return r; - } catch (e) { - return e; - } + return r; + } catch (e) { + return e; + } } function astrp(v: string) { - try { - const r = AssertStrAsPositiveNonZeroInt(v, "err"); + try { + const r = AssertStrAsPositiveNonZeroInt(v, "err"); - return r; - } catch (e) { - return e; - } + return r; + } catch (e) { + return e; + } } t.test("#AssertStrAsPositiveInt", (t) => { - t.strictSame( - astr("---"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject non-int input" - ); + t.strictSame( + astr("---"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject non-int input" + ); - t.strictSame( - astr("1.4"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject float input" - ); + t.strictSame( + astr("1.4"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject float input" + ); - t.strictSame( - astr("NaN"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject NaN" - ); - t.strictSame( - astr("-0.3"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject negative float input" - ); + t.strictSame( + astr("NaN"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject NaN" + ); + t.strictSame( + astr("-0.3"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject negative float input" + ); - t.strictSame( - astr("0xFF"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject hex input" - ); + t.strictSame( + astr("0xFF"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject hex input" + ); - t.strictSame( - astr("0b11"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject binary input" - ); + t.strictSame( + astr("0b11"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject binary input" + ); - t.strictSame( - astr("0o77"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject oct input" - ); + t.strictSame( + astr("0o77"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject oct input" + ); - t.strictSame( - astr("--1"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject double neg input" - ); + t.strictSame( + astr("--1"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject double neg input" + ); - t.strictSame( - astr("12f"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject valid parseInt but invalid Number() input" - ); + t.strictSame( + astr("12f"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject valid parseInt but invalid Number() input" + ); - t.strictSame( - astr(`${Number.MAX_SAFE_INTEGER.toString()}000`), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject numbers > max_safe_integer." - ); + t.strictSame( + astr(`${Number.MAX_SAFE_INTEGER.toString()}000`), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject numbers > max_safe_integer." + ); - t.strictSame( - astr("-1"), - new InvalidScoreFailure(`err (Was negative.)`), - "Should reject negative integer input" - ); + t.strictSame( + astr("-1"), + new InvalidScoreFailure(`err (Was negative.)`), + "Should reject negative integer input" + ); - t.strictSame(astr("-0"), 0, "Should allow negative 0 input"); - t.strictSame(astr("0"), 0, "Should allow 0 input"); - t.strictSame(astr("13"), 13, "Should allow valid int input"); + t.strictSame(astr("-0"), 0, "Should allow negative 0 input"); + t.strictSame(astr("0"), 0, "Should allow 0 input"); + t.strictSame(astr("13"), 13, "Should allow valid int input"); - t.end(); + t.end(); }); t.test("#AssertStrAsPositiveNonZeroInt", (t) => { - t.strictSame( - astrp("---"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject non-int input" - ); + t.strictSame( + astrp("---"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject non-int input" + ); - t.strictSame( - astrp("1.4"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject float input" - ); + t.strictSame( + astrp("1.4"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject float input" + ); - t.strictSame( - astrp("NaN"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject NaN" - ); - t.strictSame( - astrp("-0.3"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject negative float input" - ); + t.strictSame( + astrp("NaN"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject NaN" + ); + t.strictSame( + astrp("-0.3"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject negative float input" + ); - t.strictSame( - astrp("0xFF"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject hex input" - ); + t.strictSame( + astrp("0xFF"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject hex input" + ); - t.strictSame( - astrp("0b11"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject binary input" - ); + t.strictSame( + astrp("0b11"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject binary input" + ); - t.strictSame( - astrp("0o77"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject oct input" - ); + t.strictSame( + astrp("0o77"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject oct input" + ); - t.strictSame( - astrp("--1"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject double neg input" - ); + t.strictSame( + astrp("--1"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject double neg input" + ); - t.strictSame( - astrp("12f"), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject valid parseInt but invalid Number() input" - ); + t.strictSame( + astrp("12f"), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject valid parseInt but invalid Number() input" + ); - t.strictSame( - astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`), - new InvalidScoreFailure(`err (Not an integer.)`), - "Should reject numbers > max_safe_integer." - ); + t.strictSame( + astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`), + new InvalidScoreFailure(`err (Not an integer.)`), + "Should reject numbers > max_safe_integer." + ); - t.strictSame( - astrp("-1"), - new InvalidScoreFailure(`err (Was negative or zero.)`), - "Should reject negative integer input" - ); + t.strictSame( + astrp("-1"), + new InvalidScoreFailure(`err (Was negative or zero.)`), + "Should reject negative integer input" + ); - t.strictSame( - astrp("-0"), - new InvalidScoreFailure(`err (Was negative or zero.)`), - "Should reject negative 0 input" - ); + t.strictSame( + astrp("-0"), + new InvalidScoreFailure(`err (Was negative or zero.)`), + "Should reject negative 0 input" + ); - t.strictSame( - astrp("0"), - new InvalidScoreFailure(`err (Was negative or zero.)`), - "Should reject 0 input" - ); + t.strictSame( + astrp("0"), + new InvalidScoreFailure(`err (Was negative or zero.)`), + "Should reject 0 input" + ); - t.strictSame(astrp("13"), 13, "Should allow valid int input"); + t.strictSame(astrp("13"), 13, "Should allow valid int input"); - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/common/string-asserts.ts b/server/src/lib/score-import/framework/common/string-asserts.ts index 6a78f1c3f..45c434abf 100644 --- a/server/src/lib/score-import/framework/common/string-asserts.ts +++ b/server/src/lib/score-import/framework/common/string-asserts.ts @@ -3,55 +3,55 @@ import { Difficulties, Game, IDStrings, Playtypes } from "tachi-common"; import { validDifficulties } from "tachi-common/js/config"; export function AssertStrAsDifficulty( - strVal: string, - game: Game, - playtype: Playtypes[Game] + strVal: string, + game: Game, + playtype: Playtypes[Game] ): Difficulties[IDStrings] { - if (!validDifficulties[game].includes(strVal)) { - throw new InvalidScoreFailure( - `Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties[ - game - ].join(", ")}` - ); - } + if (!validDifficulties[game].includes(strVal)) { + throw new InvalidScoreFailure( + `Invalid Difficulty for ${game} ${playtype} - Expected any of ${validDifficulties[ + game + ].join(", ")}` + ); + } - return strVal as Difficulties[IDStrings]; + return strVal as Difficulties[IDStrings]; } const isIntegerRegex = /^-?\d+$/u; export function AssertStrAsPositiveInt(strVal: string, errorMessage: string) { - const isInt = isIntegerRegex.test(strVal); + const isInt = isIntegerRegex.test(strVal); - if (!isInt) { - throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); - } + if (!isInt) { + throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); + } - const val = Number(strVal); + const val = Number(strVal); - if (!Number.isSafeInteger(val)) { - throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); - } else if (val < 0) { - throw new InvalidScoreFailure(`${errorMessage} (Was negative.)`); - } + if (!Number.isSafeInteger(val)) { + throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); + } else if (val < 0) { + throw new InvalidScoreFailure(`${errorMessage} (Was negative.)`); + } - return val; + return val; } export function AssertStrAsPositiveNonZeroInt(strVal: string, errorMessage: string) { - const isInt = isIntegerRegex.test(strVal); + const isInt = isIntegerRegex.test(strVal); - if (!isInt) { - throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); - } + if (!isInt) { + throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); + } - const val = Number(strVal); + const val = Number(strVal); - if (!Number.isSafeInteger(val)) { - throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); - } else if (val <= 0) { - throw new InvalidScoreFailure(`${errorMessage} (Was negative or zero.)`); - } + if (!Number.isSafeInteger(val)) { + throw new InvalidScoreFailure(`${errorMessage} (Not an integer.)`); + } else if (val <= 0) { + throw new InvalidScoreFailure(`${errorMessage} (Was negative or zero.)`); + } - return val; + return val; } diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts index b1eaa868c..9e8fb4e0b 100644 --- a/server/src/lib/score-import/framework/common/types.ts +++ b/server/src/lib/score-import/framework/common/types.ts @@ -5,10 +5,10 @@ import { IDStrings, ScoreDocument, Playtypes, Game } from "tachi-common"; * HydrateScore. */ export type DryScore = Pick< - ScoreDocument, - "service" | "game" | "scoreMeta" | "timeAchieved" | "comment" | "importType" + ScoreDocument, + "service" | "game" | "scoreMeta" | "timeAchieved" | "comment" | "importType" > & { - scoreData: Omit["scoreData"], "gradeIndex" | "lampIndex" | "esd">; + scoreData: Omit["scoreData"], "gradeIndex" | "lampIndex" | "esd">; }; export type ScorePlaytypeMap = Partial>; diff --git a/server/src/lib/score-import/framework/express-wrapper.ts b/server/src/lib/score-import/framework/express-wrapper.ts index b6e693aa6..897226786 100644 --- a/server/src/lib/score-import/framework/express-wrapper.ts +++ b/server/src/lib/score-import/framework/express-wrapper.ts @@ -1,9 +1,9 @@ import { - ImportTypes, - PublicUserDocument, - SuccessfulAPIResponse, - UnsuccessfulAPIResponse, - ImportDocument, + ImportTypes, + PublicUserDocument, + SuccessfulAPIResponse, + UnsuccessfulAPIResponse, + ImportDocument, } from "tachi-common"; import { ImportInputParser } from "../import-types/common/types"; import { CreateImportLoggerAndID } from "./common/import-logger"; @@ -11,8 +11,8 @@ import ScoreImportMain from "./score-import-main"; import ScoreImportFatalError from "./score-importing/score-import-error"; export interface WrappedAPIResponse { - statusCode: number; - body: SuccessfulAPIResponse | UnsuccessfulAPIResponse; + statusCode: number; + body: SuccessfulAPIResponse | UnsuccessfulAPIResponse; } /** @@ -21,48 +21,48 @@ export interface WrappedAPIResponse { * be immediately sent with res.json(). */ export async function ExpressWrappedScoreImportMain( - user: PublicUserDocument, - userIntent: boolean, - importType: ImportTypes, - InputParser: ImportInputParser + user: PublicUserDocument, + userIntent: boolean, + importType: ImportTypes, + InputParser: ImportInputParser ): Promise { - const { importID, logger } = CreateImportLoggerAndID(user, importType); - logger.debug("Received import request."); + const { importID, logger } = CreateImportLoggerAndID(user, importType); + logger.debug("Received import request."); - try { - const res = await ScoreImportMain(user, userIntent, importType, InputParser, { - importID, - logger, - }); + try { + const res = await ScoreImportMain(user, userIntent, importType, InputParser, { + importID, + logger, + }); - return { - statusCode: 200, - body: { - success: true, - description: "Import successful.", - body: res, - }, - }; - } catch (err) { - if (err instanceof ScoreImportFatalError) { - logger.info(err.message); - return { - statusCode: 400, - body: { - success: false, - description: err.message, - }, - }; - } + return { + statusCode: 200, + body: { + success: true, + description: "Import successful.", + body: res, + }, + }; + } catch (err) { + if (err instanceof ScoreImportFatalError) { + logger.info(err.message); + return { + statusCode: 400, + body: { + success: false, + description: err.message, + }, + }; + } - logger.error(err); - return { - statusCode: 500, - body: { - success: false, - description: - "An internal service error has occured. Please do not repeat this request.", - }, - }; - } + logger.error(err); + return { + statusCode: 500, + body: { + success: false, + description: + "An internal service error has occured. Please do not repeat this request.", + }, + }; + } } diff --git a/server/src/lib/score-import/framework/goals/goals.test.ts b/server/src/lib/score-import/framework/goals/goals.test.ts index 5db96e12c..3ca355d70 100644 --- a/server/src/lib/score-import/framework/goals/goals.test.ts +++ b/server/src/lib/score-import/framework/goals/goals.test.ts @@ -5,12 +5,12 @@ import { GetRelevantFolderGoals, GetRelevantGoals, UpdateGoalsForUser, ProcessGo import { GoalDocument, UserGoalDocument } from "tachi-common"; import { CreateFolderChartLookup } from "../../../../utils/folder"; import { - GetKTDataJSON, - Testing511SPA, - TestingIIDXSPScorePB, - HC511Goal, - HC511UserGoal, - TestingIIDXFolderSP10, + GetKTDataJSON, + Testing511SPA, + TestingIIDXSPScorePB, + HC511Goal, + HC511UserGoal, + TestingIIDXFolderSP10, } from "../../../../test-utils/test-data"; import deepmerge from "deepmerge"; import CreateLogCtx from "../../../logger/logger"; @@ -20,403 +20,403 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); t.test("#GetRelevantFolderGoals", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const fakeFolderGoalDocument: GoalDocument = { - charts: { - type: "folder", - data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", - }, - createdBy: 1, - game: "iidx", - goalID: "fake_goal_id", - playtype: "SP", - timeAdded: 0, - title: "get > 1 ex score on any level 10.", - criteria: { - mode: "single", - value: 1, - key: "scoreData.score", - }, - }; + const fakeFolderGoalDocument: GoalDocument = { + charts: { + type: "folder", + data: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", + }, + createdBy: 1, + game: "iidx", + goalID: "fake_goal_id", + playtype: "SP", + timeAdded: 0, + title: "get > 1 ex score on any level 10.", + criteria: { + mode: "single", + value: 1, + key: "scoreData.score", + }, + }; - const notFolderGoalDocument: GoalDocument = { - charts: { - type: "folder", - data: "some_fake_folder_id", - }, - createdBy: 1, - game: "iidx", - goalID: "fake_bad_goal_id", - playtype: "SP", - timeAdded: 0, - title: "get > 1 ex score on some other folder.", - criteria: { - mode: "single", - value: 1, - key: "scoreData.score", - }, - }; + const notFolderGoalDocument: GoalDocument = { + charts: { + type: "folder", + data: "some_fake_folder_id", + }, + createdBy: 1, + game: "iidx", + goalID: "fake_bad_goal_id", + playtype: "SP", + timeAdded: 0, + title: "get > 1 ex score on some other folder.", + criteria: { + mode: "single", + value: 1, + key: "scoreData.score", + }, + }; - t.beforeEach(async () => { - // @ts-expect-error garbage types - const sp11folder = deepmerge(TestingIIDXFolderSP10, { data: { level: "11" } }); - await db.folders.insert(TestingIIDXFolderSP10); - await db.folders.insert(sp11folder); - await db.goals.insert(fakeFolderGoalDocument); - await db.goals.insert(notFolderGoalDocument); - await CreateFolderChartLookup(TestingIIDXFolderSP10); - await CreateFolderChartLookup(sp11folder); - }); + t.beforeEach(async () => { + // @ts-expect-error garbage types + const sp11folder = deepmerge(TestingIIDXFolderSP10, { data: { level: "11" } }); + await db.folders.insert(TestingIIDXFolderSP10); + await db.folders.insert(sp11folder); + await db.goals.insert(fakeFolderGoalDocument); + await db.goals.insert(notFolderGoalDocument); + await CreateFolderChartLookup(TestingIIDXFolderSP10); + await CreateFolderChartLookup(sp11folder); + }); - t.test("Should correctly find the goals on this folder.", async (t) => { - const res = await GetRelevantFolderGoals( - ["fake_goal_id", "fake_bad_goal_id"], - [Testing511SPA.chartID] - ); + t.test("Should correctly find the goals on this folder.", async (t) => { + const res = await GetRelevantFolderGoals( + ["fake_goal_id", "fake_bad_goal_id"], + [Testing511SPA.chartID] + ); - t.strictSame( - res, - [fakeFolderGoalDocument], - "Should correctly return only the 511 goal document." - ); + t.strictSame( + res, + [fakeFolderGoalDocument], + "Should correctly return only the 511 goal document." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#GetRelevantGoals", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(async () => { - // use the real data so we have enough charts loaded for this to test properly. - await db.songs.iidx.remove({}); - await db.charts.iidx.remove({}); - await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + // use the real data so we have enough charts loaded for this to test properly. + await db.songs.iidx.remove({}); + await db.charts.iidx.remove({}); + await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); + await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); - const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 }); - const goals: GoalDocument[] = lotsOfCharts.map((e) => ({ - charts: { - type: "single", - data: e.chartID, - }, - createdBy: 1, - game: "iidx", - goalID: crypto.randomBytes(20).toString("hex"), - playtype: "SP", - timeAdded: 0, - title: "get > 1 ex score on some other folder.", - criteria: { - mode: "single", - value: 1, - key: "scoreData.score", - }, - })); + const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 }); + const goals: GoalDocument[] = lotsOfCharts.map((e) => ({ + charts: { + type: "single", + data: e.chartID, + }, + createdBy: 1, + game: "iidx", + goalID: crypto.randomBytes(20).toString("hex"), + playtype: "SP", + timeAdded: 0, + title: "get > 1 ex score on some other folder.", + criteria: { + mode: "single", + value: 1, + key: "scoreData.score", + }, + })); - await db.goals.insert(goals); + await db.goals.insert(goals); - await db["user-goals"].insert( - goals.map((e) => ({ - achieved: false, - timeAchieved: null, - game: "iidx", - playtype: "SP", - goalID: e.goalID, - lastInteraction: null, - outOf: 5, - outOfHuman: "HARD CLEAR", - progress: null, - progressHuman: "NO DATA", - timeSet: Date.now(), - userID: 1, - })) - ); - }); + await db["user-goals"].insert( + goals.map((e) => ({ + achieved: false, + timeAchieved: null, + game: "iidx", + playtype: "SP", + goalID: e.goalID, + lastInteraction: null, + outOf: 5, + outOfHuman: "HARD CLEAR", + progress: null, + progressHuman: "NO DATA", + timeSet: Date.now(), + userID: 1, + })) + ); + }); - t.test( - "Should successfully filter goals to only those that are relevant to the session.", - async (t) => { - // lets pretend our session had scores on charts 0->4 and 20->25. We also only have goals on - // charts 1->20, so only 5 of these should resolve. - const ourCharts = [ - ...(await db.charts.iidx.find({}, { limit: 5 })), - ...(await db.charts.iidx.find({}, { skip: 20, limit: 5 })), - ]; + t.test( + "Should successfully filter goals to only those that are relevant to the session.", + async (t) => { + // lets pretend our session had scores on charts 0->4 and 20->25. We also only have goals on + // charts 1->20, so only 5 of these should resolve. + const ourCharts = [ + ...(await db.charts.iidx.find({}, { limit: 5 })), + ...(await db.charts.iidx.find({}, { skip: 20, limit: 5 })), + ]; - const chartIDs = new Set(ourCharts.map((e) => e.chartID)); + const chartIDs = new Set(ourCharts.map((e) => e.chartID)); - const res = await GetRelevantGoals("iidx", 1, chartIDs, logger); + const res = await GetRelevantGoals("iidx", 1, chartIDs, logger); - t.equal(res.goals.length, 5, "Should correctly resolve to 5 goals."); - t.equal(res.userGoalsMap.size, 5, "Should also return 5 userGoals."); + t.equal(res.goals.length, 5, "Should correctly resolve to 5 goals."); + t.equal(res.userGoalsMap.size, 5, "Should also return 5 userGoals."); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); t.test("#UpdateGoalsForUser", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const baseGoalDocument: GoalDocument = { - charts: { - type: "single" as const, - data: Testing511SPA.chartID, - }, - createdBy: 1, - game: "iidx", - goalID: "FAKE_GOAL_ID", - playtype: "SP", - timeAdded: 0, - title: "get > 1 ex score on some other folder.", - criteria: { - mode: "single", - value: 1, - key: "scoreData.score", - }, - }; + const baseGoalDocument: GoalDocument = { + charts: { + type: "single" as const, + data: Testing511SPA.chartID, + }, + createdBy: 1, + game: "iidx", + goalID: "FAKE_GOAL_ID", + playtype: "SP", + timeAdded: 0, + title: "get > 1 ex score on some other folder.", + criteria: { + mode: "single", + value: 1, + key: "scoreData.score", + }, + }; - const baseUserGoalDocument: UserGoalDocument = { - achieved: false, - game: "iidx", - playtype: "SP", - goalID: "FAKE_GOAL_ID", - lastInteraction: null, - outOf: 1, - outOfHuman: "1", - progress: 0, - progressHuman: "0", - timeAchieved: null, - timeSet: 0, - userID: 1, - }; + const baseUserGoalDocument: UserGoalDocument = { + achieved: false, + game: "iidx", + playtype: "SP", + goalID: "FAKE_GOAL_ID", + lastInteraction: null, + outOf: 1, + outOfHuman: "1", + progress: 0, + progressHuman: "0", + timeAchieved: null, + timeSet: 0, + userID: 1, + }; - t.test("Should correctly update goals when user achieves goal.", async (t) => { - await db.goals.insert(baseGoalDocument); - delete baseGoalDocument._id; + t.test("Should correctly update goals when user achieves goal.", async (t) => { + await db.goals.insert(baseGoalDocument); + delete baseGoalDocument._id; - await db["user-goals"].insert(baseUserGoalDocument); - // we dont delete _id here because updategoalsforuser - // depends on usergoal _id + await db["user-goals"].insert(baseUserGoalDocument); + // we dont delete _id here because updategoalsforuser + // depends on usergoal _id - await db["personal-bests"].insert(TestingIIDXSPScorePB); - delete TestingIIDXSPScorePB._id; + await db["personal-bests"].insert(TestingIIDXSPScorePB); + delete TestingIIDXSPScorePB._id; - const ugMap = new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]); + const ugMap = new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]); - const res = await UpdateGoalsForUser([baseGoalDocument], ugMap, 1, logger); + const res = await UpdateGoalsForUser([baseGoalDocument], ugMap, 1, logger); - t.strictSame(res, [ - { - goalID: "FAKE_GOAL_ID", - old: { - progress: 0, - progressHuman: "0", - outOf: 1, - outOfHuman: "1", - achieved: false, - }, - new: { - progress: 1479, - progressHuman: "1479", - outOf: 1, - outOfHuman: "1", - achieved: true, - }, - }, - ]); + t.strictSame(res, [ + { + goalID: "FAKE_GOAL_ID", + old: { + progress: 0, + progressHuman: "0", + outOf: 1, + outOfHuman: "1", + achieved: false, + }, + new: { + progress: 1479, + progressHuman: "1479", + outOf: 1, + outOfHuman: "1", + achieved: true, + }, + }, + ]); - const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 }); + const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 }); - t.hasStrict( - r, - { - progress: 1479, - progressHuman: "1479", - outOf: 1, - outOfHuman: "1", - achieved: true, - }, - "Should update goals in the database." - ); + t.hasStrict( + r, + { + progress: 1479, + progressHuman: "1479", + outOf: 1, + outOfHuman: "1", + achieved: true, + }, + "Should update goals in the database." + ); - delete baseUserGoalDocument._id; + delete baseUserGoalDocument._id; - t.end(); - }); + t.end(); + }); - t.test("Should correctly update goals when user does not achieve goal.", async (t) => { - const goal = deepmerge(baseGoalDocument, { criteria: { value: 2 } }); - await db.goals.insert(goal); + t.test("Should correctly update goals when user does not achieve goal.", async (t) => { + const goal = deepmerge(baseGoalDocument, { criteria: { value: 2 } }); + await db.goals.insert(goal); - const userGoal = deepmerge(baseUserGoalDocument, { - outOf: 2, - outOfHuman: "2", - }) as unknown as UserGoalDocument; + const userGoal = deepmerge(baseUserGoalDocument, { + outOf: 2, + outOfHuman: "2", + }) as unknown as UserGoalDocument; - await db["user-goals"].insert(userGoal); - // we dont delete _id here because updategoalsforuser - // depends on usergoal _id + await db["user-goals"].insert(userGoal); + // we dont delete _id here because updategoalsforuser + // depends on usergoal _id - await db["personal-bests"].insert( - deepmerge(TestingIIDXSPScorePB, { scoreData: { score: 1 } }) - ); + await db["personal-bests"].insert( + deepmerge(TestingIIDXSPScorePB, { scoreData: { score: 1 } }) + ); - const ugMap = new Map([["FAKE_GOAL_ID", userGoal]]); + const ugMap = new Map([["FAKE_GOAL_ID", userGoal]]); - const res = await UpdateGoalsForUser([goal], ugMap, 1, logger); + const res = await UpdateGoalsForUser([goal], ugMap, 1, logger); - t.strictSame(res, [ - { - goalID: "FAKE_GOAL_ID", - old: { - progress: 0, - progressHuman: "0", - outOf: 2, - outOfHuman: "2", - achieved: false, - }, - new: { - progress: 1, - progressHuman: "1", - outOf: 2, - outOfHuman: "2", - achieved: false, - }, - }, - ]); + t.strictSame(res, [ + { + goalID: "FAKE_GOAL_ID", + old: { + progress: 0, + progressHuman: "0", + outOf: 2, + outOfHuman: "2", + achieved: false, + }, + new: { + progress: 1, + progressHuman: "1", + outOf: 2, + outOfHuman: "2", + achieved: false, + }, + }, + ]); - const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 }); + const r = await db["user-goals"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 }); - t.hasStrict( - r, - { - progress: 1, - progressHuman: "1", - outOf: 2, - outOfHuman: "2", - achieved: false, - }, - "Should update goals in the database." - ); + t.hasStrict( + r, + { + progress: 1, + progressHuman: "1", + outOf: 2, + outOfHuman: "2", + achieved: false, + }, + "Should update goals in the database." + ); - delete baseUserGoalDocument._id; + delete baseUserGoalDocument._id; - t.end(); - }); + t.end(); + }); - t.test("Should return [] if no data is to be changed.", async (t) => { - const res = await UpdateGoalsForUser([], new Map(), 1, logger); + t.test("Should return [] if no data is to be changed.", async (t) => { + const res = await UpdateGoalsForUser([], new Map(), 1, logger); - t.strictSame(res, []); + t.strictSame(res, []); - t.end(); - }); + t.end(); + }); - t.test("Should handle (skip) goals if no usergoal is set.", async (t) => { - const res = await UpdateGoalsForUser([baseGoalDocument], new Map(), 1, logger); + t.test("Should handle (skip) goals if no usergoal is set.", async (t) => { + const res = await UpdateGoalsForUser([baseGoalDocument], new Map(), 1, logger); - t.strictSame(res, []); + t.strictSame(res, []); - t.end(); - }); + t.end(); + }); - t.test("Should handle (skip) invalid goals.", async (t) => { - const res = await UpdateGoalsForUser( - [deepmerge(baseGoalDocument, { charts: { type: "INVALID" } })], - new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]), - 1, - logger - ); + t.test("Should handle (skip) invalid goals.", async (t) => { + const res = await UpdateGoalsForUser( + [deepmerge(baseGoalDocument, { charts: { type: "INVALID" } })], + new Map([["FAKE_GOAL_ID", baseUserGoalDocument]]), + 1, + logger + ); - t.strictSame(res, []); + t.strictSame(res, []); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.todo("#GetAndUpdateUsersGoals", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.end(); + t.end(); }); t.test("#ProcessGoal", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(async () => { - await db.goals.insert(HC511Goal); - }); + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + await db.goals.insert(HC511Goal); + }); - t.test("Should process the users goal if a score has changed.", async (t) => { - await db["user-goals"].insert(HC511UserGoal); - await db["personal-bests"].insert(TestingIIDXSPScorePB); // score is EX HARD CLEAR by default. + t.test("Should process the users goal if a score has changed.", async (t) => { + await db["user-goals"].insert(HC511UserGoal); + await db["personal-bests"].insert(TestingIIDXSPScorePB); // score is EX HARD CLEAR by default. - const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); + const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); - t.not(res, undefined, "Should NOT return undefined."); + t.not(res, undefined, "Should NOT return undefined."); - t.strictSame( - res!.import, - { - goalID: "mock_goalID", - old: { - progress: null, - progressHuman: "NO DATA", - outOf: 5, - outOfHuman: "HARD CLEAR", - achieved: false, - }, - new: { - progress: 6, - progressHuman: "EX HARD CLEAR (BP: 2)", - outOf: 5, - outOfHuman: "HARD CLEAR", - achieved: true, - }, - }, - "Should return a valid import goal fragment." - ); + t.strictSame( + res!.import, + { + goalID: "mock_goalID", + old: { + progress: null, + progressHuman: "NO DATA", + outOf: 5, + outOfHuman: "HARD CLEAR", + achieved: false, + }, + new: { + progress: 6, + progressHuman: "EX HARD CLEAR (BP: 2)", + outOf: 5, + outOfHuman: "HARD CLEAR", + achieved: true, + }, + }, + "Should return a valid import goal fragment." + ); - t.end(); - }); + t.end(); + }); - t.test("Should return undefined if there's no score.", async (t) => { - await db["user-goals"].insert(HC511UserGoal); + t.test("Should return undefined if there's no score.", async (t) => { + await db["user-goals"].insert(HC511UserGoal); - const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); + const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); - t.equal(res, undefined, "Should return undefined."); + t.equal(res, undefined, "Should return undefined."); - t.end(); - }); + t.end(); + }); - t.test("Should return undefined if the progress has not changed.", async (t) => { - await db["user-goals"].insert(HC511UserGoal); - await db["personal-bests"].insert(TestingIIDXSPScorePB); + t.test("Should return undefined if the progress has not changed.", async (t) => { + await db["user-goals"].insert(HC511UserGoal); + await db["personal-bests"].insert(TestingIIDXSPScorePB); - const firstUpdate = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); + const firstUpdate = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); - // ignore this one - t.not(firstUpdate, undefined, "Should NOT return undefined."); + // ignore this one + t.not(firstUpdate, undefined, "Should NOT return undefined."); - await db["user-goals"].bulkWrite([firstUpdate!.bwrite]); + await db["user-goals"].bulkWrite([firstUpdate!.bwrite]); - const userGoal = await db["user-goals"].findOne({ userID: 1, goalID: HC511Goal.goalID }); + const userGoal = await db["user-goals"].findOne({ userID: 1, goalID: HC511Goal.goalID }); - const secondUpdate = await ProcessGoal(HC511Goal, userGoal!, 1, logger); + const secondUpdate = await ProcessGoal(HC511Goal, userGoal!, 1, logger); - t.equal(secondUpdate, undefined, "Should return undefined."); + t.equal(secondUpdate, undefined, "Should return undefined."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/goals/goals.ts b/server/src/lib/score-import/framework/goals/goals.ts index 675309922..7936fa1ec 100644 --- a/server/src/lib/score-import/framework/goals/goals.ts +++ b/server/src/lib/score-import/framework/goals/goals.ts @@ -8,65 +8,65 @@ import { RedisPub } from "../../../../external/redis/redis-IPC"; * Update a user's progress on all of their set goals. */ export async function GetAndUpdateUsersGoals( - game: Game, - userID: integer, - chartIDs: Set, - logger: KtLogger + game: Game, + userID: integer, + chartIDs: Set, + logger: KtLogger ) { - const { goals, userGoalsMap } = await GetRelevantGoals(game, userID, chartIDs, logger); + const { goals, userGoalsMap } = await GetRelevantGoals(game, userID, chartIDs, logger); - if (!goals.length) { - // if we hit the below code with an empty array mongodb will flip out on the bulkwrite op - return []; - } + if (!goals.length) { + // if we hit the below code with an empty array mongodb will flip out on the bulkwrite op + return []; + } - logger.verbose(`Found ${goals.length} relevant goals.`); + logger.verbose(`Found ${goals.length} relevant goals.`); - return UpdateGoalsForUser(goals, userGoalsMap, userID, logger); + return UpdateGoalsForUser(goals, userGoalsMap, userID, logger); } export async function UpdateGoalsForUser( - goals: GoalDocument[], - userGoalsMap: Map, - userID: integer, - logger: KtLogger + goals: GoalDocument[], + userGoalsMap: Map, + userID: integer, + logger: KtLogger ) { - const returns = await Promise.all( - goals.map((goal: GoalDocument) => { - const userGoal = userGoalsMap.get(goal.goalID); + const returns = await Promise.all( + goals.map((goal: GoalDocument) => { + const userGoal = userGoalsMap.get(goal.goalID); - if (!userGoal) { - logger.error( - `UserGoal:GoalID mismatch ${goal.goalID} - this user has no userGoal for this, yet it is set.` - ); - return; - } + if (!userGoal) { + logger.error( + `UserGoal:GoalID mismatch ${goal.goalID} - this user has no userGoal for this, yet it is set.` + ); + return; + } - return ProcessGoal(goal, userGoal, userID, logger); - }) - ); + return ProcessGoal(goal, userGoal, userID, logger); + }) + ); - const importInfo = []; - const bulkWrite = []; + const importInfo = []; + const bulkWrite = []; - for (const ret of returns) { - if (!ret) { - continue; - } + for (const ret of returns) { + if (!ret) { + continue; + } - importInfo.push(ret.import); - bulkWrite.push(ret.bwrite); - } + importInfo.push(ret.import); + bulkWrite.push(ret.bwrite); + } - if (bulkWrite.length === 0) { - // bulkwrite cannot be an empty array -- this means there's nothing to update or return, then. - // i.e. goals was non empty but returns was entirely [undefined, undefined...]. - return []; - } + if (bulkWrite.length === 0) { + // bulkwrite cannot be an empty array -- this means there's nothing to update or return, then. + // i.e. goals was non empty but returns was entirely [undefined, undefined...]. + return []; + } - await db["user-goals"].bulkWrite(bulkWrite, { ordered: false }); + await db["user-goals"].bulkWrite(bulkWrite, { ordered: false }); - return importInfo; + return importInfo; } /** @@ -76,73 +76,73 @@ export async function UpdateGoalsForUser( * to say (i.e. user didnt raise the goal). */ export async function ProcessGoal( - goal: GoalDocument, - userGoal: UserGoalDocument, - userID: integer, - logger: KtLogger + goal: GoalDocument, + userGoal: UserGoalDocument, + userID: integer, + logger: KtLogger ) { - const res = await EvaluateGoalForUser(goal, userID, logger); + const res = await EvaluateGoalForUser(goal, userID, logger); - if (!res) { - // some sort of error occured - its logged by the previous function. - return; - } + if (!res) { + // some sort of error occured - its logged by the previous function. + return; + } - // nothing has changed - if (userGoal.progress === res.progress && userGoal.outOf === res.outOf) { - return; - } + // nothing has changed + if (userGoal.progress === res.progress && userGoal.outOf === res.outOf) { + return; + } - const newData = { - progress: res.progress, - progressHuman: res.progressHuman, - outOf: res.outOf, - outOfHuman: res.outOfHuman, - achieved: res.achieved, - }; + const newData = { + progress: res.progress, + progressHuman: res.progressHuman, + outOf: res.outOf, + outOfHuman: res.outOfHuman, + achieved: res.achieved, + }; - const oldData = { - progress: userGoal.progress, - progressHuman: userGoal.progressHuman, - outOf: userGoal.outOf, - outOfHuman: userGoal.outOfHuman, - achieved: userGoal.achieved, - }; + const oldData = { + progress: userGoal.progress, + progressHuman: userGoal.progressHuman, + outOf: userGoal.outOf, + outOfHuman: userGoal.outOfHuman, + achieved: userGoal.achieved, + }; - // if this is a newly-achieved goal - if (res.achieved && !userGoal.achieved) { - RedisPub("goal-achieved", { - userID, - goalID: goal.goalID, - old: oldData, - new: newData, - }); - } + // if this is a newly-achieved goal + if (res.achieved && !userGoal.achieved) { + RedisPub("goal-achieved", { + userID, + goalID: goal.goalID, + old: oldData, + new: newData, + }); + } - const bulkWrite = { - updateOne: { - filter: { _id: userGoal._id! }, - update: { - $set: { - ...newData, - timeAchieved: newData.achieved ? Date.now() : null, - // we're guaranteed that this works, because things - // that haven't changed return nothing instead of - // getting to this point. - lastInteraction: Date.now(), - }, - }, - }, - }; + const bulkWrite = { + updateOne: { + filter: { _id: userGoal._id! }, + update: { + $set: { + ...newData, + timeAchieved: newData.achieved ? Date.now() : null, + // we're guaranteed that this works, because things + // that haven't changed return nothing instead of + // getting to this point. + lastInteraction: Date.now(), + }, + }, + }, + }; - return { - bwrite: bulkWrite, - import: { - goalID: goal.goalID, - old: oldData, - new: newData, - }, - }; + return { + bwrite: bulkWrite, + import: { + goalID: goal.goalID, + old: oldData, + new: newData, + }, + }; } /** @@ -154,57 +154,57 @@ export async function ProcessGoal( * @returns An array of Goals, and an array of userGoals. */ export async function GetRelevantGoals( - game: Game, - userID: integer, - chartIDs: Set, - logger: KtLogger + game: Game, + userID: integer, + chartIDs: Set, + logger: KtLogger ): Promise<{ goals: GoalDocument[]; userGoalsMap: Map }> { - const userGoals = await db["user-goals"].find({ game, userID }); + const userGoals = await db["user-goals"].find({ game, userID }); - logger.verbose(`Found user has ${userGoals.length} goals.`); + logger.verbose(`Found user has ${userGoals.length} goals.`); - if (!userGoals.length) { - return { goals: [], userGoalsMap: new Map() }; - } + if (!userGoals.length) { + return { goals: [], userGoalsMap: new Map() }; + } - const goalIDs = userGoals.map((e) => e.goalID); + const goalIDs = userGoals.map((e) => e.goalID); - const chartIDsArr: string[] = []; - for (const c of chartIDs) { - chartIDsArr.push(c); - } + const chartIDsArr: string[] = []; + for (const c of chartIDs) { + chartIDsArr.push(c); + } - const goals = await Promise.all([ - // this gets the relevantGoals for direct and multi - db.goals.find({ - "charts.type": { $in: ["single", "multi"] }, - "charts.data": { $in: chartIDsArr }, - goalID: { $in: goalIDs }, - }), - db.goals.find({ - "charts.type": "any", - goalID: { $in: goalIDs }, - }), - GetRelevantFolderGoals(goalIDs, chartIDsArr), - ]).then((r) => r.flat(1)); + const goals = await Promise.all([ + // this gets the relevantGoals for direct and multi + db.goals.find({ + "charts.type": { $in: ["single", "multi"] }, + "charts.data": { $in: chartIDsArr }, + goalID: { $in: goalIDs }, + }), + db.goals.find({ + "charts.type": "any", + goalID: { $in: goalIDs }, + }), + GetRelevantFolderGoals(goalIDs, chartIDsArr), + ]).then((r) => r.flat(1)); - const goalSet = new Set(goals.map((e) => e.goalID)); + const goalSet = new Set(goals.map((e) => e.goalID)); - const userGoalsMap: Map = new Map(); + const userGoalsMap: Map = new Map(); - for (const userGoal of userGoals) { - if (!goalSet.has(userGoal.goalID)) { - continue; - } - // since these are guaranteed to be unique, lets make a hot map of goalID -> userGoalDocument, so we can - // pull them in for post-processing and filter out the userGoalDocuments that aren't relevant. - userGoalsMap.set(userGoal.goalID, userGoal); - } + for (const userGoal of userGoals) { + if (!goalSet.has(userGoal.goalID)) { + continue; + } + // since these are guaranteed to be unique, lets make a hot map of goalID -> userGoalDocument, so we can + // pull them in for post-processing and filter out the userGoalDocuments that aren't relevant. + userGoalsMap.set(userGoal.goalID, userGoal); + } - return { - goals, - userGoalsMap, - }; + return { + goals, + userGoalsMap, + }; } /** @@ -212,34 +212,34 @@ export async function GetRelevantGoals( * of chartIDsArr. */ export function GetRelevantFolderGoals(goalIDs: string[], chartIDsArr: string[]) { - // Slightly black magic - this is kind of like doing an SQL join. - // it's weird to do this in mongodb, but this seems like the right - // way to actually handle this. + // Slightly black magic - this is kind of like doing an SQL join. + // it's weird to do this in mongodb, but this seems like the right + // way to actually handle this. - return db.goals.aggregate([ - { - $match: { - "charts.type": "folder", - goalID: { $in: goalIDs }, - }, - }, - { - $lookup: { - from: "folder-chart-lookup", - localField: "charts.data", - foreignField: "folderID", - as: "folderCharts", - }, - }, - { - $match: { - "folderCharts.chartID": { $in: chartIDsArr }, - }, - }, - { - $project: { - folderCharts: 0, - }, - }, - ]); + return db.goals.aggregate([ + { + $match: { + "charts.type": "folder", + goalID: { $in: goalIDs }, + }, + }, + { + $lookup: { + from: "folder-chart-lookup", + localField: "charts.data", + foreignField: "folderID", + as: "folderCharts", + }, + }, + { + $match: { + "folderCharts.chartID": { $in: chartIDsArr }, + }, + }, + { + $project: { + folderCharts: 0, + }, + }, + ]); } diff --git a/server/src/lib/score-import/framework/import-locks/lock.ts b/server/src/lib/score-import/framework/import-locks/lock.ts index dd9ae628e..894a38bd8 100644 --- a/server/src/lib/score-import/framework/import-locks/lock.ts +++ b/server/src/lib/score-import/framework/import-locks/lock.ts @@ -8,27 +8,27 @@ import { integer } from "tachi-common"; * If a lock exists for the user, the lock is returned. */ export function GetOrSetUserLock(userID: integer) { - return db["import-locks"].findOneAndUpdate( - { - userID, - }, - { - $set: { userID }, - }, - { - upsert: true, - // this is marked as deprecated, but it shouldn't be, as returnDocument: "before" - // does nothing. - returnOriginal: true, - } - ); + return db["import-locks"].findOneAndUpdate( + { + userID, + }, + { + $set: { userID }, + }, + { + upsert: true, + // this is marked as deprecated, but it shouldn't be, as returnDocument: "before" + // does nothing. + returnOriginal: true, + } + ); } /** * Removes a users import lock. */ export function RemoveUserLock(userID: integer) { - return db["import-locks"].remove({ - userID, - }); + return db["import-locks"].remove({ + userID, + }); } diff --git a/server/src/lib/score-import/framework/milestones/milestones.test.ts b/server/src/lib/score-import/framework/milestones/milestones.test.ts index 2203a9a79..de9de4b60 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.test.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.test.ts @@ -11,347 +11,347 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); function CreateMockGII(...garr: [string, boolean][]) { - return garr.map((e) => ({ - goalID: e[0], - old: {}, - new: { achieved: e[1] }, - })) as unknown as GoalImportInfo[]; + return garr.map((e) => ({ + goalID: e[0], + old: {}, + new: { achieved: e[1] }, + })) as unknown as GoalImportInfo[]; } t.test("#UpdateUsersMilestones", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => db.milestones.insert(TestingIIDXSPMilestone)); - t.beforeEach(() => - db["user-milestones"].insert({ - achieved: false, - game: "iidx", - milestoneID: TestingIIDXSPMilestone.milestoneID, - playtype: "SP", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }) - ); + t.beforeEach(ResetDBState); + t.beforeEach(() => db.milestones.insert(TestingIIDXSPMilestone)); + t.beforeEach(() => + db["user-milestones"].insert({ + achieved: false, + game: "iidx", + milestoneID: TestingIIDXSPMilestone.milestoneID, + playtype: "SP", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }) + ); - t.test("Test with clean achieved milestone.", async (t) => { - const res = await UpdateUsersMilestones( - CreateMockGII( - ["eg_goal_1", true], - ["eg_goal_2", true], - ["eg_goal_3", true], - ["eg_goal_4", true] - ), - "iidx", - ["SP"], - 1, - logger - ); + t.test("Test with clean achieved milestone.", async (t) => { + const res = await UpdateUsersMilestones( + CreateMockGII( + ["eg_goal_1", true], + ["eg_goal_2", true], + ["eg_goal_3", true], + ["eg_goal_4", true] + ), + "iidx", + ["SP"], + 1, + logger + ); - t.strictSame( - res, - [ - { - milestoneID: "example_milestone_id", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - ], - "Should correctly assert the milestone is achieved." - ); + t.strictSame( + res, + [ + { + milestoneID: "example_milestone_id", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + ], + "Should correctly assert the milestone is achieved." + ); - t.end(); - }); + t.end(); + }); - t.test("Test with unclean achieved milestone.", async (t) => { - const res = await UpdateUsersMilestones( - CreateMockGII( - ["eg_goal_1", true], - ["eg_goal_2", true], - ["eg_goal_3", true], - ["eg_goal_4", true], - ["x_goal_1", true], - ["x_goal_2", true], - ["x_goal_3", true] - ), - "iidx", - ["SP"], - 1, - logger - ); + t.test("Test with unclean achieved milestone.", async (t) => { + const res = await UpdateUsersMilestones( + CreateMockGII( + ["eg_goal_1", true], + ["eg_goal_2", true], + ["eg_goal_3", true], + ["eg_goal_4", true], + ["x_goal_1", true], + ["x_goal_2", true], + ["x_goal_3", true] + ), + "iidx", + ["SP"], + 1, + logger + ); - t.strictSame( - res, - [ - { - milestoneID: "example_milestone_id", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - ], - "Should correctly assert the milestone is achieved." - ); + t.strictSame( + res, + [ + { + milestoneID: "example_milestone_id", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + ], + "Should correctly assert the milestone is achieved." + ); - t.end(); - }); + t.end(); + }); - t.test("Test with increased progress on milestone.", async (t) => { - const res = await UpdateUsersMilestones( - CreateMockGII(["eg_goal_1", true], ["eg_goal_2", true]), - "iidx", - ["SP"], - 1, - logger - ); + t.test("Test with increased progress on milestone.", async (t) => { + const res = await UpdateUsersMilestones( + CreateMockGII(["eg_goal_1", true], ["eg_goal_2", true]), + "iidx", + ["SP"], + 1, + logger + ); - t.strictSame( - res, - [ - { - milestoneID: "example_milestone_id", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 2, - achieved: false, - }, - }, - ], - "Should correctly assert the milestone progress has increased." - ); + t.strictSame( + res, + [ + { + milestoneID: "example_milestone_id", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 2, + achieved: false, + }, + }, + ], + "Should correctly assert the milestone progress has increased." + ); - t.end(); - }); + t.end(); + }); - t.test("Test with no new goals on milestone.", async (t) => { - const res = await UpdateUsersMilestones([], "iidx", ["SP"], 1, logger); + t.test("Test with no new goals on milestone.", async (t) => { + const res = await UpdateUsersMilestones([], "iidx", ["SP"], 1, logger); - t.strictSame(res, [], "Should correctly return no changes."); + t.strictSame(res, [], "Should correctly return no changes."); - t.end(); - }); + t.end(); + }); - t.test("Test with new goals on multiple milestones.", async (t) => { - delete TestingIIDXSPMilestone._id; - await db.milestones.insert( - // @ts-expect-error lol - deepmerge(TestingIIDXSPMilestone, { - milestoneID: "some_other_milestone_with_mutual_goals", - }) - ); - await db["user-milestones"].insert({ - achieved: false, - game: "iidx", - milestoneID: "some_other_milestone_with_mutual_goals", - playtype: "SP", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }); + t.test("Test with new goals on multiple milestones.", async (t) => { + delete TestingIIDXSPMilestone._id; + await db.milestones.insert( + // @ts-expect-error lol + deepmerge(TestingIIDXSPMilestone, { + milestoneID: "some_other_milestone_with_mutual_goals", + }) + ); + await db["user-milestones"].insert({ + achieved: false, + game: "iidx", + milestoneID: "some_other_milestone_with_mutual_goals", + playtype: "SP", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }); - const res = await UpdateUsersMilestones( - CreateMockGII( - ["eg_goal_1", true], - ["eg_goal_2", true], - ["eg_goal_3", true], - ["eg_goal_4", true], - ["x_goal_1", true], - ["x_goal_2", true], - ["x_goal_3", true] - ), - "iidx", - ["SP", "DP"], - 1, - logger - ); + const res = await UpdateUsersMilestones( + CreateMockGII( + ["eg_goal_1", true], + ["eg_goal_2", true], + ["eg_goal_3", true], + ["eg_goal_4", true], + ["x_goal_1", true], + ["x_goal_2", true], + ["x_goal_3", true] + ), + "iidx", + ["SP", "DP"], + 1, + logger + ); - t.strictSame( - res, - [ - { - milestoneID: "example_milestone_id", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - { - milestoneID: "some_other_milestone_with_mutual_goals", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - ], - "Should correctly assert the milestones are achieved." - ); - t.end(); - }); + t.strictSame( + res, + [ + { + milestoneID: "example_milestone_id", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + { + milestoneID: "some_other_milestone_with_mutual_goals", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + ], + "Should correctly assert the milestones are achieved." + ); + t.end(); + }); - t.test("Test with multiple milestones that only some match", async (t) => { - delete TestingIIDXSPMilestone._id; - await db.milestones.insert([ - // @ts-expect-error lol - deepmerge(TestingIIDXSPMilestone, { - milestoneID: "some_other_milestone_with_mutual_goals", - }), - deepmerge( - TestingIIDXSPMilestone, - { - milestoneID: "dp_milestone", - playtype: "DP", - milestoneData: [ - { - goals: [{ goalID: "foobar" }], - }, - ], - }, - { arrayMerge: (d, s) => s } - ), - deepmerge( - TestingIIDXSPMilestone, - { - milestoneID: "other_game_milestone", - game: "museca", - playtype: "Single", - milestoneData: [ - { - goals: [{ goalID: "foo" }], - }, - ], - }, - { arrayMerge: (d, s) => s } - ), - deepmerge( - TestingIIDXSPMilestone, - { - milestoneID: "iidx_with_not_goal", - game: "iidx", - playtype: "SP", - milestoneData: [ - { - goals: [{ goalID: "not_real_goal_id" }], - }, - ], - }, - { arrayMerge: (d, s) => s } - ), - ]); + t.test("Test with multiple milestones that only some match", async (t) => { + delete TestingIIDXSPMilestone._id; + await db.milestones.insert([ + // @ts-expect-error lol + deepmerge(TestingIIDXSPMilestone, { + milestoneID: "some_other_milestone_with_mutual_goals", + }), + deepmerge( + TestingIIDXSPMilestone, + { + milestoneID: "dp_milestone", + playtype: "DP", + milestoneData: [ + { + goals: [{ goalID: "foobar" }], + }, + ], + }, + { arrayMerge: (d, s) => s } + ), + deepmerge( + TestingIIDXSPMilestone, + { + milestoneID: "other_game_milestone", + game: "museca", + playtype: "Single", + milestoneData: [ + { + goals: [{ goalID: "foo" }], + }, + ], + }, + { arrayMerge: (d, s) => s } + ), + deepmerge( + TestingIIDXSPMilestone, + { + milestoneID: "iidx_with_not_goal", + game: "iidx", + playtype: "SP", + milestoneData: [ + { + goals: [{ goalID: "not_real_goal_id" }], + }, + ], + }, + { arrayMerge: (d, s) => s } + ), + ]); - await db["user-milestones"].insert([ - { - achieved: false, - game: "iidx", - milestoneID: "some_other_milestone_with_mutual_goals", - playtype: "SP", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }, - { - achieved: false, - game: "iidx", - milestoneID: "dp_milestone", - playtype: "DP", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }, - { - achieved: false, - game: "museca", - milestoneID: "other_game_milestone", - playtype: "Single", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }, - { - achieved: false, - game: "iidx", - milestoneID: "iidx_with_not_goal", - playtype: "SP", - progress: 0, - timeAchieved: null, - timeSet: 0, - userID: 1, - }, - ]); + await db["user-milestones"].insert([ + { + achieved: false, + game: "iidx", + milestoneID: "some_other_milestone_with_mutual_goals", + playtype: "SP", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }, + { + achieved: false, + game: "iidx", + milestoneID: "dp_milestone", + playtype: "DP", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }, + { + achieved: false, + game: "museca", + milestoneID: "other_game_milestone", + playtype: "Single", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }, + { + achieved: false, + game: "iidx", + milestoneID: "iidx_with_not_goal", + playtype: "SP", + progress: 0, + timeAchieved: null, + timeSet: 0, + userID: 1, + }, + ]); - const res = await UpdateUsersMilestones( - CreateMockGII( - ["eg_goal_1", true], - ["eg_goal_2", true], - ["eg_goal_3", true], - ["eg_goal_4", true], - ["x_goal_1", true], - ["x_goal_2", true], - ["x_goal_3", true] - ), - "iidx", - ["SP", "DP"], - 1, - logger - ); + const res = await UpdateUsersMilestones( + CreateMockGII( + ["eg_goal_1", true], + ["eg_goal_2", true], + ["eg_goal_3", true], + ["eg_goal_4", true], + ["x_goal_1", true], + ["x_goal_2", true], + ["x_goal_3", true] + ), + "iidx", + ["SP", "DP"], + 1, + logger + ); - t.strictSame( - res, - [ - { - milestoneID: "example_milestone_id", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - { - milestoneID: "some_other_milestone_with_mutual_goals", - old: { - progress: 0, - achieved: false, - }, - new: { - progress: 4, - achieved: true, - }, - }, - ], - "Should correctly assert the milestones are achieved." - ); - t.end(); - }); + t.strictSame( + res, + [ + { + milestoneID: "example_milestone_id", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + { + milestoneID: "some_other_milestone_with_mutual_goals", + old: { + progress: 0, + achieved: false, + }, + new: { + progress: 4, + achieved: true, + }, + }, + ], + "Should correctly assert the milestones are achieved." + ); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/milestones/milestones.ts b/server/src/lib/score-import/framework/milestones/milestones.ts index 0aa708064..8a33c19f8 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.ts @@ -1,12 +1,12 @@ import { KtLogger } from "../../../logger/logger"; import { - integer, - Game, - Playtypes, - GoalImportInfo, - UserMilestoneDocument, - MilestoneImportInfo, - MilestoneDocument, + integer, + Game, + Playtypes, + GoalImportInfo, + UserMilestoneDocument, + MilestoneImportInfo, + MilestoneDocument, } from "tachi-common"; import db from "../../../../external/mongo/db"; import { BulkWriteUpdateOneOperation } from "mongodb"; @@ -18,154 +18,154 @@ import { RedisPub, RedisSub } from "../../../../external/redis/redis-IPC"; * about goals from imports) */ export function ProcessMilestoneFromGII( - milestone: MilestoneDocument, - gii: Map + milestone: MilestoneDocument, + gii: Map ) { - const goalIDs = GetGoalIDsFromMilestone(milestone); + const goalIDs = GetGoalIDsFromMilestone(milestone); - let progress = 0; + let progress = 0; - for (const goalID of goalIDs) { - const userInfo = gii.get(goalID); + for (const goalID of goalIDs) { + const userInfo = gii.get(goalID); - if (!userInfo) { - continue; - } + if (!userInfo) { + continue; + } - if (!userInfo.achieved) { - continue; - } + if (!userInfo.achieved) { + continue; + } - progress++; - } + progress++; + } - const outOf = CalculateMilestoneOutOf(milestone, goalIDs); + const outOf = CalculateMilestoneOutOf(milestone, goalIDs); - // milestone achieved! - if (progress >= outOf) { - return { achieved: true, progress }; - } + // milestone achieved! + if (progress >= outOf) { + return { achieved: true, progress }; + } - return { achieved: false, progress }; + return { achieved: false, progress }; } export async function UpdateUsersMilestones( - importGoalInfo: GoalImportInfo[], - game: Game, - playtypes: Playtypes[Game][], - userID: integer, - logger: KtLogger + importGoalInfo: GoalImportInfo[], + game: Game, + playtypes: Playtypes[Game][], + userID: integer, + logger: KtLogger ) { - const userGoalInfoMap: Map = new Map(); + const userGoalInfoMap: Map = new Map(); - const goalIDs = []; - for (const e of importGoalInfo) { - userGoalInfoMap.set(e.goalID, e.new); - goalIDs.push(e.goalID); - } + const goalIDs = []; + for (const e of importGoalInfo) { + userGoalInfoMap.set(e.goalID, e.new); + goalIDs.push(e.goalID); + } - const { milestones, userMilestones } = await GetRelevantMilestones( - goalIDs, - game, - playtypes, - userID, - logger - ); + const { milestones, userMilestones } = await GetRelevantMilestones( + goalIDs, + game, + playtypes, + userID, + logger + ); - // create a map here to avoid linear searching when - // co-iterating - const userMilestoneMap = new Map(); - for (const um of userMilestones) { - userMilestoneMap.set(um.milestoneID, um); - } + // create a map here to avoid linear searching when + // co-iterating + const userMilestoneMap = new Map(); + for (const um of userMilestones) { + userMilestoneMap.set(um.milestoneID, um); + } - const importGoalMap = new Map(); + const importGoalMap = new Map(); - for (const ig of importGoalInfo) { - importGoalMap.set(ig.goalID, ig.new); - } + for (const ig of importGoalInfo) { + importGoalMap.set(ig.goalID, ig.new); + } - const bwrite: BulkWriteUpdateOneOperation[] = []; + const bwrite: BulkWriteUpdateOneOperation[] = []; - const importMilestoneInfo: MilestoneImportInfo[] = []; + const importMilestoneInfo: MilestoneImportInfo[] = []; - for (const milestone of milestones) { - const { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap); + for (const milestone of milestones) { + const { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap); - const userMilestone = userMilestoneMap.get(milestone.milestoneID); + const userMilestone = userMilestoneMap.get(milestone.milestoneID); - if (!userMilestone) { - logger.severe( - `Invalid state achieved in milestone processing - processed milestone that user did not have? ${milestone.milestoneID}` - ); + if (!userMilestone) { + logger.severe( + `Invalid state achieved in milestone processing - processed milestone that user did not have? ${milestone.milestoneID}` + ); - throw new Error("Invalid state achieved in milestone processing."); - } + throw new Error("Invalid state achieved in milestone processing."); + } - bwrite.push({ - updateOne: { - filter: { milestoneID: milestone.milestoneID }, - update: { - $set: { - achieved, - progress, - }, - }, - }, - }); + bwrite.push({ + updateOne: { + filter: { milestoneID: milestone.milestoneID }, + update: { + $set: { + achieved, + progress, + }, + }, + }, + }); - const milestoneInfo = { - milestoneID: userMilestone.milestoneID, - old: { - progress: userMilestone.progress, - achieved: userMilestone.achieved, - }, - new: { - progress, - achieved, - }, - }; + const milestoneInfo = { + milestoneID: userMilestone.milestoneID, + old: { + progress: userMilestone.progress, + achieved: userMilestone.achieved, + }, + new: { + progress, + achieved, + }, + }; - if (progress !== userMilestone.progress) { - importMilestoneInfo.push(milestoneInfo); - } + if (progress !== userMilestone.progress) { + importMilestoneInfo.push(milestoneInfo); + } - if (achieved && !userMilestone.achieved) { - RedisPub("milestone-achieved", { - userID, - ...milestoneInfo, - }); - } - } + if (achieved && !userMilestone.achieved) { + RedisPub("milestone-achieved", { + userID, + ...milestoneInfo, + }); + } + } - if (bwrite.length !== 0) { - await db["user-milestones"].bulkWrite(bwrite, { ordered: false }); - } + if (bwrite.length !== 0) { + await db["user-milestones"].bulkWrite(bwrite, { ordered: false }); + } - return importMilestoneInfo; + return importMilestoneInfo; } async function GetRelevantMilestones( - goalIDs: string[], - game: Game, - playtypes: Playtypes[Game][], - userID: integer, - logger: KtLogger + goalIDs: string[], + game: Game, + playtypes: Playtypes[Game][], + userID: integer, + logger: KtLogger ) { - const userMilestones = await db["user-milestones"].find({ - game, - playtype: { $in: playtypes }, - userID, - }); + const userMilestones = await db["user-milestones"].find({ + game, + playtype: { $in: playtypes }, + userID, + }); - logger.debug(`Found ${userMilestones.length} user-milestones.`); + logger.debug(`Found ${userMilestones.length} user-milestones.`); - const milestones = await db.milestones.find({ - milestoneID: { $in: userMilestones.map((e) => e.milestoneID) }, - "milestoneData.goals.goalID": { $in: goalIDs }, - }); + const milestones = await db.milestones.find({ + milestoneID: { $in: userMilestones.map((e) => e.milestoneID) }, + "milestoneData.goals.goalID": { $in: goalIDs }, + }); - logger.debug(`Found ${milestones.length} relevant milestones.`); + logger.debug(`Found ${milestones.length} relevant milestones.`); - return { userMilestones, milestones }; + return { userMilestones, milestones }; } diff --git a/server/src/lib/score-import/framework/orphans/orphans.test.ts b/server/src/lib/score-import/framework/orphans/orphans.test.ts index 498407836..d3109ddd5 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.test.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.test.ts @@ -12,242 +12,242 @@ import deepmerge from "deepmerge"; const logger = CreateLogCtx(__filename); const batchManualScore: BatchManualScore = { - score: 500, - lamp: "HARD CLEAR", - matchType: "songTitle", - identifier: "5.1.1.", - playtype: "SP", - difficulty: "ANOTHER", + score: 500, + lamp: "HARD CLEAR", + matchType: "songTitle", + identifier: "5.1.1.", + playtype: "SP", + difficulty: "ANOTHER", }; const batchManualContext: BatchManualContext = { game: "iidx", service: "foo", version: "27" }; t.test("#OrphanScore", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should orphan data and context information.", async (t) => { - const res = await OrphanScore( - "ir/direct-manual", - 1, - batchManualScore, - batchManualContext, - "Example Error Message", - logger - ); + t.test("Should orphan data and context information.", async (t) => { + const res = await OrphanScore( + "ir/direct-manual", + 1, + batchManualScore, + batchManualContext, + "Example Error Message", + logger + ); - t.equal(res.success, true); - t.equal( - res.orphanID, - `O${fjsh.hash( - { - importType: "ir/direct-manual", - data: batchManualScore, - context: batchManualContext, - userID: 1, - }, - "sha256" - )}`, - "Should be a checksum of the orphan's contents." - ); + t.equal(res.success, true); + t.equal( + res.orphanID, + `O${fjsh.hash( + { + importType: "ir/direct-manual", + data: batchManualScore, + context: batchManualContext, + userID: 1, + }, + "sha256" + )}`, + "Should be a checksum of the orphan's contents." + ); - const dbCheck = await db["orphan-scores"].findOne({ - orphanID: res.orphanID, - }); + const dbCheck = await db["orphan-scores"].findOne({ + orphanID: res.orphanID, + }); - t.hasStrict(dbCheck, { - orphanID: res.orphanID, - userID: 1, - data: batchManualScore, - context: batchManualContext, - importType: "ir/direct-manual", - errMsg: "Example Error Message", - }); + t.hasStrict(dbCheck, { + orphanID: res.orphanID, + userID: 1, + data: batchManualScore, + context: batchManualContext, + importType: "ir/direct-manual", + errMsg: "Example Error Message", + }); - t.ok( - Math.abs(dbCheck!.timeInserted - Date.now()) < 10_000, - "timeInserted should be decently close to now." - ); + t.ok( + Math.abs(dbCheck!.timeInserted - Date.now()) < 10_000, + "timeInserted should be decently close to now." + ); - t.end(); - }); + t.end(); + }); - t.test("Should skip orphans that already exist.", async (t) => { - const res1 = await OrphanScore( - "ir/direct-manual", - 1, - batchManualScore, - batchManualContext, - "Example Error Message", - logger - ); + t.test("Should skip orphans that already exist.", async (t) => { + const res1 = await OrphanScore( + "ir/direct-manual", + 1, + batchManualScore, + batchManualContext, + "Example Error Message", + logger + ); - const res2 = await OrphanScore( - "ir/direct-manual", - 1, - batchManualScore, - batchManualContext, - "Example Error Message", - logger - ); + const res2 = await OrphanScore( + "ir/direct-manual", + 1, + batchManualScore, + batchManualContext, + "Example Error Message", + logger + ); - t.equal(res2.success, false); - t.equal(res1.orphanID, res2.orphanID); + t.equal(res2.success, false); + t.equal(res1.orphanID, res2.orphanID); - const dbCheck = await db["orphan-scores"].count(); + const dbCheck = await db["orphan-scores"].count(); - t.equal(dbCheck, 1); - }); + t.equal(dbCheck, 1); + }); - t.end(); + t.end(); }); t.test("#ReprocessOrphan", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const orphanDoc: OrphanScoreDocument = { - context: batchManualContext, - data: batchManualScore, - errMsg: "foo", - importType: "ir/direct-manual", - orphanID: "foo", - timeInserted: 0, - userID: 1, - }; + const orphanDoc: OrphanScoreDocument = { + context: batchManualContext, + data: batchManualScore, + errMsg: "foo", + importType: "ir/direct-manual", + orphanID: "foo", + timeInserted: 0, + userID: 1, + }; - t.test("Should convert and insert an orphan if parents are found", async (t) => { - // this orphan doc is for 5.1.1. SPA, which definitely exists in - // the test DB. - const res = await ReprocessOrphan(orphanDoc, logger); + t.test("Should convert and insert an orphan if parents are found", async (t) => { + // this orphan doc is for 5.1.1. SPA, which definitely exists in + // the test DB. + const res = await ReprocessOrphan(orphanDoc, logger); - t.hasStrict( - res, - { - success: true, - type: "ScoreImported", - message: null, - content: { - score: { - game: "iidx", - service: "foo (DIRECT-MANUAL)", - comment: null, - importType: "ir/direct-manual", - timeAchieved: null, - scoreMeta: {}, - scoreData: { - lampIndex: 5, - gradeIndex: 1, - esd: 60.546875, - lamp: "HARD CLEAR", - score: 500, - grade: "E", - percent: 31.806615776081426, - hitData: {}, - hitMeta: {}, - }, - highlight: false, - userID: 1, - calculatedData: { - ktRating: 0.6840944877852578, - ktLampRating: 10.6, - BPI: -15, - "K%": null, - }, - songID: 1, - chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", - scoreID: - "R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602", - playtype: "SP", - isPrimary: true, - }, - }, - }, - "Should successfully import the score." - ); + t.hasStrict( + res, + { + success: true, + type: "ScoreImported", + message: null, + content: { + score: { + game: "iidx", + service: "foo (DIRECT-MANUAL)", + comment: null, + importType: "ir/direct-manual", + timeAchieved: null, + scoreMeta: {}, + scoreData: { + lampIndex: 5, + gradeIndex: 1, + esd: 60.546875, + lamp: "HARD CLEAR", + score: 500, + grade: "E", + percent: 31.806615776081426, + hitData: {}, + hitMeta: {}, + }, + highlight: false, + userID: 1, + calculatedData: { + ktRating: 0.6840944877852578, + ktLampRating: 10.6, + BPI: -15, + "K%": null, + }, + songID: 1, + chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", + scoreID: + "R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602", + playtype: "SP", + isPrimary: true, + }, + }, + }, + "Should successfully import the score." + ); - const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); + const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); - t.equal(orphan, null, "Should remove the orphan document."); + t.equal(orphan, null, "Should remove the orphan document."); - t.end(); - }); + t.end(); + }); - t.test("Should delete the orphan doc and return null if the score is invalid.", async (t) => { - await db["orphan-scores"].insert(orphanDoc); - const res = await ReprocessOrphan( - deepmerge(orphanDoc, { - data: { - score: 99999, - }, - }), - logger - ); + t.test("Should delete the orphan doc and return null if the score is invalid.", async (t) => { + await db["orphan-scores"].insert(orphanDoc); + const res = await ReprocessOrphan( + deepmerge(orphanDoc, { + data: { + score: 99999, + }, + }), + logger + ); - t.equal(res, null); + t.equal(res, null); - const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); + const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); - t.equal(orphan, null, "Should remove the orphan document."); + t.equal(orphan, null, "Should remove the orphan document."); - t.end(); - }); + t.end(); + }); - t.test( - "Should keep the orphan doc and return false if no parents could be found.", - async (t) => { - await db["orphan-scores"].insert(orphanDoc); - const res = await ReprocessOrphan( - deepmerge(orphanDoc, { - data: { - identifier: "NONSENSE CHART TITLE", - }, - }), - logger - ); + t.test( + "Should keep the orphan doc and return false if no parents could be found.", + async (t) => { + await db["orphan-scores"].insert(orphanDoc); + const res = await ReprocessOrphan( + deepmerge(orphanDoc, { + data: { + identifier: "NONSENSE CHART TITLE", + }, + }), + logger + ); - t.equal(res, false); + t.equal(res, false); - const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); + const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); - t.not(orphan, null, "Should not remove the orphan document."); + t.not(orphan, null, "Should not remove the orphan document."); - t.end(); - } - ); + t.end(); + } + ); - t.test( - "Should keep the orphan doc and return false if an internal failure was triggered.", - async (t) => { - await db["orphan-scores"].insert(orphanDoc); - const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; + t.test( + "Should keep the orphan doc and return false if an internal failure was triggered.", + async (t) => { + await db["orphan-scores"].insert(orphanDoc); + const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; - // force an internal failure by removing all bms songs and then - // leaving a valid bms chart to match with. - await db.songs.bms.remove({}); + // force an internal failure by removing all bms songs and then + // leaving a valid bms chart to match with. + await db.songs.bms.remove({}); - const res = await ReprocessOrphan( - deepmerge(orphanDoc, { - context: { - game: "bms", - }, - data: { - matchType: "bmsChartHash", - identifier: GAZER_SHA256, - }, - }), - logger - ); + const res = await ReprocessOrphan( + deepmerge(orphanDoc, { + context: { + game: "bms", + }, + data: { + matchType: "bmsChartHash", + identifier: GAZER_SHA256, + }, + }), + logger + ); - t.equal(res, false); + t.equal(res, false); - const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); + const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID }); - t.not(orphan, null, "Should not remove the orphan document."); + t.not(orphan, null, "Should not remove the orphan document."); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/orphans/orphans.ts b/server/src/lib/score-import/framework/orphans/orphans.ts index 6d4cc04b6..4bce809be 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.ts @@ -1,19 +1,19 @@ import db from "../../../../external/mongo/db"; import { - ConverterFunction, - ConverterFnReturnOrFailure, - ImportTypeContextMap, - ImportTypeDataMap, - OrphanScoreDocument, + ConverterFunction, + ConverterFnReturnOrFailure, + ImportTypeContextMap, + ImportTypeDataMap, + OrphanScoreDocument, } from "../../import-types/common/types"; import { ImportTypes, integer } from "tachi-common"; import fjsh from "fast-json-stable-hash"; import { KtLogger } from "../../../logger/logger"; import { Converters } from "../../import-types/converters"; import { - ConverterFailure, - InternalFailure, - KTDataNotFoundFailure, + ConverterFailure, + InternalFailure, + KTDataNotFoundFailure, } from "../common/converter-failures"; import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing"; @@ -24,39 +24,39 @@ import { ProcessSuccessfulConverterReturn } from "../score-importing/score-impor * @returns Returns { success: true | false, orphanID } */ export async function OrphanScore( - importType: T, - userID: integer, - data: ImportTypeDataMap[T], - context: ImportTypeContextMap[T], - errMsg: string | null, - logger: KtLogger + importType: T, + userID: integer, + data: ImportTypeDataMap[T], + context: ImportTypeContextMap[T], + errMsg: string | null, + logger: KtLogger ) { - const orphan: Pick = { - importType, - data, - context: context, - userID, - }; + const orphan: Pick = { + importType, + data, + context: context, + userID, + }; - const orphanID = `O${fjsh.hash(orphan, "sha256")}`; + const orphanID = `O${fjsh.hash(orphan, "sha256")}`; - const exists = await db["orphan-scores"].findOne({ orphanID }); + const exists = await db["orphan-scores"].findOne({ orphanID }); - if (exists) { - logger.debug(`Skipped orphaning score ${orphanID} because it already exists.`); - return { success: false, orphanID }; - } + if (exists) { + logger.debug(`Skipped orphaning score ${orphanID} because it already exists.`); + return { success: false, orphanID }; + } - const orphanScoreDoc: OrphanScoreDocument = { - ...orphan, - orphanID, - errMsg, - timeInserted: Date.now(), - }; + const orphanScoreDoc: OrphanScoreDocument = { + ...orphan, + orphanID, + errMsg, + timeInserted: Date.now(), + }; - await db["orphan-scores"].insert(orphanScoreDoc); + await db["orphan-scores"].insert(orphanScoreDoc); - return { success: true, orphanID }; + return { success: true, orphanID }; } /** @@ -68,50 +68,50 @@ export async function OrphanScore( * ImportProcessingInfo on success. */ export async function ReprocessOrphan(orphan: OrphanScoreDocument, logger: KtLogger) { - const ConverterFunction = Converters[orphan.importType] as ConverterFunction< - ImportTypeDataMap[ImportTypes], - ImportTypeContextMap[ImportTypes] - >; + const ConverterFunction = Converters[orphan.importType] as ConverterFunction< + ImportTypeDataMap[ImportTypes], + ImportTypeContextMap[ImportTypes] + >; - let res: ConverterFnReturnOrFailure; + let res: ConverterFnReturnOrFailure; - try { - res = await ConverterFunction(orphan.data, orphan.context, orphan.importType, logger); - } catch (err) { - // this is impossible to test, so we're going to ignore it - /* istanbul ignore next */ - if (!(err instanceof ConverterFailure)) { - logger.error(`Converter function ${orphan.importType} returned unexpected error.`, { - err, - }); - throw err; // throw this higher up, i guess. - } + try { + res = await ConverterFunction(orphan.data, orphan.context, orphan.importType, logger); + } catch (err) { + // this is impossible to test, so we're going to ignore it + /* istanbul ignore next */ + if (!(err instanceof ConverterFailure)) { + logger.error(`Converter function ${orphan.importType} returned unexpected error.`, { + err, + }); + throw err; // throw this higher up, i guess. + } - res = err; - } + res = err; + } - // If the data still can't be found, we do nothing about it. - if (res instanceof KTDataNotFoundFailure) { - logger.debug(`Unorphaning ${orphan.orphanID} failed. (${res.message})`); - return false; - } else if (res instanceof InternalFailure) { - logger.error(`Orphan Internal Failure - ${res.message}, OrphanID ${orphan.orphanID}`); + // If the data still can't be found, we do nothing about it. + if (res instanceof KTDataNotFoundFailure) { + logger.debug(`Unorphaning ${orphan.orphanID} failed. (${res.message})`); + return false; + } else if (res instanceof InternalFailure) { + logger.error(`Orphan Internal Failure - ${res.message}, OrphanID ${orphan.orphanID}`); - return false; - } else if (res instanceof ConverterFailure) { - logger.warn( - `Recieved ConverterFailure ${res.message} on orphan ${orphan.orphanID}. Removing orphan.` - ); + return false; + } else if (res instanceof ConverterFailure) { + logger.warn( + `Recieved ConverterFailure ${res.message} on orphan ${orphan.orphanID}. Removing orphan.` + ); - // @danger - This could go terribly, if there's a mistake in the converterFN we might accidentally - // remove a users score. - await db["orphan-scores"].remove({ orphanID: orphan.orphanID }); + // @danger - This could go terribly, if there's a mistake in the converterFN we might accidentally + // remove a users score. + await db["orphan-scores"].remove({ orphanID: orphan.orphanID }); - return null; - } + return null; + } - await db["orphan-scores"].remove({ orphanID: orphan.orphanID }); + await db["orphan-scores"].remove({ orphanID: orphan.orphanID }); - // else, import the orphan. - return ProcessSuccessfulConverterReturn(orphan.userID, res, logger); + // else, import the orphan. + return ProcessSuccessfulConverterReturn(orphan.userID, res, logger); } diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts index a33c1bcac..806b30f52 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts @@ -14,159 +14,159 @@ let IIDXScore = TestingIIDXSPScore; const logger = CreateLogCtx(__filename); t.test("#CreatePBDoc", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => { - // monk adds _id onto the file when you import it, so lets try and avoid that - IIDXScore = GetKTDataJSON("./tachi/iidx-score.json"); - }); + t.beforeEach(ResetDBState); + t.beforeEach(() => { + // monk adds _id onto the file when you import it, so lets try and avoid that + IIDXScore = GetKTDataJSON("./tachi/iidx-score.json"); + }); - const chartID = Testing511SPA.chartID; + const chartID = Testing511SPA.chartID; - const ExamplePBDoc = { - chartID, - userID: 1, - songID: 1, - // rankingInfo -- is not present because it is not added until post-processing. - highlight: false, - isPrimary: true, - timeAchieved: 1619454485988, - game: "iidx", - playtype: "SP", - comments: [], - composedFrom: { - scorePB: IIDXScore.scoreID, - lampPB: "LAMP_PB_ID", - }, - scoreData: { - score: IIDXScore.scoreData.score, - percent: IIDXScore.scoreData.percent, - esd: IIDXScore.scoreData.esd, - grade: IIDXScore.scoreData.grade, - gradeIndex: IIDXScore.scoreData.gradeIndex, - hitData: IIDXScore.scoreData.hitData, - lamp: "FULL COMBO", - lampIndex: lamps.iidx.indexOf("FULL COMBO"), - hitMeta: { bp: 1 }, - }, - calculatedData: { - ktRating: IIDXScore.calculatedData.ktRating, - ktLampRating: 12, - }, - }; + const ExamplePBDoc = { + chartID, + userID: 1, + songID: 1, + // rankingInfo -- is not present because it is not added until post-processing. + highlight: false, + isPrimary: true, + timeAchieved: 1619454485988, + game: "iidx", + playtype: "SP", + comments: [], + composedFrom: { + scorePB: IIDXScore.scoreID, + lampPB: "LAMP_PB_ID", + }, + scoreData: { + score: IIDXScore.scoreData.score, + percent: IIDXScore.scoreData.percent, + esd: IIDXScore.scoreData.esd, + grade: IIDXScore.scoreData.grade, + gradeIndex: IIDXScore.scoreData.gradeIndex, + hitData: IIDXScore.scoreData.hitData, + lamp: "FULL COMBO", + lampIndex: lamps.iidx.indexOf("FULL COMBO"), + hitMeta: { bp: 1 }, + }, + calculatedData: { + ktRating: IIDXScore.calculatedData.ktRating, + ktLampRating: 12, + }, + }; - t.test( - "(IIDX) Should use the GameSpecificMergeFN to also join the BP PB if necessary.", - async (t) => { - await db.scores.remove({}); - await db.scores.insert([ - IIDXScore, - deepmerge(IIDXScore, { - scoreData: { - lamp: "FULL COMBO", - lampIndex: lamps.iidx.indexOf("FULL COMBO"), - score: 0, - percent: 0, - hitMeta: { - bp: 15, - }, - }, - calculatedData: { - ktLampRating: 12, - }, - scoreID: "LAMP_PB_ID", - }), - deepmerge(IIDXScore, { - scoreData: { - lamp: "CLEAR", - lampIndex: lamps.iidx.indexOf("CLEAR"), - score: 1, - percent: 1, - hitMeta: { - bp: 5, - }, - }, - calculatedData: { - ktLampRating: 10, - }, - scoreID: "BP_PB_ID", - }), - ]); + t.test( + "(IIDX) Should use the GameSpecificMergeFN to also join the BP PB if necessary.", + async (t) => { + await db.scores.remove({}); + await db.scores.insert([ + IIDXScore, + deepmerge(IIDXScore, { + scoreData: { + lamp: "FULL COMBO", + lampIndex: lamps.iidx.indexOf("FULL COMBO"), + score: 0, + percent: 0, + hitMeta: { + bp: 15, + }, + }, + calculatedData: { + ktLampRating: 12, + }, + scoreID: "LAMP_PB_ID", + }), + deepmerge(IIDXScore, { + scoreData: { + lamp: "CLEAR", + lampIndex: lamps.iidx.indexOf("CLEAR"), + score: 1, + percent: 1, + hitMeta: { + bp: 5, + }, + }, + calculatedData: { + ktLampRating: 10, + }, + scoreID: "BP_PB_ID", + }), + ]); - const res = await CreatePBDoc(1, chartID, logger); + const res = await CreatePBDoc(1, chartID, logger); - t.not(res, undefined, "Should actually return something."); + t.not(res, undefined, "Should actually return something."); - t.strictSame( - res, - deepmerge(ExamplePBDoc, { - composedFrom: { - other: [{ name: "Best BP", scoreID: "BP_PB_ID" }], - }, - scoreData: { - hitMeta: { - bp: 5, - gsm: null, - gauge: null, - gaugeHistory: null, - }, - }, - }), - "Should correctly return a merged PBDocument with BP" - ); + t.strictSame( + res, + deepmerge(ExamplePBDoc, { + composedFrom: { + other: [{ name: "Best BP", scoreID: "BP_PB_ID" }], + }, + scoreData: { + hitMeta: { + bp: 5, + gsm: null, + gauge: null, + gaugeHistory: null, + }, + }, + }), + "Should correctly return a merged PBDocument with BP" + ); - t.end(); - } - ); + t.end(); + } + ); - t.test("Should merge a score and lamp PB into one document.", async (t) => { - const d = deepmerge(IIDXScore, { - scoreData: { - lamp: "FULL COMBO", - lampIndex: lamps.iidx.indexOf("FULL COMBO"), - score: 0, - percent: 0, - hitMeta: { - bp: 1, - }, - }, - calculatedData: { - ktLampRating: 12, - }, - scoreID: "LAMP_PB_ID", - }); + t.test("Should merge a score and lamp PB into one document.", async (t) => { + const d = deepmerge(IIDXScore, { + scoreData: { + lamp: "FULL COMBO", + lampIndex: lamps.iidx.indexOf("FULL COMBO"), + score: 0, + percent: 0, + hitMeta: { + bp: 1, + }, + }, + calculatedData: { + ktLampRating: 12, + }, + scoreID: "LAMP_PB_ID", + }); - await db.scores.remove({}); - await db.scores.insert([IIDXScore, d]); + await db.scores.remove({}); + await db.scores.insert([IIDXScore, d]); - const res = await CreatePBDoc(1, chartID, logger); + const res = await CreatePBDoc(1, chartID, logger); - t.not(res, undefined, "Should actually return something."); + t.not(res, undefined, "Should actually return something."); - t.strictSame(res, ExamplePBDoc, "Should correctly return a merged PBDocument"); + t.strictSame(res, ExamplePBDoc, "Should correctly return a merged PBDocument"); - t.end(); - }); + t.end(); + }); - t.test("Should bail safely if no score exists when one should", async (t) => { - // a work of genius - const fakeLogger = { - severe: () => (severeCalled = true), - } as unknown as KtLogger; + t.test("Should bail safely if no score exists when one should", async (t) => { + // a work of genius + const fakeLogger = { + severe: () => (severeCalled = true), + } as unknown as KtLogger; - let severeCalled = false; + let severeCalled = false; - await db.scores.remove({}); + await db.scores.remove({}); - const res = await CreatePBDoc(1, chartID, fakeLogger); + const res = await CreatePBDoc(1, chartID, fakeLogger); - t.equal(res, undefined, "Should return nothing (and emit a warning)"); + t.equal(res, undefined, "Should return nothing (and emit a warning)"); - t.equal(severeCalled, true, "Severe logging should have been called."); + t.equal(severeCalled, true, "Severe logging should have been called."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts index 4575506ab..6359230d4 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts @@ -7,89 +7,89 @@ import { BulkWriteUpdateOneOperation } from "mongodb"; export type PBScoreDocumentNoRank = Omit; export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLogger) { - const scorePB = await db.scores.findOne( - { - userID, - chartID, - }, - { - sort: { - "scoreData.percent": -1, - }, - } - ); + const scorePB = await db.scores.findOne( + { + userID, + chartID, + }, + { + sort: { + "scoreData.percent": -1, + }, + } + ); - if (!scorePB) { - logger.severe(`User has no scores on chart, but a PB was attempted to be created?`, { - chartID, - userID, - }); - return; // ?? - } + if (!scorePB) { + logger.severe(`User has no scores on chart, but a PB was attempted to be created?`, { + chartID, + userID, + }); + return; // ?? + } - const lampPB = (await db.scores.findOne( - { - userID, - chartID, - }, - { - sort: { - "scoreData.lampIndex": -1, - }, - } - )) as ScoreDocument; // guaranteed to not be null, as this always resolves - // to atleast one score (and we got ScorePB above, so we know there's - // atleast one). + const lampPB = (await db.scores.findOne( + { + userID, + chartID, + }, + { + sort: { + "scoreData.lampIndex": -1, + }, + } + )) as ScoreDocument; // guaranteed to not be null, as this always resolves + // to atleast one score (and we got ScorePB above, so we know there's + // atleast one). - const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger); + const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger); - if (!pbDoc) { - return; - } + if (!pbDoc) { + return; + } - // finally, return our full pbDoc, that does NOT have the ranking props. - // (We will add those later) - return pbDoc; + // finally, return our full pbDoc, that does NOT have the ranking props. + // (We will add those later) + return pbDoc; } /** * Updates users' rankings on a given chart. */ export async function UpdateChartRanking(chartID: string) { - const scores = await db["personal-bests"].find( - { chartID }, - { - sort: { - "scoreData.percent": -1, - timeAchieved: -1, - }, - } - ); + const scores = await db["personal-bests"].find( + { chartID }, + { + sort: { + "scoreData.percent": -1, + timeAchieved: -1, + }, + } + ); - const bwrite: BulkWriteUpdateOneOperation[] = []; + const bwrite: BulkWriteUpdateOneOperation[] = []; - let rank = 0; + let rank = 0; - for (let i = 0; i < scores.length; i++) { - const score = scores[i]; - rank++; + for (let i = 0; i < scores.length; i++) { + const score = scores[i]; + rank++; - bwrite.push({ - updateOne: { - filter: { chartID: score.chartID, userID: score.userID }, - update: { - $set: { - rankingInfo: { - rank, - outOf: scores.length, - }, - }, - }, - }, - }); - } + bwrite.push({ + updateOne: { + filter: { chartID: score.chartID, userID: score.userID }, + update: { + $set: { + rankingInfo: { + rank, + outOf: scores.length, + }, + }, + }, + }, + }); + } - await db["personal-bests"].bulkWrite(bwrite, { ordered: false }); + await db["personal-bests"].bulkWrite(bwrite, { ordered: false }); } // Explicit acknowledgement that typing this properly simply takes too much time @@ -97,67 +97,67 @@ export async function UpdateChartRanking(chartID: string) { // They return true on success, false on failure, and mutate their arguments. // eslint-disable-next-line @typescript-eslint/no-explicit-any const GAME_SPECIFIC_MERGE_FNS: Record = { - iidx: IIDXMergeFn, - sdvx: SDVXMergeFn, + iidx: IIDXMergeFn, + sdvx: SDVXMergeFn, }; async function MergeScoreLampIntoPB( - userID: integer, - scorePB: ScoreDocument, - lampPB: ScoreDocument, - logger: KtLogger + userID: integer, + scorePB: ScoreDocument, + lampPB: ScoreDocument, + logger: KtLogger ): Promise { - // @hack - // since time cannot be negative, this is a rough hack - // to resolve nullable timeAchieveds without hitting NaN. - let timeAchieved: number | null = Math.max( - scorePB.timeAchieved ?? -1, - lampPB.timeAchieved ?? -1 - ); + // @hack + // since time cannot be negative, this is a rough hack + // to resolve nullable timeAchieveds without hitting NaN. + let timeAchieved: number | null = Math.max( + scorePB.timeAchieved ?? -1, + lampPB.timeAchieved ?? -1 + ); - if (timeAchieved === -1) { - timeAchieved = null; - } + if (timeAchieved === -1) { + timeAchieved = null; + } - const pbDoc: PBScoreDocumentNoRank = { - composedFrom: { - scorePB: scorePB.scoreID, - lampPB: lampPB.scoreID, - }, - chartID: scorePB.chartID, - comments: [scorePB.comment, lampPB.comment].filter((e) => e !== null) as string[], - userID, - songID: scorePB.songID, - highlight: scorePB.highlight || lampPB.highlight, - timeAchieved, - game: scorePB.game, - playtype: scorePB.playtype, - isPrimary: scorePB.isPrimary, - scoreData: { - score: scorePB.scoreData.score, - percent: scorePB.scoreData.percent, - esd: scorePB.scoreData.esd, - grade: scorePB.scoreData.grade, - gradeIndex: scorePB.scoreData.gradeIndex, - lamp: lampPB.scoreData.lamp, - lampIndex: lampPB.scoreData.lampIndex, - hitData: scorePB.scoreData.hitData, - hitMeta: scorePB.scoreData.hitMeta, // this will probably be overrode by game-specific fns - }, - calculatedData: scorePB.calculatedData, - }; + const pbDoc: PBScoreDocumentNoRank = { + composedFrom: { + scorePB: scorePB.scoreID, + lampPB: lampPB.scoreID, + }, + chartID: scorePB.chartID, + comments: [scorePB.comment, lampPB.comment].filter((e) => e !== null) as string[], + userID, + songID: scorePB.songID, + highlight: scorePB.highlight || lampPB.highlight, + timeAchieved, + game: scorePB.game, + playtype: scorePB.playtype, + isPrimary: scorePB.isPrimary, + scoreData: { + score: scorePB.scoreData.score, + percent: scorePB.scoreData.percent, + esd: scorePB.scoreData.esd, + grade: scorePB.scoreData.grade, + gradeIndex: scorePB.scoreData.gradeIndex, + lamp: lampPB.scoreData.lamp, + lampIndex: lampPB.scoreData.lampIndex, + hitData: scorePB.scoreData.hitData, + hitMeta: scorePB.scoreData.hitMeta, // this will probably be overrode by game-specific fns + }, + calculatedData: scorePB.calculatedData, + }; - const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game]; - if (GameSpecificMergeFn) { - const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger); + const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game]; + if (GameSpecificMergeFn) { + const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger); - // If the mergeFn returns false, this means something has gone - // rather wrong. We just return undefined here, which in turn - // tells our calling code to skip this PB entirely. - if (success === false) { - return; - } - } + // If the mergeFn returns false, this means something has gone + // rather wrong. We just return undefined here, which in turn + // tells our calling code to skip this PB entirely. + if (success === false) { + return; + } + } - return pbDoc; + return pbDoc; } diff --git a/server/src/lib/score-import/framework/pb/game-specific-merge.ts b/server/src/lib/score-import/framework/pb/game-specific-merge.ts index e099542c5..4629018c3 100644 --- a/server/src/lib/score-import/framework/pb/game-specific-merge.ts +++ b/server/src/lib/score-import/framework/pb/game-specific-merge.ts @@ -6,57 +6,57 @@ import { CalculateVF6 } from "../calculated-data/stats"; import { InternalFailure } from "../common/converter-failures"; export async function IIDXMergeFn( - pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">, - scorePB: ScoreDocument<"iidx:SP" | "iidx:DP">, - lampPB: ScoreDocument<"iidx:SP" | "iidx:DP">, - logger: KtLogger + pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">, + scorePB: ScoreDocument<"iidx:SP" | "iidx:DP">, + lampPB: ScoreDocument<"iidx:SP" | "iidx:DP">, + logger: KtLogger ): Promise { - // lampRating needs to be updated. - pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating; + // lampRating needs to be updated. + pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating; - // bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about. - const bpPB = (await db.scores.findOne( - { - userID: scorePB.userID, - chartID: scorePB.chartID, - "scoreData.hitMeta.bp": { $exists: true }, - }, - { - sort: { - "scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on - }, - } - )) as ScoreDocument<"iidx:SP" | "iidx:DP">; + // bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about. + const bpPB = (await db.scores.findOne( + { + userID: scorePB.userID, + chartID: scorePB.chartID, + "scoreData.hitMeta.bp": { $exists: true }, + }, + { + sort: { + "scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on + }, + } + )) as ScoreDocument<"iidx:SP" | "iidx:DP">; - if (!bpPB) { - logger.verbose( - `Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`, - { pbDoc } - ); - // this isn't actually an error! we just don't have to do anything. - return true; - } + if (!bpPB) { + logger.verbose( + `Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`, + { pbDoc } + ); + // this isn't actually an error! we just don't have to do anything. + return true; + } - // by default scorePB is chosen for hitMeta fields, so, we can skip any assignments here by returning here. - if (bpPB.scoreID === scorePB.scoreID) { - logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`); - return true; - } else if (bpPB.scoreID === lampPB.scoreID) { - pbDoc.scoreData.hitMeta.bp = lampPB.scoreData.hitMeta.bp; - logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`); - return true; - } + // by default scorePB is chosen for hitMeta fields, so, we can skip any assignments here by returning here. + if (bpPB.scoreID === scorePB.scoreID) { + logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`); + return true; + } else if (bpPB.scoreID === lampPB.scoreID) { + pbDoc.scoreData.hitMeta.bp = lampPB.scoreData.hitMeta.bp; + logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`); + return true; + } - pbDoc.scoreData.hitMeta.bp = bpPB.scoreData.hitMeta.bp!; + pbDoc.scoreData.hitMeta.bp = bpPB.scoreData.hitMeta.bp!; - pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }]; + pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }]; - // Update lamp related iidx-specific info from the lampPB. - pbDoc.scoreData.hitMeta.gsm = lampPB.scoreData.hitMeta.gsm ?? null; - pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge ?? null; - pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory ?? null; + // Update lamp related iidx-specific info from the lampPB. + pbDoc.scoreData.hitMeta.gsm = lampPB.scoreData.hitMeta.gsm ?? null; + pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge ?? null; + pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory ?? null; - return true; + return true; } /** @@ -66,28 +66,28 @@ export async function IIDXMergeFn( * re-calculated for any different permutation of scorePB + lampPB. */ export async function SDVXMergeFn( - pbDoc: PBScoreDocument<"sdvx:Single">, - scorePB: ScoreDocument<"sdvx:Single">, - lampPB: ScoreDocument<"sdvx:Single">, - logger: KtLogger + pbDoc: PBScoreDocument<"sdvx:Single">, + scorePB: ScoreDocument<"sdvx:Single">, + lampPB: ScoreDocument<"sdvx:Single">, + logger: KtLogger ): Promise { - // @optimisable - // This is a re-fetch, but it's difficult to pass the chart all - // the way down here due to how chartIDs (set) works. :( - const chart = await FindChartWithChartID("sdvx", pbDoc.chartID); + // @optimisable + // This is a re-fetch, but it's difficult to pass the chart all + // the way down here due to how chartIDs (set) works. :( + const chart = await FindChartWithChartID("sdvx", pbDoc.chartID); - if (!chart) { - logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`); - throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`); - } + if (!chart) { + logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`); + throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`); + } - pbDoc.calculatedData.VF6 = CalculateVF6( - pbDoc.scoreData.grade, - pbDoc.scoreData.lamp, - pbDoc.scoreData.percent, - chart.levelNum, - logger - ); + pbDoc.calculatedData.VF6 = CalculateVF6( + pbDoc.scoreData.grade, + pbDoc.scoreData.lamp, + pbDoc.scoreData.percent, + chart.levelNum, + logger + ); - return true; + return true; } diff --git a/server/src/lib/score-import/framework/pb/process-pbs.test.ts b/server/src/lib/score-import/framework/pb/process-pbs.test.ts index d51a1338e..5f44770af 100644 --- a/server/src/lib/score-import/framework/pb/process-pbs.test.ts +++ b/server/src/lib/score-import/framework/pb/process-pbs.test.ts @@ -11,52 +11,52 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); t.test("#ProcessPBs", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should successfully insert a pb into the score-pb database", async (t) => { - await db["personal-bests"].remove({}); + t.test("Should successfully insert a pb into the score-pb database", async (t) => { + await db["personal-bests"].remove({}); - // scores on 511 SPA are pre-loaded into the database - await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger); + // scores on 511 SPA are pre-loaded into the database + await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger); - const pbs = await db["personal-bests"].find({}); + const pbs = await db["personal-bests"].find({}); - t.equal(pbs.length, 1, "Should match the amount of PBs inserted into the DB."); + t.equal(pbs.length, 1, "Should match the amount of PBs inserted into the DB."); - t.end(); - }); + t.end(); + }); - t.test("Should successfully insert multiple pbs into the score-pb database", async (t) => { - await db["personal-bests"].remove({}); + t.test("Should successfully insert multiple pbs into the score-pb database", async (t) => { + await db["personal-bests"].remove({}); - await db.scores.insert([ - // @ts-expect-error lol - deepmerge(TestingIIDXSPScore, { - chartID: "test1", - scoreID: crypto.randomBytes(20).toString("hex"), - }), - // @ts-expect-error lol - deepmerge(TestingIIDXSPScore, { - chartID: "test2", - scoreID: crypto.randomBytes(20).toString("hex"), - }), - // @ts-expect-error lol - deepmerge(TestingIIDXSPScore, { - chartID: "test3", - scoreID: crypto.randomBytes(20).toString("hex"), - }), - ]); + await db.scores.insert([ + // @ts-expect-error lol + deepmerge(TestingIIDXSPScore, { + chartID: "test1", + scoreID: crypto.randomBytes(20).toString("hex"), + }), + // @ts-expect-error lol + deepmerge(TestingIIDXSPScore, { + chartID: "test2", + scoreID: crypto.randomBytes(20).toString("hex"), + }), + // @ts-expect-error lol + deepmerge(TestingIIDXSPScore, { + chartID: "test3", + scoreID: crypto.randomBytes(20).toString("hex"), + }), + ]); - await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger); + await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger); - const pbs = await db["personal-bests"].find({}); + const pbs = await db["personal-bests"].find({}); - t.equal(pbs.length, 4, "Should match the amount of PBs inserted into the DB."); + t.equal(pbs.length, 4, "Should match the amount of PBs inserted into the DB."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/pb/process-pbs.ts b/server/src/lib/score-import/framework/pb/process-pbs.ts index c04f7c774..83db9e501 100644 --- a/server/src/lib/score-import/framework/pb/process-pbs.ts +++ b/server/src/lib/score-import/framework/pb/process-pbs.ts @@ -4,57 +4,57 @@ import { KtLogger } from "../../../logger/logger"; import { CreatePBDoc, UpdateChartRanking, PBScoreDocumentNoRank } from "./create-pb-doc"; export async function ProcessPBs( - userID: integer, - chartIDs: Set, - logger: KtLogger + userID: integer, + chartIDs: Set, + logger: KtLogger ): Promise { - if (chartIDs.size === 0) { - return; - } + if (chartIDs.size === 0) { + return; + } - const promises = []; + const promises = []; - for (const chartID of chartIDs) { - promises.push(CreatePBDoc(userID, chartID, logger)); - } + for (const chartID of chartIDs) { + promises.push(CreatePBDoc(userID, chartID, logger)); + } - const pbDocsReturn = await Promise.all(promises); + const pbDocsReturn = await Promise.all(promises); - const pbDocs: PBScoreDocumentNoRank[] = []; + const pbDocs: PBScoreDocumentNoRank[] = []; - for (const doc of pbDocsReturn) { - if (!doc) { - continue; - } + for (const doc of pbDocsReturn) { + if (!doc) { + continue; + } - pbDocs.push(doc); - } + pbDocs.push(doc); + } - if (pbDocsReturn.length === 0) { - return; - } + if (pbDocsReturn.length === 0) { + return; + } - // so here's the kinda awkward part - for the time between this operation - // and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGINFO. - // this *is* bad behaviour, but I don't have a nice way to fix it. - // This should be fixed in the future to avoid crashes between these two - // calls - but that is unlikely. - await db["personal-bests"].bulkWrite( - pbDocs.map((e) => ({ - updateOne: { - filter: { chartID: e.chartID, userID: e.userID }, - update: { $set: e }, - upsert: true, - }, - })), - { - ordered: false, - } - ); + // so here's the kinda awkward part - for the time between this operation + // and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGINFO. + // this *is* bad behaviour, but I don't have a nice way to fix it. + // This should be fixed in the future to avoid crashes between these two + // calls - but that is unlikely. + await db["personal-bests"].bulkWrite( + pbDocs.map((e) => ({ + updateOne: { + filter: { chartID: e.chartID, userID: e.userID }, + update: { $set: e }, + upsert: true, + }, + })), + { + ordered: false, + } + ); - // now that everything has been updated or inserted, we can refresh - // the chart rankings. - await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID))); + // now that everything has been updated or inserted, we can refresh + // the chart rankings. + await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID))); - // and we're done! + // and we're done! } diff --git a/server/src/lib/score-import/framework/score-import-main.ts b/server/src/lib/score-import/framework/score-import-main.ts index 695236e0c..90ad6ec06 100644 --- a/server/src/lib/score-import/framework/score-import-main.ts +++ b/server/src/lib/score-import/framework/score-import-main.ts @@ -1,12 +1,12 @@ import { - ImportProcessingInfo, - ImportTypes, - ImportDocument, - PublicUserDocument, - Playtypes, - Game, - integer, - IDStrings, + ImportProcessingInfo, + ImportTypes, + ImportDocument, + PublicUserDocument, + Playtypes, + Game, + integer, + IDStrings, } from "tachi-common"; import { KtLogger } from "../../logger/logger"; import { ImportAllIterableData } from "./score-importing/score-importing"; @@ -29,225 +29,225 @@ import ScoreImportFatalError from "./score-importing/score-import-error"; * Performs a Score Import. */ export default async function ScoreImportMain( - user: PublicUserDocument, - userIntent: boolean, - importType: ImportTypes, - InputParser: ImportInputParser, - providedImportObjects?: { logger: KtLogger; importID: string } + user: PublicUserDocument, + userIntent: boolean, + importType: ImportTypes, + InputParser: ImportInputParser, + providedImportObjects?: { logger: KtLogger; importID: string } ) { - // in the event of any error, we remove the user lock. - try { - const lock = await GetOrSetUserLock(user.id); + // in the event of any error, we remove the user lock. + try { + const lock = await GetOrSetUserLock(user.id); - if (lock) { - // @danger - // Throwing away an import if the user already has one outgoing is *bad*, as in the case - // of degraded performance we might just start throwing scores away. This is obviously - // not great, but any other solution involves making a queue, which can't be done because - // InputParser is a very dynamic function that cannot be stored in redis or something. - // - // Under normal circumstances, there is no scenario where a user would have two ongoing - // imports at the same time - even if they were using single-score imports on a 5 second - // chart, as each score import takes only around ~10-15miliseconds. - throw new ScoreImportFatalError(409, "This user already has an ongoing import."); - } + if (lock) { + // @danger + // Throwing away an import if the user already has one outgoing is *bad*, as in the case + // of degraded performance we might just start throwing scores away. This is obviously + // not great, but any other solution involves making a queue, which can't be done because + // InputParser is a very dynamic function that cannot be stored in redis or something. + // + // Under normal circumstances, there is no scenario where a user would have two ongoing + // imports at the same time - even if they were using single-score imports on a 5 second + // chart, as each score import takes only around ~10-15miliseconds. + throw new ScoreImportFatalError(409, "This user already has an ongoing import."); + } - const timeStarted = Date.now(); - let importID; - let logger; + const timeStarted = Date.now(); + let importID; + let logger; - if (!providedImportObjects) { - // If they weren't given to us - - // we create an "import logger". - // this holds a reference to the user's name, ID, and type - // of score import for any future debugging. - ({ importID, logger } = CreateImportLoggerAndID(user, importType)); - logger.debug("Received import request."); - } else { - ({ importID, logger } = providedImportObjects); - } + if (!providedImportObjects) { + // If they weren't given to us - + // we create an "import logger". + // this holds a reference to the user's name, ID, and type + // of score import for any future debugging. + ({ importID, logger } = CreateImportLoggerAndID(user, importType)); + logger.debug("Received import request."); + } else { + ({ importID, logger } = providedImportObjects); + } - // --- 1. Parsing --- - // We get an iterable from the provided parser function, alongside some context and a converter function. - // This iterable does not have to be an array - it's anything that's iterable, like a generator. - const parseTimeStart = process.hrtime.bigint(); - const { iterable, context, game, classHandler } = await InputParser(logger); + // --- 1. Parsing --- + // We get an iterable from the provided parser function, alongside some context and a converter function. + // This iterable does not have to be an array - it's anything that's iterable, like a generator. + const parseTimeStart = process.hrtime.bigint(); + const { iterable, context, game, classHandler } = await InputParser(logger); - const parseTime = GetMilisecondsSince(parseTimeStart); + const parseTime = GetMilisecondsSince(parseTimeStart); - logger.debug(`Parsing took ${parseTime} miliseconds.`); + logger.debug(`Parsing took ${parseTime} miliseconds.`); - // We have to cast here due to typescript generic confusions. This is guaranteed` to be correct. - const ConverterFunction = Converters[importType] as unknown as ConverterFunction; + // We have to cast here due to typescript generic confusions. This is guaranteed` to be correct. + const ConverterFunction = Converters[importType] as unknown as ConverterFunction; - // --- 2. Importing --- - // ImportAllIterableData iterates over the iterable, applying the converter function to each bit of data. - const importTimeStart = process.hrtime.bigint(); - const importInfo = await ImportAllIterableData( - user.id, - importType, - iterable, - ConverterFunction, - context, - logger - ); + // --- 2. Importing --- + // ImportAllIterableData iterates over the iterable, applying the converter function to each bit of data. + const importTimeStart = process.hrtime.bigint(); + const importInfo = await ImportAllIterableData( + user.id, + importType, + iterable, + ConverterFunction, + context, + logger + ); - const importTime = GetMilisecondsSince(importTimeStart); - const importTimeRel = importTime / importInfo.length; + const importTime = GetMilisecondsSince(importTimeStart); + const importTimeRel = importTime / importInfo.length; - logger.debug(`Importing took ${importTime} miliseconds. (${importTimeRel}ms/doc)`); + logger.debug(`Importing took ${importTime} miliseconds. (${importTimeRel}ms/doc)`); - // --- 3. ParseImportInfo --- - // ImportInfo is a relatively complex structure. We need some information from it for subsequent steps - // such as the list of chartIDs involved in this import. - const importParseTimeStart = process.hrtime.bigint(); - const { scorePlaytypeMap, errors, scoreIDs, chartIDs } = ParseImportInfo(importInfo); + // --- 3. ParseImportInfo --- + // ImportInfo is a relatively complex structure. We need some information from it for subsequent steps + // such as the list of chartIDs involved in this import. + const importParseTimeStart = process.hrtime.bigint(); + const { scorePlaytypeMap, errors, scoreIDs, chartIDs } = ParseImportInfo(importInfo); - const importParseTime = GetMilisecondsSince(importParseTimeStart); - const importParseTimeRel = importParseTime / importInfo.length; + const importParseTime = GetMilisecondsSince(importParseTimeStart); + const importParseTimeRel = importParseTime / importInfo.length; - logger.debug( - `Import Parsing took ${importParseTime} miliseconds. (${importParseTimeRel}ms/doc)` - ); + logger.debug( + `Import Parsing took ${importParseTime} miliseconds. (${importParseTimeRel}ms/doc)` + ); - // --- 4. Sessions --- - // We create (or update existing) sessions here. This uses the aforementioned parsed import info - // to determine what goes where. - const sessionTimeStart = process.hrtime.bigint(); - const sessionInfo = await CreateSessions( - user.id, - importType, - game, - scorePlaytypeMap, - logger - ); + // --- 4. Sessions --- + // We create (or update existing) sessions here. This uses the aforementioned parsed import info + // to determine what goes where. + const sessionTimeStart = process.hrtime.bigint(); + const sessionInfo = await CreateSessions( + user.id, + importType, + game, + scorePlaytypeMap, + logger + ); - const sessionTime = GetMilisecondsSince(sessionTimeStart); - const sessionTimeRel = sessionTime / sessionInfo.length; + const sessionTime = GetMilisecondsSince(sessionTimeStart); + const sessionTimeRel = sessionTime / sessionInfo.length; - logger.debug( - `Session Processing took ${sessionTime} miliseconds (${sessionTimeRel}ms/doc).` - ); + logger.debug( + `Session Processing took ${sessionTime} miliseconds (${sessionTimeRel}ms/doc).` + ); - // --- 5. PersonalBests --- - // We want to keep an updated reference of a users best score on a given chart. - // This function also handles conjoining different scores together (such as unioning best lamp and - // best score). - const pbTimeStart = process.hrtime.bigint(); - await ProcessPBs(user.id, chartIDs, logger); + // --- 5. PersonalBests --- + // We want to keep an updated reference of a users best score on a given chart. + // This function also handles conjoining different scores together (such as unioning best lamp and + // best score). + const pbTimeStart = process.hrtime.bigint(); + await ProcessPBs(user.id, chartIDs, logger); - const pbTime = GetMilisecondsSince(pbTimeStart); - const pbTimeRel = pbTime / chartIDs.size; + const pbTime = GetMilisecondsSince(pbTimeStart); + const pbTimeRel = pbTime / chartIDs.size; - logger.debug(`PB Processing took ${pbTime} miliseconds (${pbTimeRel}ms/doc)`); + logger.debug(`PB Processing took ${pbTime} miliseconds (${pbTimeRel}ms/doc)`); - const playtypes = Object.keys(scorePlaytypeMap) as Playtypes[Game][]; + const playtypes = Object.keys(scorePlaytypeMap) as Playtypes[Game][]; - // --- 6. Game Stats --- - // This function updates the users "stats" for this game - such as their profile rating or their classes. - const ugsTimeStart = process.hrtime.bigint(); - const classDeltas = await UpdateUsersGameStats( - game, - playtypes, - user.id, - classHandler, - logger - ); + // --- 6. Game Stats --- + // This function updates the users "stats" for this game - such as their profile rating or their classes. + const ugsTimeStart = process.hrtime.bigint(); + const classDeltas = await UpdateUsersGameStats( + game, + playtypes, + user.id, + classHandler, + logger + ); - const ugsTime = GetMilisecondsSince(ugsTimeStart); + const ugsTime = GetMilisecondsSince(ugsTimeStart); - logger.debug(`UGS Processing took ${ugsTime} miliseconds.`); + logger.debug(`UGS Processing took ${ugsTime} miliseconds.`); - // --- 7. Goals --- - // Evaluate and update the users goals. This returns information about goals that have changed. - const goalTimeStart = process.hrtime.bigint(); - const goalInfo = await GetAndUpdateUsersGoals(game, user.id, chartIDs, logger); + // --- 7. Goals --- + // Evaluate and update the users goals. This returns information about goals that have changed. + const goalTimeStart = process.hrtime.bigint(); + const goalInfo = await GetAndUpdateUsersGoals(game, user.id, chartIDs, logger); - const goalTime = GetMilisecondsSince(goalTimeStart); + const goalTime = GetMilisecondsSince(goalTimeStart); - logger.debug(`Goal Processing took ${goalTime} miliseconds.`); + logger.debug(`Goal Processing took ${goalTime} miliseconds.`); - // --- 8. Milestones --- - // Evaluate and update the users milestones. This returns... - const milestoneTimeStart = process.hrtime.bigint(); - const milestoneInfo = await UpdateUsersMilestones( - goalInfo, - game, - playtypes, - user.id, - logger - ); + // --- 8. Milestones --- + // Evaluate and update the users milestones. This returns... + const milestoneTimeStart = process.hrtime.bigint(); + const milestoneInfo = await UpdateUsersMilestones( + goalInfo, + game, + playtypes, + user.id, + logger + ); - const milestoneTime = GetMilisecondsSince(milestoneTimeStart); + const milestoneTime = GetMilisecondsSince(milestoneTimeStart); - logger.debug(`Milestone Processing took ${milestoneTime} miliseconds.`); + logger.debug(`Milestone Processing took ${milestoneTime} miliseconds.`); - // --- 9. Finalise Import Document --- - // Create and Save an import document to the database, and finish everything up! - const ImportDocument: ImportDocument = { - importType, - idStrings: playtypes.map((e) => `${game}:${e}`) as IDStrings[], - scoreIDs, - errors, - importID, - timeFinished: Date.now(), - timeStarted, - createdSessions: sessionInfo, - userID: user.id, - classDeltas, - goalInfo, - milestoneInfo, - userIntent, - }; + // --- 9. Finalise Import Document --- + // Create and Save an import document to the database, and finish everything up! + const ImportDocument: ImportDocument = { + importType, + idStrings: playtypes.map((e) => `${game}:${e}`) as IDStrings[], + scoreIDs, + errors, + importID, + timeFinished: Date.now(), + timeStarted, + createdSessions: sessionInfo, + userID: user.id, + classDeltas, + goalInfo, + milestoneInfo, + userIntent, + }; - const logMessage = `Import took: ${ImportDocument.timeFinished - timeStarted}ms, with ${ - importInfo.length - } documents (Fails: ${errors.length}, Successes: ${scoreIDs.length}, Sessions: ${ - sessionInfo.length - }). Aprx ${(ImportDocument.timeFinished - timeStarted) / importInfo.length}ms/doc`; + const logMessage = `Import took: ${ImportDocument.timeFinished - timeStarted}ms, with ${ + importInfo.length + } documents (Fails: ${errors.length}, Successes: ${scoreIDs.length}, Sessions: ${ + sessionInfo.length + }). Aprx ${(ImportDocument.timeFinished - timeStarted) / importInfo.length}ms/doc`; - // I only really want to log "big" imports. The others are here for debugging purposes. - if (scoreIDs.length > 500) { - logger.info(logMessage); - } else if (scoreIDs.length > 1) { - logger.verbose(logMessage); - } else { - logger.debug(logMessage); - } + // I only really want to log "big" imports. The others are here for debugging purposes. + if (scoreIDs.length > 500) { + logger.info(logMessage); + } else if (scoreIDs.length > 1) { + logger.verbose(logMessage); + } else { + logger.debug(logMessage); + } - await db.imports.insert(ImportDocument); + await db.imports.insert(ImportDocument); - // we don't await this because we don't - // particularly care about waiting for it. - db["import-timings"].insert({ - importID, - timestamp: Date.now(), - total: ImportDocument.timeFinished - timeStarted, - rel: { - import: importTimeRel, - importParse: importParseTimeRel, - pb: pbTimeRel, - session: sessionTimeRel, - }, - abs: { - parse: parseTime, - import: importTime, - importParse: importParseTime, - session: sessionTime, - pb: pbTime, - ugs: ugsTime, - goal: goalTime, - milestone: milestoneTime, - }, - }); + // we don't await this because we don't + // particularly care about waiting for it. + db["import-timings"].insert({ + importID, + timestamp: Date.now(), + total: ImportDocument.timeFinished - timeStarted, + rel: { + import: importTimeRel, + importParse: importParseTimeRel, + pb: pbTimeRel, + session: sessionTimeRel, + }, + abs: { + parse: parseTime, + import: importTime, + importParse: importParseTime, + session: sessionTime, + pb: pbTime, + ugs: ugsTime, + goal: goalTime, + milestone: milestoneTime, + }, + }); - await RemoveUserLock(user.id); + await RemoveUserLock(user.id); - return ImportDocument; - } catch (err) { - await RemoveUserLock(user.id); - throw err; - } + return ImportDocument; + } catch (err) { + await RemoveUserLock(user.id); + throw err; + } } /** @@ -255,20 +255,20 @@ export default async function ScoreImportMain( * @returns A flattened array of ClassDeltas */ async function UpdateUsersGameStats( - game: Game, - playtypes: Playtypes[Game][], - userID: integer, - classHandler: ClassHandler | null, - logger: KtLogger + game: Game, + playtypes: Playtypes[Game][], + userID: integer, + classHandler: ClassHandler | null, + logger: KtLogger ) { - const promises = []; + const promises = []; - for (const pt of playtypes) { - promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger)); - } + for (const pt of playtypes) { + promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger)); + } - const r = await Promise.all(promises); - return r.flat(1); + const r = await Promise.all(promises); + return r.flat(1); } /** @@ -279,26 +279,26 @@ async function UpdateUsersGameStats( * on their playtype. */ function ParseImportInfo(importInfo: ImportProcessingInfo[]) { - const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null); + const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null); - const scoreIDs = []; - const errors = []; - const chartIDs: Set = new Set(); + const scoreIDs = []; + const errors = []; + const chartIDs: Set = new Set(); - for (const info of importInfo) { - if (info.success) { - scoreIDs.push(info.content.score.scoreID); - chartIDs.add(info.content.score.chartID); + for (const info of importInfo) { + if (info.success) { + scoreIDs.push(info.content.score.scoreID); + chartIDs.add(info.content.score.chartID); - if (scorePlaytypeMap[info.content.score.playtype]) { - scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score); - } else { - scorePlaytypeMap[info.content.score.playtype] = [info.content.score]; - } - } else { - errors.push({ type: info.type, message: info.message }); - } - } + if (scorePlaytypeMap[info.content.score.playtype]) { + scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score); + } else { + scorePlaytypeMap[info.content.score.playtype] = [info.content.score]; + } + } else { + errors.push({ type: info.type, message: info.message }); + } + } - return { scoreIDs, errors, scorePlaytypeMap, chartIDs }; + return { scoreIDs, errors, scorePlaytypeMap, chartIDs }; } diff --git a/server/src/lib/score-import/framework/score-importing/hydrate-score.test.ts b/server/src/lib/score-import/framework/score-importing/hydrate-score.test.ts index 7750b4295..c2599adbd 100644 --- a/server/src/lib/score-import/framework/score-importing/hydrate-score.test.ts +++ b/server/src/lib/score-import/framework/score-importing/hydrate-score.test.ts @@ -2,5 +2,5 @@ import t from "tap"; import { HydrateScore } from "./hydrate-score"; t.todo("#HydrateScore", (t) => { - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts index 92332684d..ebac51a87 100644 --- a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts +++ b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts @@ -10,50 +10,50 @@ import { DryScore } from "../common/types"; * @param userID The userID this score is for. */ export async function HydrateScore( - userID: integer, - dryScore: DryScore, - chart: AnyChartDocument, - song: AnySongDocument, - scoreID: string, - logger: KtLogger + userID: integer, + dryScore: DryScore, + chart: AnyChartDocument, + song: AnySongDocument, + scoreID: string, + logger: KtLogger ): Promise { - const esd = CalculateESDForGame( - dryScore.game, - chart.playtype, - dryScore.scoreData.percent / 100 - ); + const esd = CalculateESDForGame( + dryScore.game, + chart.playtype, + dryScore.scoreData.percent / 100 + ); - const calculatedData = await CreateCalculatedData(dryScore, chart, esd, logger); + const calculatedData = await CreateCalculatedData(dryScore, chart, esd, logger); - const { scoreData: dryScoreData, ...rest } = dryScore; + const { scoreData: dryScoreData, ...rest } = dryScore; - // Fill out the rest of the fields we want for scoreData - const scoreData = Object.assign( - { - // @todo #103 lamps may need to be separate upon game:playtype someday. Maybe. We need to check this out - lampIndex: config.lamps[dryScore.game].indexOf(dryScore.scoreData.lamp as string), - gradeIndex: config.grades[dryScore.game].indexOf(dryScore.scoreData.grade as string), - esd, - }, - dryScoreData - ); + // Fill out the rest of the fields we want for scoreData + const scoreData = Object.assign( + { + // @todo #103 lamps may need to be separate upon game:playtype someday. Maybe. We need to check this out + lampIndex: config.lamps[dryScore.game].indexOf(dryScore.scoreData.lamp as string), + gradeIndex: config.grades[dryScore.game].indexOf(dryScore.scoreData.grade as string), + esd, + }, + dryScoreData + ); - const score: ScoreDocument = { - // extract all of the non-scoreData props from a dry score and push them here - ...rest, - // then push our score data. - scoreData, - // everything below this point is sane - highlight: false, - timeAdded: Date.now(), - userID, - calculatedData, - songID: song.id, - chartID: chart.chartID, - scoreID, - playtype: chart.playtype, - isPrimary: chart.isPrimary, - }; + const score: ScoreDocument = { + // extract all of the non-scoreData props from a dry score and push them here + ...rest, + // then push our score data. + scoreData, + // everything below this point is sane + highlight: false, + timeAdded: Date.now(), + userID, + calculatedData, + songID: song.id, + chartID: chart.chartID, + scoreID, + playtype: chart.playtype, + isPrimary: chart.isPrimary, + }; - return score; + return score; } diff --git a/server/src/lib/score-import/framework/score-importing/insert-score.test.ts b/server/src/lib/score-import/framework/score-importing/insert-score.test.ts index 5356b493c..676a44396 100644 --- a/server/src/lib/score-import/framework/score-importing/insert-score.test.ts +++ b/server/src/lib/score-import/framework/score-importing/insert-score.test.ts @@ -7,105 +7,105 @@ import { InsertQueue, QueueScoreInsert } from "./insert-score"; // these two get the same tests, because they're too closely linked t.test("#QueueScoreInsert, #InsertQueue", async (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - // empty scoreDB after - t.afterEach(async () => { - await db.scores.remove({}); - }); + // empty scoreDB after + t.afterEach(async () => { + await db.scores.remove({}); + }); - t.test("Single Queue Test", async (t) => { - // fake score doc - const res = await QueueScoreInsert({ scoreID: "foo" } as unknown as ScoreDocument); + t.test("Single Queue Test", async (t) => { + // fake score doc + const res = await QueueScoreInsert({ scoreID: "foo" } as unknown as ScoreDocument); - t.equal( - res, - true, - "QueueScoreInsert should not insert a score when the queue is not full." - ); + t.equal( + res, + true, + "QueueScoreInsert should not insert a score when the queue is not full." + ); - // this is the best way to get the size of the queue - const flushSize = await InsertQueue(); + // this is the best way to get the size of the queue + const flushSize = await InsertQueue(); - t.equal(flushSize, 1, "QueueScoreInsert should append the score to the queue."); + t.equal(flushSize, 1, "QueueScoreInsert should append the score to the queue."); - const dbRes = await db.scores.find({ - scoreID: "foo", - }); + const dbRes = await db.scores.find({ + scoreID: "foo", + }); - t.equal( - dbRes.length, - 1, - "InsertQueue should insert all 1 members of the queue into the database." - ); + t.equal( + dbRes.length, + 1, + "InsertQueue should insert all 1 members of the queue into the database." + ); - t.end(); - }); + t.end(); + }); - let r = await InsertQueue(); // flush queue just incase former test fails. + let r = await InsertQueue(); // flush queue just incase former test fails. - t.equal(r, 0, "Queue should be empty after test."); + t.equal(r, 0, "Queue should be empty after test."); - t.test("Queue Overflow Test", async (t) => { - for (let i = 0; i < 499; i++) { - // eslint-disable-next-line no-await-in-loop - await QueueScoreInsert({ scoreID: i, chartID: "test" } as unknown as ScoreDocument); - } + t.test("Queue Overflow Test", async (t) => { + for (let i = 0; i < 499; i++) { + // eslint-disable-next-line no-await-in-loop + await QueueScoreInsert({ scoreID: i, chartID: "test" } as unknown as ScoreDocument); + } - const overflowRes = await QueueScoreInsert({ - scoreID: "foo", - chartID: "test", - } as unknown as ScoreDocument); + const overflowRes = await QueueScoreInsert({ + scoreID: "foo", + chartID: "test", + } as unknown as ScoreDocument); - t.equal( - overflowRes, - 500, - "Appending 500 items to the queue should result in them being inserted." - ); + t.equal( + overflowRes, + 500, + "Appending 500 items to the queue should result in them being inserted." + ); - const flushRes = await InsertQueue(); + const flushRes = await InsertQueue(); - t.equal(flushRes, 0, "The queue should now be empty."); + t.equal(flushRes, 0, "The queue should now be empty."); - const dbRes = await db.scores.find({ - scoreID: { $exists: true }, - chartID: "test", - }); + const dbRes = await db.scores.find({ + scoreID: { $exists: true }, + chartID: "test", + }); - t.equal( - dbRes.length, - 500, - "InsertQueue should insert all 500 members of the queue into the database." - ); + t.equal( + dbRes.length, + 500, + "InsertQueue should insert all 500 members of the queue into the database." + ); - t.end(); - }); + t.end(); + }); - r = await InsertQueue(); // flush queue just incase former test fails. + r = await InsertQueue(); // flush queue just incase former test fails. - t.equal(r, 0, "Queue should be empty after test."); + t.equal(r, 0, "Queue should be empty after test."); - t.test("Queue Dedupe Test", async (t) => { - await QueueScoreInsert({ scoreID: 1, chartID: "foo" } as unknown as ScoreDocument); - const r2 = await QueueScoreInsert({ - scoreID: 1, - chartID: "foo", - } as unknown as ScoreDocument); + t.test("Queue Dedupe Test", async (t) => { + await QueueScoreInsert({ scoreID: 1, chartID: "foo" } as unknown as ScoreDocument); + const r2 = await QueueScoreInsert({ + scoreID: 1, + chartID: "foo", + } as unknown as ScoreDocument); - t.equal(r2, null, "Should return null when a duplicate scoreID is submitted"); + t.equal(r2, null, "Should return null when a duplicate scoreID is submitted"); - const flushRes = await InsertQueue(); + const flushRes = await InsertQueue(); - t.equal(flushRes, 1, "Should flush 1 score document"); + t.equal(flushRes, 1, "Should flush 1 score document"); - const dbRes = await db.scores.find({ - chartID: "foo", - }); + const dbRes = await db.scores.find({ + chartID: "foo", + }); - t.equal(dbRes.length, 1, "Should only insert one document"); - }); + t.equal(dbRes.length, 1, "Should only insert one document"); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/score-importing/insert-score.ts b/server/src/lib/score-import/framework/score-importing/insert-score.ts index 376db5978..95cb15a3c 100644 --- a/server/src/lib/score-import/framework/score-importing/insert-score.ts +++ b/server/src/lib/score-import/framework/score-importing/insert-score.ts @@ -14,21 +14,21 @@ const MAX_PIPELINE_LENGTH = 500; * the score provided is already loaded. */ export function QueueScoreInsert(score: ScoreDocument) { - if (ScoreIDs.has(score.scoreID)) { - // skip - logger.verbose(`Triggered skip for ID ${score.scoreID}`); - return null; - } + if (ScoreIDs.has(score.scoreID)) { + // skip + logger.verbose(`Triggered skip for ID ${score.scoreID}`); + return null; + } - ScoreQueue.push(score); - ScoreIDs.add(score.scoreID); + ScoreQueue.push(score); + ScoreIDs.add(score.scoreID); - if (ScoreQueue.length >= MAX_PIPELINE_LENGTH) { - logger.verbose(`Triggered pipeline flush with len ${ScoreQueue.length}.`); - return InsertQueue(); - } + if (ScoreQueue.length >= MAX_PIPELINE_LENGTH) { + logger.verbose(`Triggered pipeline flush with len ${ScoreQueue.length}.`); + return InsertQueue(); + } - return true; + return true; } /** @@ -36,18 +36,18 @@ export function QueueScoreInsert(score: ScoreDocument) { * @warn Be cautious of inducing race conditions when using this function. */ export async function InsertQueue() { - const temp = ScoreQueue.splice(0); - if (temp.length !== 0) { - ScoreIDs = new Set(); - try { - await db.scores.insert(temp); - } catch (err) { - logger.warn( - `Triggered duplicate key protection. Race condition protected against, but this is not good.` - ); - return null; - } - } + const temp = ScoreQueue.splice(0); + if (temp.length !== 0) { + ScoreIDs = new Set(); + try { + await db.scores.insert(temp); + } catch (err) { + logger.warn( + `Triggered duplicate key protection. Race condition protected against, but this is not good.` + ); + return null; + } + } - return temp.length; + return temp.length; } diff --git a/server/src/lib/score-import/framework/score-importing/score-id.test.ts b/server/src/lib/score-import/framework/score-importing/score-id.test.ts index 2a6bdb9f1..bb02e4c7d 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.test.ts @@ -7,90 +7,90 @@ import { CreateScoreID, GetWithScoreID } from "./score-id"; t.beforeEach(ResetDBState); t.test("#GetWithScoreID", async (t) => { - const sc = await GetWithScoreID("TESTING_SCORE_ID"); + const sc = await GetWithScoreID("TESTING_SCORE_ID"); - t.not(sc, null, "Return a score for a valid score id"); + t.not(sc, null, "Return a score for a valid score id"); - const nl = await GetWithScoreID("INVALID SCORE ID"); + const nl = await GetWithScoreID("INVALID SCORE ID"); - t.equal(nl, null, "Should return null for score IDs it cannot resolve"); + t.equal(nl, null, "Should return null for score IDs it cannot resolve"); - t.end(); + t.end(); }); t.test("#CreateScoreID", (t) => { - const scoreID = CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID); + const scoreID = CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID); - t.match( - scoreID, - /^R[0-9a-f]{40}/u, - "Should return an R followed by 40 characters of lowercase hex as scoreID." - ); + t.match( + scoreID, + /^R[0-9a-f]{40}/u, + "Should return an R followed by 40 characters of lowercase hex as scoreID." + ); - t.not( - scoreID, - CreateScoreID(2, TestingIIDXSPDryScore, Testing511SPA.chartID), - "Hash should be affected by userID." - ); + t.not( + scoreID, + CreateScoreID(2, TestingIIDXSPDryScore, Testing511SPA.chartID), + "Hash should be affected by userID." + ); - t.equal( - scoreID, - CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID), - "ScoreIDs should be consistently generatable" - ); + t.equal( + scoreID, + CreateScoreID(1, TestingIIDXSPDryScore, Testing511SPA.chartID), + "ScoreIDs should be consistently generatable" + ); - // duplicate score - const minimalScore = { - scoreData: { - percent: TestingIIDXSPDryScore.scoreData.percent, - score: TestingIIDXSPDryScore.scoreData.score, - grade: TestingIIDXSPDryScore.scoreData.grade, - lamp: TestingIIDXSPDryScore.scoreData.lamp, - }, - }; + // duplicate score + const minimalScore = { + scoreData: { + percent: TestingIIDXSPDryScore.scoreData.percent, + score: TestingIIDXSPDryScore.scoreData.score, + grade: TestingIIDXSPDryScore.scoreData.grade, + lamp: TestingIIDXSPDryScore.scoreData.lamp, + }, + }; - t.equal( - scoreID, - CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), - "ScoreIDs should only be affected by score, percent, grade and lamp." - ); + t.equal( + scoreID, + CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), + "ScoreIDs should only be affected by score, percent, grade and lamp." + ); - minimalScore.scoreData.percent = 0; + minimalScore.scoreData.percent = 0; - t.not( - scoreID, - CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), - "ScoreIDs should not produce the same value if percent is different." - ); + t.not( + scoreID, + CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), + "ScoreIDs should not produce the same value if percent is different." + ); - minimalScore.scoreData.percent = TestingIIDXSPDryScore.scoreData.percent; - minimalScore.scoreData.score = 0; + minimalScore.scoreData.percent = TestingIIDXSPDryScore.scoreData.percent; + minimalScore.scoreData.score = 0; - t.not( - scoreID, - CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), - "ScoreIDs should not produce the same value if score is different." - ); + t.not( + scoreID, + CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), + "ScoreIDs should not produce the same value if score is different." + ); - minimalScore.scoreData.score = TestingIIDXSPDryScore.scoreData.score; - minimalScore.scoreData.lamp = "ASSIST CLEAR"; + minimalScore.scoreData.score = TestingIIDXSPDryScore.scoreData.score; + minimalScore.scoreData.lamp = "ASSIST CLEAR"; - t.not( - scoreID, - CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), - "ScoreIDs should not produce the same value if lamp is different." - ); + t.not( + scoreID, + CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), + "ScoreIDs should not produce the same value if lamp is different." + ); - minimalScore.scoreData.lamp = TestingIIDXSPDryScore.scoreData.lamp; - minimalScore.scoreData.grade = "F"; + minimalScore.scoreData.lamp = TestingIIDXSPDryScore.scoreData.lamp; + minimalScore.scoreData.grade = "F"; - t.not( - scoreID, - CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), - "ScoreIDs should not produce the same value if grade is different." - ); + t.not( + scoreID, + CreateScoreID(1, minimalScore as ScoreDocument, Testing511SPA.chartID), + "ScoreIDs should not produce the same value if grade is different." + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/score-importing/score-id.ts b/server/src/lib/score-import/framework/score-importing/score-id.ts index b4df536fa..df5ffcfcf 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.ts @@ -4,9 +4,9 @@ import { integer } from "tachi-common"; import { DryScore } from "../common/types"; function CreateScoreIDString(userID: integer, partialScore: DryScore, chartID: string) { - const { lamp, grade } = partialScore.scoreData; + const { lamp, grade } = partialScore.scoreData; - return `${userID}|${chartID}|${lamp}|${grade}|${partialScore.scoreData.score}|${partialScore.scoreData.percent}`; + return `${userID}|${chartID}|${lamp}|${grade}|${partialScore.scoreData.score}|${partialScore.scoreData.percent}`; } /** @@ -15,7 +15,7 @@ function CreateScoreIDString(userID: integer, partialScore: DryScore, chartID: s * @returns A sha256 checksum in lowercase hex. */ function HashScoreIDString(scoreIDString: string) { - return crypto.createHash("sha256").update(scoreIDString).digest("hex"); + return crypto.createHash("sha256").update(scoreIDString).digest("hex"); } /** @@ -24,13 +24,13 @@ function HashScoreIDString(scoreIDString: string) { * @returns @see HashScoreIDString - prefixed with R. */ export function CreateScoreID(userID: integer, dryScore: DryScore, chartID: string) { - const scoreIDString = CreateScoreIDString(userID, dryScore, chartID); + const scoreIDString = CreateScoreIDString(userID, dryScore, chartID); - return `R${HashScoreIDString(scoreIDString)}`; + return `R${HashScoreIDString(scoreIDString)}`; } export function GetWithScoreID(scoreID: string) { - return db.scores.findOne({ - scoreID, - }); + return db.scores.findOne({ + scoreID, + }); } diff --git a/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts b/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts index ffc4d8779..bb5cc9c85 100644 --- a/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts @@ -2,10 +2,10 @@ import t from "tap"; import ScoreImportFatalError from "./score-import-error"; t.test("#new ScoreImportFatalError()", (t) => { - const err = new ScoreImportFatalError(500, "error message"); + const err = new ScoreImportFatalError(500, "error message"); - t.equal(err.statusCode, 500, "Should store the status code passed to it."); - t.equal(err.message, "error message", "Should store description as the error message."); + t.equal(err.statusCode, 500, "Should store the status code passed to it."); + t.equal(err.message, "error message", "Should store description as the error message."); - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/score-importing/score-import-error.ts b/server/src/lib/score-import/framework/score-importing/score-import-error.ts index 0d219c745..1091eb050 100644 --- a/server/src/lib/score-import/framework/score-importing/score-import-error.ts +++ b/server/src/lib/score-import/framework/score-importing/score-import-error.ts @@ -6,11 +6,11 @@ * process. */ export default class ScoreImportFatalError extends Error { - statusCode: number; + statusCode: number; - constructor(statusCode: number, description: string) { - super(description); + constructor(statusCode: number, description: string) { + super(description); - this.statusCode = statusCode; - } + this.statusCode = statusCode; + } } diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.test.ts b/server/src/lib/score-import/framework/score-importing/score-importing.test.ts index 372f96474..38499a7b9 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.test.ts @@ -2,9 +2,9 @@ import t from "tap"; import { ImportAllIterableData, ImportIterableDatapoint } from "./score-importing"; t.todo("#ImportAllIterableData", (t) => { - t.end(); + t.end(); }); t.todo("#ImportIterableDatapoint", (t) => { - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.ts b/server/src/lib/score-import/framework/score-importing/score-importing.ts index 2e5d1ce62..6d7d35e7d 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.ts @@ -1,29 +1,29 @@ import { - AnyChartDocument, - ImportProcessingInfo, - integer, - ScoreDocument, - AnySongDocument, - ImportTypes, - IDStrings, + AnyChartDocument, + ImportProcessingInfo, + integer, + ScoreDocument, + AnySongDocument, + ImportTypes, + IDStrings, } from "tachi-common"; import { HydrateScore } from "./hydrate-score"; import { InsertQueue, QueueScoreInsert, ScoreIDs } from "./insert-score"; import { - ConverterFailure, - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, - SkipScoreFailure, + ConverterFailure, + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, + SkipScoreFailure, } from "../common/converter-failures"; import { CreateScoreID } from "./score-id"; import db from "../../../../external/mongo/db"; import { AppendLogCtx, KtLogger } from "../../../logger/logger"; import { - ConverterFnReturnOrFailure, - ConverterFunction, - ConverterFnSuccessReturn, + ConverterFnReturnOrFailure, + ConverterFunction, + ConverterFnSuccessReturn, } from "../../import-types/common/types"; import { DryScore } from "../common/types"; import { OrphanScore } from "../orphans/orphans"; @@ -37,49 +37,49 @@ import { OrphanScore } from "../orphans/orphans"; * @returns An array of ImportProcessInfo objects. */ export async function ImportAllIterableData( - userID: integer, - importType: ImportTypes, - iterableData: Iterable | AsyncIterable, - ConverterFunction: ConverterFunction, - context: C, - logger: KtLogger + userID: integer, + importType: ImportTypes, + iterableData: Iterable | AsyncIterable, + ConverterFunction: ConverterFunction, + context: C, + logger: KtLogger ): Promise { - logger.verbose(`Starting Data Processing...`); + logger.verbose(`Starting Data Processing...`); - const promises = []; + const promises = []; - // for await is used here as iterableData may be an async iterable - // An example would be making an api request after exhausting - // the first set of data. - for await (const data of iterableData) { - promises.push( - ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger) - ); - } + // for await is used here as iterableData may be an async iterable + // An example would be making an api request after exhausting + // the first set of data. + for await (const data of iterableData) { + promises.push( + ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger) + ); + } - // We need to filter out nulls, which we don't care for (these are neither successes or failures) - const processedResults = await Promise.all(promises); + // We need to filter out nulls, which we don't care for (these are neither successes or failures) + const processedResults = await Promise.all(promises); - logger.verbose(`Finished Importing Data (${promises.length} datapoints).`); - logger.debug(`Removing null returns...`); + logger.verbose(`Finished Importing Data (${promises.length} datapoints).`); + logger.debug(`Removing null returns...`); - const datapoints = processedResults.filter( - (e) => e !== null - ) as ImportProcessingInfo[]; + const datapoints = processedResults.filter( + (e) => e !== null + ) as ImportProcessingInfo[]; - logger.debug(`Removed null from results.`); + logger.debug(`Removed null from results.`); - logger.verbose(`Recieved ${datapoints.length} returns, from ${promises.length} data.`); + logger.verbose(`Recieved ${datapoints.length} returns, from ${promises.length} data.`); - // Flush the score queue out after finishing most of the import. This ensures no scores get left in the - // queue. - const emptied = await InsertQueue(); + // Flush the score queue out after finishing most of the import. This ensures no scores get left in the + // queue. + const emptied = await InsertQueue(); - if (emptied) { - logger.verbose(`Emptied ${emptied} documents from score queue.`); - } + if (emptied) { + logger.verbose(`Emptied ${emptied} documents from score queue.`); + } - return datapoints; + return datapoints; } /** @@ -91,140 +91,140 @@ export async function ImportAllIterableData( * @returns An array of ImportProcessingInfo objects, or a single ImportProcessingInfo object */ export async function ImportIterableDatapoint( - userID: integer, - importType: ImportTypes, - data: D, - ConverterFunction: ConverterFunction, - context: C, - logger: KtLogger + userID: integer, + importType: ImportTypes, + data: D, + ConverterFunction: ConverterFunction, + context: C, + logger: KtLogger ): Promise { - // Converter Function Return - let cfnReturn: ConverterFnReturnOrFailure; + // Converter Function Return + let cfnReturn: ConverterFnReturnOrFailure; - try { - cfnReturn = await ConverterFunction(data, context, importType, logger); - } catch (err) { - cfnReturn = err; - } + try { + cfnReturn = await ConverterFunction(data, context, importType, logger); + } catch (err) { + cfnReturn = err; + } - // if this conversion failed, return it in the proper format - if (cfnReturn instanceof ConverterFailure) { - if (cfnReturn instanceof KTDataNotFoundFailure) { - logger.warn(`KTDataNotFoundFailure: ${cfnReturn.message ?? "No message?"}`, { - cfnReturn, - hideFromConsole: ["cfnReturn"], - }); + // if this conversion failed, return it in the proper format + if (cfnReturn instanceof ConverterFailure) { + if (cfnReturn instanceof KTDataNotFoundFailure) { + logger.warn(`KTDataNotFoundFailure: ${cfnReturn.message ?? "No message?"}`, { + cfnReturn, + hideFromConsole: ["cfnReturn"], + }); - const insertOrphan = await OrphanScore( - cfnReturn.importType, - userID, - cfnReturn.data, - cfnReturn.converterContext, - cfnReturn.message, - logger - ); + const insertOrphan = await OrphanScore( + cfnReturn.importType, + userID, + cfnReturn.data, + cfnReturn.converterContext, + cfnReturn.message, + logger + ); - if (insertOrphan.success) { - return { - success: false, - type: "KTDataNotFound", - message: cfnReturn.message, - content: { - context: cfnReturn.converterContext, - data: cfnReturn.data, - orphanID: insertOrphan.orphanID, - }, - }; - } + if (insertOrphan.success) { + return { + success: false, + type: "KTDataNotFound", + message: cfnReturn.message, + content: { + context: cfnReturn.converterContext, + data: cfnReturn.data, + orphanID: insertOrphan.orphanID, + }, + }; + } - return { - success: false, - type: "OrphanExists", - message: cfnReturn.message, - content: { - orphanID: insertOrphan.orphanID, - }, - }; - } else if (cfnReturn instanceof InvalidScoreFailure) { - logger.info(`InvalidScoreFailure: ${cfnReturn.message ?? "No message?"}`, { - cfnReturn, - hideFromConsole: ["cfnReturn"], - }); - return { - success: false, - type: "InvalidDatapoint", - message: cfnReturn.message, - content: {}, - }; - } else if (cfnReturn instanceof InternalFailure) { - logger.error(`Internal error occured.`, { cfnReturn }); - return { - success: false, - type: "InternalError", - // could return cfnReturn.message here, but we might want to hide the details of the crash. - message: "An internal error has occured.", - content: {}, - }; - } else if (cfnReturn instanceof SkipScoreFailure) { - return null; - } else { - logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, { - err: cfnReturn, - }); - return { - success: false, - type: "InternalError", - message: "An internal service error has occured.", - content: {}, - }; - } - } + return { + success: false, + type: "OrphanExists", + message: cfnReturn.message, + content: { + orphanID: insertOrphan.orphanID, + }, + }; + } else if (cfnReturn instanceof InvalidScoreFailure) { + logger.info(`InvalidScoreFailure: ${cfnReturn.message ?? "No message?"}`, { + cfnReturn, + hideFromConsole: ["cfnReturn"], + }); + return { + success: false, + type: "InvalidDatapoint", + message: cfnReturn.message, + content: {}, + }; + } else if (cfnReturn instanceof InternalFailure) { + logger.error(`Internal error occured.`, { cfnReturn }); + return { + success: false, + type: "InternalError", + // could return cfnReturn.message here, but we might want to hide the details of the crash. + message: "An internal error has occured.", + content: {}, + }; + } else if (cfnReturn instanceof SkipScoreFailure) { + return null; + } else { + logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, { + err: cfnReturn, + }); + return { + success: false, + type: "InternalError", + message: "An internal service error has occured.", + content: {}, + }; + } + } - if (cfnReturn instanceof Error) { - logger.error(`Unknown error thrown from converter, Ignoring.`, { - err: cfnReturn, - }); - return { - success: false, - type: "InternalError", - message: "An internal service error has occured.", - content: {}, - }; - } + if (cfnReturn instanceof Error) { + logger.error(`Unknown error thrown from converter, Ignoring.`, { + err: cfnReturn, + }); + return { + success: false, + type: "InternalError", + message: "An internal service error has occured.", + content: {}, + }; + } - return ProcessSuccessfulConverterReturn(userID, cfnReturn as ConverterFnSuccessReturn, logger); + return ProcessSuccessfulConverterReturn(userID, cfnReturn as ConverterFnSuccessReturn, logger); } export async function ProcessSuccessfulConverterReturn( - userID: integer, - cfnReturn: ConverterFnSuccessReturn, - logger: KtLogger + userID: integer, + cfnReturn: ConverterFnSuccessReturn, + logger: KtLogger ): Promise { - const result = await HydrateAndInsertScore( - userID, - cfnReturn.dryScore, - cfnReturn.chart, - cfnReturn.song, - logger - ); + const result = await HydrateAndInsertScore( + userID, + cfnReturn.dryScore, + cfnReturn.chart, + cfnReturn.song, + logger + ); - // This used to be a ScoreExists error. However, we never actually care about - // handling ScoreExists errors (they're nobodies issue) - // so instead, the function will just return null, and we pass that on here. - if (result === null) { - return null; - } + // This used to be a ScoreExists error. However, we never actually care about + // handling ScoreExists errors (they're nobodies issue) + // so instead, the function will just return null, and we pass that on here. + if (result === null) { + return null; + } - logger.debug(`Successfully imported score: ${result.scoreID}`); + logger.debug(`Successfully imported score: ${result.scoreID}`); - return { - success: true, - type: "ScoreImported", - message: null, - content: { - score: result, - }, - }; + return { + success: true, + type: "ScoreImported", + message: null, + content: { + score: result, + }, + }; } /** @@ -235,52 +235,52 @@ export async function ProcessSuccessfulConverterReturn( * @param song - The song this score is on. */ async function HydrateAndInsertScore( - userID: integer, - dryScore: DryScore, - chart: AnyChartDocument, - song: AnySongDocument, - importLogger: KtLogger + userID: integer, + dryScore: DryScore, + chart: AnyChartDocument, + song: AnySongDocument, + importLogger: KtLogger ): Promise { - const scoreID = CreateScoreID(userID, dryScore, chart.chartID); + const scoreID = CreateScoreID(userID, dryScore, chart.chartID); - // sub-context the logger so the below logs are more accurate - const logger = AppendLogCtx(scoreID, importLogger); + // sub-context the logger so the below logs are more accurate + const logger = AppendLogCtx(scoreID, importLogger); - const existingScore = await db.scores.findOne( - { - scoreID, - }, - { - // micro-optimisation - mongoDB is significantly faster when returning less fields - // since we only care about whether we have a score or not here, we can minimise returned - // fields. - // - // feel free to test this yourself! - zkldi (09/04/2021) - projection: { - _id: 1, - }, - } - ); + const existingScore = await db.scores.findOne( + { + scoreID, + }, + { + // micro-optimisation - mongoDB is significantly faster when returning less fields + // since we only care about whether we have a score or not here, we can minimise returned + // fields. + // + // feel free to test this yourself! - zkldi (09/04/2021) + projection: { + _id: 1, + }, + } + ); - if (existingScore) { - logger.verbose(`Skipped score.`); - return null; - } + if (existingScore) { + logger.verbose(`Skipped score.`); + return null; + } - if (ScoreIDs.has(scoreID)) { - logger.verbose(`Skipped score.`); - return null; - } + if (ScoreIDs.has(scoreID)) { + logger.verbose(`Skipped score.`); + return null; + } - const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger); + const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger); - const res = await QueueScoreInsert(score); + const res = await QueueScoreInsert(score); - // emergency state - this is a last resort for avoiding doubled imports - if (res === null) { - logger.verbose(`Skipped score - Race Condition protection triggered.`); - return null; - } + // emergency state - this is a last resort for avoiding doubled imports + if (res === null) { + logger.verbose(`Skipped score - Race Condition protection triggered.`); + return null; + } - return score; + return score; } diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.test.ts b/server/src/lib/score-import/framework/sessions/calculated-data.test.ts index 6ed9d63cd..9d7b6f3fa 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.test.ts @@ -2,68 +2,68 @@ import t from "tap"; import { CreateSessionCalcData } from "./calculated-data"; function ratingwrap(ratings: [number, number][]) { - return ratings.map((e) => ({ - calculatedData: { - ktRating: e[0], - ktLampRating: e[1], - }, - })); + return ratings.map((e) => ({ + calculatedData: { + ktRating: e[0], + ktLampRating: e[1], + }, + })); } function avgbest10(arr: number[]) { - return ( - arr - .sort((a, b) => b - a) - .slice(0, 10) - .reduce((a, r) => a + r, 0) / 10 - ); + return ( + arr + .sort((a, b) => b - a) + .slice(0, 10) + .reduce((a, r) => a + r, 0) / 10 + ); } t.test("#CreateSessionCalcData", (t) => { - t.test("Should return null if less than 10 scores", (t) => { - const res = CreateSessionCalcData("iidx", "SP", ratingwrap([[1, 2]])); + t.test("Should return null if less than 10 scores", (t) => { + const res = CreateSessionCalcData("iidx", "SP", ratingwrap([[1, 2]])); - t.strictSame(res, { BPI: null, ktRating: null, ktLampRating: null }); + t.strictSame(res, { BPI: null, ktRating: null, ktLampRating: null }); - t.end(); - }); + t.end(); + }); - t.test("Should calculate session performance", (t) => { - const { BPI, ktRating, ktLampRating } = CreateSessionCalcData( - "iidx", - "SP", - ratingwrap([ - [1, 1], - [2, 2], - [3, 4], - [4, 4], - [6, 1], - [1, 6], - [1, 1], - [2, 2], - [3, 4], - [4, 4], - [6, 1], - [1, 6], - ]) - ); + t.test("Should calculate session performance", (t) => { + const { BPI, ktRating, ktLampRating } = CreateSessionCalcData( + "iidx", + "SP", + ratingwrap([ + [1, 1], + [2, 2], + [3, 4], + [4, 4], + [6, 1], + [1, 6], + [1, 1], + [2, 2], + [3, 4], + [4, 4], + [6, 1], + [1, 6], + ]) + ); - t.equal( - ktLampRating, - avgbest10([1, 2, 4, 4, 1, 6, 1, 2, 4, 4, 1, 6]), - "Should correctly calculate lamp performance" - ); + t.equal( + ktLampRating, + avgbest10([1, 2, 4, 4, 1, 6, 1, 2, 4, 4, 1, 6]), + "Should correctly calculate lamp performance" + ); - t.equal( - ktRating, - avgbest10([1, 2, 3, 4, 6, 1, 1, 2, 3, 4, 6, 1]), - "Should correctly calculate score performance" - ); + t.equal( + ktRating, + avgbest10([1, 2, 3, 4, 6, 1, 1, 2, 3, 4, 6, 1]), + "Should correctly calculate score performance" + ); - t.equal(BPI, null); + t.equal(BPI, null); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.ts b/server/src/lib/score-import/framework/sessions/calculated-data.ts index 75ce6b1f1..bf1547dbd 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.ts @@ -5,131 +5,131 @@ type ScoreCalculatedDataOnly = Pick; const RELEVANT_SCORES = 10; function AverageBest10(vals: (number | null | undefined)[]) { - const numbers = vals.filter((e) => typeof e === "number") as number[]; + const numbers = vals.filter((e) => typeof e === "number") as number[]; - if (numbers.length < RELEVANT_SCORES) { - return null; - } + if (numbers.length < RELEVANT_SCORES) { + return null; + } - return ( - numbers - .sort((a, b) => b - a) - .slice(0, RELEVANT_SCORES) - .reduce((a, r) => a + r, 0) / RELEVANT_SCORES - ); + return ( + numbers + .sort((a, b) => b - a) + .slice(0, RELEVANT_SCORES) + .reduce((a, r) => a + r, 0) / RELEVANT_SCORES + ); } function SumAll(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) { - return arr.reduce((a, r) => { - if (typeof r.calculatedData[prop] === "number") { - return a + (r.calculatedData[prop] as number); - } + return arr.reduce((a, r) => { + if (typeof r.calculatedData[prop] === "number") { + return a + (r.calculatedData[prop] as number); + } - return a; - }, 0); + return a; + }, 0); } function AvgBest10Map(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) { - return AverageBest10(arr.map((e) => e.calculatedData[prop])); + return AverageBest10(arr.map((e) => e.calculatedData[prop])); } type CalculatedDataFunctions = { - [G in Game]: { - [P in Playtypes[G]]: ( - scoreCalcData: ScoreCalculatedDataOnly[] - ) => SessionDocument["calculatedData"]; - }; + [G in Game]: { + [P in Playtypes[G]]: ( + scoreCalcData: ScoreCalculatedDataOnly[] + ) => SessionDocument["calculatedData"]; + }; }; const CalculatedDataFunctions: CalculatedDataFunctions = { - iidx: { - SP: (scd) => ({ - BPI: AvgBest10Map(scd, "BPI"), - ktRating: AvgBest10Map(scd, "ktRating"), - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - DP: (scd) => ({ - BPI: AvgBest10Map(scd, "BPI"), - ktRating: AvgBest10Map(scd, "ktRating"), - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - }, - sdvx: { - Single: (scd) => { - const VF6 = AvgBest10Map(scd, "VF6"); + iidx: { + SP: (scd) => ({ + BPI: AvgBest10Map(scd, "BPI"), + ktRating: AvgBest10Map(scd, "ktRating"), + ktLampRating: AvgBest10Map(scd, "ktLampRating"), + }), + DP: (scd) => ({ + BPI: AvgBest10Map(scd, "BPI"), + ktRating: AvgBest10Map(scd, "ktRating"), + ktLampRating: AvgBest10Map(scd, "ktLampRating"), + }), + }, + sdvx: { + Single: (scd) => { + const VF6 = AvgBest10Map(scd, "VF6"); - return { - VF6, - profileVF6: VF6 === null ? null : VF6 * 50, - }; - }, - }, - popn: { - "9B": () => ({}), - }, - museca: { - Single: (scd) => ({ - ktRating: AvgBest10Map(scd, "ktRating"), - }), - }, - chunithm: { - Single: (scd) => ({ - rating: AvgBest10Map(scd, "rating"), - }), - }, - maimai: { - Single: (scd) => ({ - ktRating: AvgBest10Map(scd, "ktRating"), - }), - }, - gitadora: { - Gita: (scd) => ({ - skill: AvgBest10Map(scd, "skill"), - }), - Dora: (scd) => ({ - skill: AvgBest10Map(scd, "skill"), - }), - }, - bms: { - "7K": (scd) => ({ - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - "14K": (scd) => ({ - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - }, - ddr: { - SP: (scd) => ({ - MFCP: SumAll(scd, "MFCP"), - ktRating: AvgBest10Map(scd, "ktRating"), - }), - DP: (scd) => ({ - MFCP: SumAll(scd, "MFCP"), - ktRating: AvgBest10Map(scd, "ktRating"), - }), - }, - jubeat: { - Single: (scd) => ({ - jubility: AvgBest10Map(scd, "jubility"), - }), - }, - usc: { - Single: (scd) => { - const VF6 = AvgBest10Map(scd, "VF6"); + return { + VF6, + profileVF6: VF6 === null ? null : VF6 * 50, + }; + }, + }, + popn: { + "9B": () => ({}), + }, + museca: { + Single: (scd) => ({ + ktRating: AvgBest10Map(scd, "ktRating"), + }), + }, + chunithm: { + Single: (scd) => ({ + rating: AvgBest10Map(scd, "rating"), + }), + }, + maimai: { + Single: (scd) => ({ + ktRating: AvgBest10Map(scd, "ktRating"), + }), + }, + gitadora: { + Gita: (scd) => ({ + skill: AvgBest10Map(scd, "skill"), + }), + Dora: (scd) => ({ + skill: AvgBest10Map(scd, "skill"), + }), + }, + bms: { + "7K": (scd) => ({ + ktLampRating: AvgBest10Map(scd, "ktLampRating"), + }), + "14K": (scd) => ({ + ktLampRating: AvgBest10Map(scd, "ktLampRating"), + }), + }, + ddr: { + SP: (scd) => ({ + MFCP: SumAll(scd, "MFCP"), + ktRating: AvgBest10Map(scd, "ktRating"), + }), + DP: (scd) => ({ + MFCP: SumAll(scd, "MFCP"), + ktRating: AvgBest10Map(scd, "ktRating"), + }), + }, + jubeat: { + Single: (scd) => ({ + jubility: AvgBest10Map(scd, "jubility"), + }), + }, + usc: { + Single: (scd) => { + const VF6 = AvgBest10Map(scd, "VF6"); - return { - VF6, - profileVF6: VF6 === null ? null : VF6 * 50, - }; - }, - }, + return { + VF6, + profileVF6: VF6 === null ? null : VF6 * 50, + }; + }, + }, }; export function CreateSessionCalcData( - game: Game, - playtype: Playtypes[Game], - scoreCalcData: ScoreCalculatedDataOnly[] + game: Game, + playtype: Playtypes[Game], + scoreCalcData: ScoreCalculatedDataOnly[] ): SessionDocument["calculatedData"] { - // @ts-expect-error standard game->pt stuff. - return CalculatedDataFunctions[game][playtype](scoreCalcData); + // @ts-expect-error standard game->pt stuff. + return CalculatedDataFunctions[game][playtype](scoreCalcData); } diff --git a/server/src/lib/score-import/framework/sessions/name-generation.test.ts b/server/src/lib/score-import/framework/sessions/name-generation.test.ts index a83f00f61..89e103745 100644 --- a/server/src/lib/score-import/framework/sessions/name-generation.test.ts +++ b/server/src/lib/score-import/framework/sessions/name-generation.test.ts @@ -2,9 +2,9 @@ import t from "tap"; import { GenerateRandomSessionName } from "./name-generation"; t.test("#GenerateRandomSessionName", (t) => { - const res = GenerateRandomSessionName(); + const res = GenerateRandomSessionName(); - t.type(res, "string", "Should return a string."); + t.type(res, "string", "Should return a string."); - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/sessions/name-generation.ts b/server/src/lib/score-import/framework/sessions/name-generation.ts index ab5304b83..e13a67c42 100644 --- a/server/src/lib/score-import/framework/sessions/name-generation.ts +++ b/server/src/lib/score-import/framework/sessions/name-generation.ts @@ -1,9 +1,9 @@ import { GetRandomAdjective, GetRandomNoun } from "../../../../datasets/words"; export function GenerateRandomSessionName() { - const adj1 = GetRandomAdjective(); - const adj2 = GetRandomAdjective(); - const noun = GetRandomNoun(); + const adj1 = GetRandomAdjective(); + const adj2 = GetRandomAdjective(); + const noun = GetRandomNoun(); - return `${adj1} ${adj2} ${noun}`; + return `${adj1} ${adj2} ${noun}`; } diff --git a/server/src/lib/score-import/framework/sessions/sessions.test.ts b/server/src/lib/score-import/framework/sessions/sessions.test.ts index a4c0f386a..aa5ebb2f6 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.test.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.test.ts @@ -9,440 +9,440 @@ import { PublicUserDocument, ScoreDocument, SessionDocument } from "tachi-common import deepmerge from "deepmerge"; const logger = CreateScoreLogger( - { username: "test_zkldi", id: 1 } as PublicUserDocument, - "foo", - "ir/direct-manual" + { username: "test_zkldi", id: 1 } as PublicUserDocument, + "foo", + "ir/direct-manual" ); t.test("#CreateSessions", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => db.sessions.remove({})); + t.beforeEach(ResetDBState); + t.beforeEach(() => db.sessions.remove({})); - t.test("Should compose sessions from one timestamped score provided.", async (t) => { - const res = await CreateSessions( - 1, - "ir/direct-manual", - "iidx", - { SP: [TestingIIDXSPScore] }, - logger - ); + t.test("Should compose sessions from one timestamped score provided.", async (t) => { + const res = await CreateSessions( + 1, + "ir/direct-manual", + "iidx", + { SP: [TestingIIDXSPScore] }, + logger + ); - t.match(res, [ - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - ]); + t.match(res, [ + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + ]); - t.equal(res.length, 1); + t.equal(res.length, 1); - const session = await db.sessions.findOne({ userID: 1, game: "iidx", playtype: "SP" }); + const session = await db.sessions.findOne({ userID: 1, game: "iidx", playtype: "SP" }); - const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); + const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); - t.equal(sessionCount, 1); + t.equal(sessionCount, 1); - t.hasStrict(session, { - userID: 1, - importType: "ir/direct-manual", - // name: "adjective1 adjective2 noun1", - sessionID: res[0].sessionID, - desc: null, - game: "iidx", - playtype: "SP", - highlight: false, - scoreInfo: [ - { - scoreID: "TESTING_SCORE_ID", - isNewScore: true, - }, - ], - // timeInserted: 1622289329729, - timeStarted: 1619454485988, - timeEnded: 1619454485988, - calculatedData: {}, - }); + t.hasStrict(session, { + userID: 1, + importType: "ir/direct-manual", + // name: "adjective1 adjective2 noun1", + sessionID: res[0].sessionID, + desc: null, + game: "iidx", + playtype: "SP", + highlight: false, + scoreInfo: [ + { + scoreID: "TESTING_SCORE_ID", + isNewScore: true, + }, + ], + // timeInserted: 1622289329729, + timeStarted: 1619454485988, + timeEnded: 1619454485988, + calculatedData: {}, + }); - t.end(); - }); + t.end(); + }); - t.test("Should not compose sessions from untimestamped scores.", async (t) => { - const res = await CreateSessions( - 1, - "ir/direct-manual", - "iidx", - { SP: [deepmerge(TestingIIDXSPScore, { timeAchieved: null })] }, - logger - ); + t.test("Should not compose sessions from untimestamped scores.", async (t) => { + const res = await CreateSessions( + 1, + "ir/direct-manual", + "iidx", + { SP: [deepmerge(TestingIIDXSPScore, { timeAchieved: null })] }, + logger + ); - t.strictSame(res, []); + t.strictSame(res, []); - const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); + const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); - t.equal(sessionCount, 0); + t.equal(sessionCount, 0); - t.end(); - }); + t.end(); + }); - t.test("Should compose sessions for multiple playtypes.", async (t) => { - const res = await CreateSessions( - 1, - "ir/direct-manual", - "iidx", - { DP: [deepmerge(TestingIIDXSPScore, { playtype: "DP" })], SP: [TestingIIDXSPScore] }, - logger - ); + t.test("Should compose sessions for multiple playtypes.", async (t) => { + const res = await CreateSessions( + 1, + "ir/direct-manual", + "iidx", + { DP: [deepmerge(TestingIIDXSPScore, { playtype: "DP" })], SP: [TestingIIDXSPScore] }, + logger + ); - t.equal(res.length, 2); + t.equal(res.length, 2); - const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); + const sessionCount = await db.sessions.count({ userID: 1, game: "iidx", playtype: "SP" }); - t.equal(sessionCount, 1); + t.equal(sessionCount, 1); - const sessionCountDP = await db.sessions.count({ userID: 1, game: "iidx", playtype: "DP" }); + const sessionCountDP = await db.sessions.count({ userID: 1, game: "iidx", playtype: "DP" }); - t.equal(sessionCountDP, 1); + t.equal(sessionCountDP, 1); - t.end(); - }); - t.end(); + t.end(); + }); + t.end(); }); // just an arbitrary start point for our sessions as an offset const start = 1619454485988; t.test("#LoadScoresIntoSessions", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => db.sessions.remove({})); + t.beforeEach(ResetDBState); + t.beforeEach(() => db.sessions.remove({})); - t.test("Should create sessions from scores.", async (t) => { - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [null, start, start + 1000, start + 2000, start + 3000].map((e, i) => - deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], - "iidx", - "SP", - logger - ); + t.test("Should create sessions from scores.", async (t) => { + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [null, start, start + 1000, start + 2000, start + 3000].map((e, i) => + deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) + ) as ScoreDocument[], + "iidx", + "SP", + logger + ); - t.match(res, [ - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - ]); + t.match(res, [ + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 1); + t.equal(sessions.length, 1); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "SCORE_ID_1", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_2", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_3", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_4", - isNewScore: true, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "SCORE_ID_1", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_2", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_3", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_4", + isNewScore: true, + }, + ]); - t.end(); - }); + t.end(); + }); - t.test("Should sort scores before creating sessions.", async (t) => { - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [null, start, start - 4000, start + 2000, start - 3000, null, start + 1000].map( - (e, i) => - deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], - "iidx", - "SP", - logger - ); + t.test("Should sort scores before creating sessions.", async (t) => { + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [null, start, start - 4000, start + 2000, start - 3000, null, start + 1000].map( + (e, i) => + deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) + ) as ScoreDocument[], + "iidx", + "SP", + logger + ); - t.match(res, [ - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - ]); + t.match(res, [ + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 1); + t.equal(sessions.length, 1); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "SCORE_ID_2", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_4", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_1", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_6", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_3", - isNewScore: true, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "SCORE_ID_2", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_4", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_1", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_6", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_3", + isNewScore: true, + }, + ]); - t.end(); - }); + t.end(); + }); - const TWO_HOURS = 1000 * 60 * 60 * 2; + const TWO_HOURS = 1000 * 60 * 60 * 2; - t.test("Should create multiple sessions if scores are far apart.", async (t) => { - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [ - null, - start, - start + 1000, - start + 2000, - start + TWO_HOURS + 4000, - start + TWO_HOURS + 5000, - start + TWO_HOURS + 6000, - ].map((e, i) => - deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], - "iidx", - "SP", - logger - ); + t.test("Should create multiple sessions if scores are far apart.", async (t) => { + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [ + null, + start, + start + 1000, + start + 2000, + start + TWO_HOURS + 4000, + start + TWO_HOURS + 5000, + start + TWO_HOURS + 6000, + ].map((e, i) => + deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) + ) as ScoreDocument[], + "iidx", + "SP", + logger + ); - t.match(res, [ - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - ]); + t.match(res, [ + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 2); + t.equal(sessions.length, 2); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "SCORE_ID_1", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_2", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_3", - isNewScore: true, - }, - ]); - t.strictSame(sessions[1].scoreInfo, [ - { - scoreID: "SCORE_ID_4", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_5", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_6", - isNewScore: true, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "SCORE_ID_1", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_2", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_3", + isNewScore: true, + }, + ]); + t.strictSame(sessions[1].scoreInfo, [ + { + scoreID: "SCORE_ID_4", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_5", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_6", + isNewScore: true, + }, + ]); - t.end(); - }); + t.end(); + }); - t.test("Should append to existing sessions.", async (t) => { - await db.sessions.insert({ - sessionID: "EXAMPLE_SESSION_ID", - userID: 1, - game: "iidx", - playtype: "SP", - importType: "ir/direct-manual", - timeStarted: start, - timeEnded: start, - scoreInfo: [ - { - scoreID: "EXAMPLE_SCORE_ID", - isNewScore: true, - }, - ], - } as SessionDocument); + t.test("Should append to existing sessions.", async (t) => { + await db.sessions.insert({ + sessionID: "EXAMPLE_SESSION_ID", + userID: 1, + game: "iidx", + playtype: "SP", + importType: "ir/direct-manual", + timeStarted: start, + timeEnded: start, + scoreInfo: [ + { + scoreID: "EXAMPLE_SCORE_ID", + isNewScore: true, + }, + ], + } as SessionDocument); - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [null, start, start + 1000, start + 2000].map((e, i) => - deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], - "iidx", - "SP", - logger - ); + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [null, start, start + 1000, start + 2000].map((e, i) => + deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) + ) as ScoreDocument[], + "iidx", + "SP", + logger + ); - t.strictSame(res, [ - { - sessionID: "EXAMPLE_SESSION_ID", - type: "Appended", - }, - ]); + t.strictSame(res, [ + { + sessionID: "EXAMPLE_SESSION_ID", + type: "Appended", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 1); + t.equal(sessions.length, 1); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "EXAMPLE_SCORE_ID", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_1", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_2", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_3", - isNewScore: true, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "EXAMPLE_SCORE_ID", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_1", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_2", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_3", + isNewScore: true, + }, + ]); - t.equal(sessions[0].timeEnded, start + 2000); + t.equal(sessions[0].timeEnded, start + 2000); - t.end(); - }); + t.end(); + }); - t.test("Should change the timeStarted of existing sessions if prepending.", async (t) => { - await db.sessions.insert({ - sessionID: "EXAMPLE_SESSION_ID", - userID: 1, - game: "iidx", - playtype: "SP", - importType: "ir/direct-manual", - timeStarted: start, - timeEnded: start, - scoreInfo: [ - { - scoreID: "EXAMPLE_SCORE_ID", - isNewScore: true, - }, - ], - } as SessionDocument); + t.test("Should change the timeStarted of existing sessions if prepending.", async (t) => { + await db.sessions.insert({ + sessionID: "EXAMPLE_SESSION_ID", + userID: 1, + game: "iidx", + playtype: "SP", + importType: "ir/direct-manual", + timeStarted: start, + timeEnded: start, + scoreInfo: [ + { + scoreID: "EXAMPLE_SCORE_ID", + isNewScore: true, + }, + ], + } as SessionDocument); - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [null, start, start - 1000, start - 2000].map((e, i) => - deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], - "iidx", - "SP", - logger - ); + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [null, start, start - 1000, start - 2000].map((e, i) => + deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) + ) as ScoreDocument[], + "iidx", + "SP", + logger + ); - t.strictSame(res, [ - { - sessionID: "EXAMPLE_SESSION_ID", - type: "Appended", - }, - ]); + t.strictSame(res, [ + { + sessionID: "EXAMPLE_SESSION_ID", + type: "Appended", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 1); + t.equal(sessions.length, 1); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "EXAMPLE_SCORE_ID", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_3", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_2", - isNewScore: true, - }, - { - scoreID: "SCORE_ID_1", - isNewScore: true, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "EXAMPLE_SCORE_ID", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_3", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_2", + isNewScore: true, + }, + { + scoreID: "SCORE_ID_1", + isNewScore: true, + }, + ]); - t.equal(sessions[0].timeStarted, start - 2000); + t.equal(sessions[0].timeStarted, start - 2000); - t.end(); - }); + t.end(); + }); - t.test("Should calculate pbDifferences if a scorePB exists.", async (t) => { - await db["personal-bests"].insert(TestingIIDXSPScorePB); + t.test("Should calculate pbDifferences if a scorePB exists.", async (t) => { + await db["personal-bests"].insert(TestingIIDXSPScorePB); - const res = await LoadScoresIntoSessions( - 1, - "ir/direct-manual", - [TestingIIDXSPScore], - "iidx", - "SP", - logger - ); + const res = await LoadScoresIntoSessions( + 1, + "ir/direct-manual", + [TestingIIDXSPScore], + "iidx", + "SP", + logger + ); - t.match(res, [ - { - sessionID: /^Q[a-f0-9]{40}$/u, - type: "Created", - }, - ]); + t.match(res, [ + { + sessionID: /^Q[a-f0-9]{40}$/u, + type: "Created", + }, + ]); - const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); + const sessions = await db.sessions.find({ game: "iidx", playtype: "SP", userID: 1 }); - t.equal(sessions.length, 1); + t.equal(sessions.length, 1); - t.strictSame(sessions[0].scoreInfo, [ - { - scoreID: "TESTING_SCORE_ID", - isNewScore: false, - gradeDelta: 4, - lampDelta: -2, - percentDelta: -44.08396946564885, - scoreDelta: -693, - }, - ]); + t.strictSame(sessions[0].scoreInfo, [ + { + scoreID: "TESTING_SCORE_ID", + isNewScore: false, + gradeDelta: 4, + lampDelta: -2, + percentDelta: -44.08396946564885, + scoreDelta: -693, + }, + ]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/sessions/sessions.ts b/server/src/lib/score-import/framework/sessions/sessions.ts index bca1757c8..12a58df38 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.ts @@ -1,13 +1,13 @@ import { - integer, - ImportTypes, - ScoreDocument, - SessionDocument, - Playtypes, - PBScoreDocument, - SessionScoreInfo, - SessionInfoReturn, - Game, + integer, + ImportTypes, + ScoreDocument, + SessionDocument, + Playtypes, + PBScoreDocument, + SessionScoreInfo, + SessionInfoReturn, + Game, } from "tachi-common"; import db from "../../../../external/mongo/db"; import { AppendLogCtx, KtLogger } from "../../../logger/logger"; @@ -21,33 +21,33 @@ import { ScorePlaytypeMap } from "../common/types"; const TWO_HOURS = 1000 * 60 * 60 * 2; export async function CreateSessions( - userID: integer, - importType: ImportTypes, - game: Game, - scorePtMap: ScorePlaytypeMap, - logger: KtLogger + userID: integer, + importType: ImportTypes, + game: Game, + scorePtMap: ScorePlaytypeMap, + logger: KtLogger ) { - const allSessionInfo = []; + const allSessionInfo = []; - /* eslint-disable no-await-in-loop */ - for (const playtype in scorePtMap) { - // @ts-expect-error This is my least favourite thing about ts. - const scores = scorePtMap[playtype] as ScoreDocument[]; + /* eslint-disable no-await-in-loop */ + for (const playtype in scorePtMap) { + // @ts-expect-error This is my least favourite thing about ts. + const scores = scorePtMap[playtype] as ScoreDocument[]; - const sessionInfo = await LoadScoresIntoSessions( - userID, - importType, - scores, - game, - playtype as Playtypes[Game], - logger - ); + const sessionInfo = await LoadScoresIntoSessions( + userID, + importType, + scores, + game, + playtype as Playtypes[Game], + logger + ); - allSessionInfo.push(...sessionInfo); - } - /* eslint-enable no-await-in-loop */ + allSessionInfo.push(...sessionInfo); + } + /* eslint-enable no-await-in-loop */ - return allSessionInfo; + return allSessionInfo; } /** @@ -55,228 +55,228 @@ export async function CreateSessions( * as a SessionScoreInfo object. */ function ProcessScoreIntoSessionScoreInfo( - score: ScoreDocument, - previousPB: PBScoreDocument | undefined + score: ScoreDocument, + previousPB: PBScoreDocument | undefined ): SessionScoreInfo { - if (!previousPB) { - return { - scoreID: score.scoreID, - isNewScore: true, - }; - } + if (!previousPB) { + return { + scoreID: score.scoreID, + isNewScore: true, + }; + } - return { - scoreID: score.scoreID, - isNewScore: false, - gradeDelta: score.scoreData.gradeIndex - previousPB.scoreData.gradeIndex, - lampDelta: score.scoreData.lampIndex - previousPB.scoreData.lampIndex, - percentDelta: score.scoreData.percent - previousPB.scoreData.percent, - scoreDelta: score.scoreData.score - previousPB.scoreData.score, - }; + return { + scoreID: score.scoreID, + isNewScore: false, + gradeDelta: score.scoreData.gradeIndex - previousPB.scoreData.gradeIndex, + lampDelta: score.scoreData.lampIndex - previousPB.scoreData.lampIndex, + percentDelta: score.scoreData.percent - previousPB.scoreData.percent, + scoreDelta: score.scoreData.score - previousPB.scoreData.score, + }; } function CreateSessionID() { - return `Q${crypto.randomBytes(20).toString("hex")}`; + return `Q${crypto.randomBytes(20).toString("hex")}`; } function UpdateExistingSession( - existingSession: SessionDocument, - newInfo: SessionScoreInfo[], - oldScores: ScoreDocument[], - newScores: ScoreDocument[] + existingSession: SessionDocument, + newInfo: SessionScoreInfo[], + oldScores: ScoreDocument[], + newScores: ScoreDocument[] ) { - const allScores = [...oldScores, ...newScores]; + const allScores = [...oldScores, ...newScores]; - const calculatedData = CreateSessionCalcData( - existingSession.game, - existingSession.playtype, - allScores - ); + const calculatedData = CreateSessionCalcData( + existingSession.game, + existingSession.playtype, + allScores + ); - existingSession.calculatedData = calculatedData; - existingSession.scoreInfo = [...existingSession.scoreInfo, ...newInfo]; + existingSession.calculatedData = calculatedData; + existingSession.scoreInfo = [...existingSession.scoreInfo, ...newInfo]; - if (newScores[0].timeAchieved! < existingSession.timeStarted) { - existingSession.timeStarted = newScores[0].timeAchieved!; - } + if (newScores[0].timeAchieved! < existingSession.timeStarted) { + existingSession.timeStarted = newScores[0].timeAchieved!; + } - if (newScores[newScores.length - 1].timeAchieved! > existingSession.timeEnded) { - existingSession.timeEnded = newScores[newScores.length - 1].timeAchieved!; - } + if (newScores[newScores.length - 1].timeAchieved! > existingSession.timeEnded) { + existingSession.timeEnded = newScores[newScores.length - 1].timeAchieved!; + } - return existingSession; + return existingSession; } function CreateSession( - userID: integer, - importType: ImportTypes, - groupInfo: SessionScoreInfo[], - groupScores: ScoreDocument[], - game: Game, - playtype: Playtypes[Game] + userID: integer, + importType: ImportTypes, + groupInfo: SessionScoreInfo[], + groupScores: ScoreDocument[], + game: Game, + playtype: Playtypes[Game] ): SessionDocument { - const name = GenerateRandomSessionName(); + const name = GenerateRandomSessionName(); - const calculatedData = CreateSessionCalcData(game, playtype, groupScores); + const calculatedData = CreateSessionCalcData(game, playtype, groupScores); - return { - userID, - importType, - name, - sessionID: CreateSessionID(), - desc: null, - game, - playtype, - highlight: false, - scoreInfo: groupInfo, - timeInserted: Date.now(), - timeStarted: groupScores[0].timeAchieved!, - timeEnded: groupScores[groupScores.length - 1].timeAchieved!, - calculatedData, - }; + return { + userID, + importType, + name, + sessionID: CreateSessionID(), + desc: null, + game, + playtype, + highlight: false, + scoreInfo: groupInfo, + timeInserted: Date.now(), + timeStarted: groupScores[0].timeAchieved!, + timeEnded: groupScores[groupScores.length - 1].timeAchieved!, + calculatedData, + }; } export async function LoadScoresIntoSessions( - userID: integer, - importType: ImportTypes, - importScores: ScoreDocument[], - game: Game, - playtype: Playtypes[Game], - baseLogger: KtLogger + userID: integer, + importType: ImportTypes, + importScores: ScoreDocument[], + game: Game, + playtype: Playtypes[Game], + baseLogger: KtLogger ): Promise { - const logger = AppendLogCtx("Session Generation", baseLogger); + const logger = AppendLogCtx("Session Generation", baseLogger); - const timestampedScores = []; + const timestampedScores = []; - for (const score of importScores) { - if (!score.timeAchieved) { - logger.verbose(`Ignored score ${score.scoreID}, as it had no timeAchieved.`); - // ignore scores without timestamps. We can't use these for sessions. - continue; - } + for (const score of importScores) { + if (!score.timeAchieved) { + logger.verbose(`Ignored score ${score.scoreID}, as it had no timeAchieved.`); + // ignore scores without timestamps. We can't use these for sessions. + continue; + } - timestampedScores.push(score); - } + timestampedScores.push(score); + } - // If we have nothing to work with, why bother? - if (timestampedScores.length === 0) { - logger.verbose(`Skipped calculating sessions as there were no timestamped scores`); - return []; - } + // If we have nothing to work with, why bother? + if (timestampedScores.length === 0) { + logger.verbose(`Skipped calculating sessions as there were no timestamped scores`); + return []; + } - // sort scores ascendingly. - timestampedScores.sort((a, b) => a.timeAchieved! - b.timeAchieved!); + // sort scores ascendingly. + timestampedScores.sort((a, b) => a.timeAchieved! - b.timeAchieved!); - // The "Score Groups" for the array of scores provided. - // This contains scores split on 2hr margins, which allows for more optimised - // session db requests. - const sessionScoreGroups: ScoreDocument[][] = []; - let curGroup: ScoreDocument[] = []; - let lastTimestamp = 0; + // The "Score Groups" for the array of scores provided. + // This contains scores split on 2hr margins, which allows for more optimised + // session db requests. + const sessionScoreGroups: ScoreDocument[][] = []; + let curGroup: ScoreDocument[] = []; + let lastTimestamp = 0; - for (const score of timestampedScores) { - if (score.timeAchieved! < lastTimestamp + TWO_HOURS) { - curGroup.push(score); - } else { - sessionScoreGroups.push(curGroup); - curGroup = [score]; - } - lastTimestamp = score.timeAchieved!; - } + for (const score of timestampedScores) { + if (score.timeAchieved! < lastTimestamp + TWO_HOURS) { + curGroup.push(score); + } else { + sessionScoreGroups.push(curGroup); + curGroup = [score]; + } + lastTimestamp = score.timeAchieved!; + } - // There's no state here where curGroup is empty, - // so push the group (which is guaranteed to have atleast one score) - sessionScoreGroups.push(curGroup); + // There's no state here where curGroup is empty, + // so push the group (which is guaranteed to have atleast one score) + sessionScoreGroups.push(curGroup); - logger.verbose(`Created ${sessionScoreGroups.length} groups from timestamped scores.`); + logger.verbose(`Created ${sessionScoreGroups.length} groups from timestamped scores.`); - const sessionInfoReturns: SessionInfoReturn[] = []; + const sessionInfoReturns: SessionInfoReturn[] = []; - // All async operations inside here *need* to be done in lockstep to avoid colliding sessions. - // realistically, that shouldn't be possible, but hey. - /* eslint-disable no-await-in-loop */ - for (const groupScores of sessionScoreGroups) { - if (groupScores.length === 0) { - continue; - } + // All async operations inside here *need* to be done in lockstep to avoid colliding sessions. + // realistically, that shouldn't be possible, but hey. + /* eslint-disable no-await-in-loop */ + for (const groupScores of sessionScoreGroups) { + if (groupScores.length === 0) { + continue; + } - const startOfGroup = groupScores[0].timeAchieved!; - const endOfGroup = groupScores[groupScores.length - 1].timeAchieved!; + const startOfGroup = groupScores[0].timeAchieved!; + const endOfGroup = groupScores[groupScores.length - 1].timeAchieved!; - // A bug exists here where if a session is created - // backwards in time, this will grab your PBs - // from the future. - // @todo #179 - const pbs = await db["personal-bests"].find({ - chartID: { $in: groupScores.map((e) => e.chartID) }, - userID, - }); + // A bug exists here where if a session is created + // backwards in time, this will grab your PBs + // from the future. + // @todo #179 + const pbs = await db["personal-bests"].find({ + chartID: { $in: groupScores.map((e) => e.chartID) }, + userID, + }); - const pbMap: Map = new Map(); - for (const pb of pbs) { - pbMap.set(pb.chartID, pb); - } + const pbMap: Map = new Map(); + for (const pb of pbs) { + pbMap.set(pb.chartID, pb); + } - const groupInfo = groupScores.map((e) => - ProcessScoreIntoSessionScoreInfo(e, pbMap.get(e.chartID)) - ); + const groupInfo = groupScores.map((e) => + ProcessScoreIntoSessionScoreInfo(e, pbMap.get(e.chartID)) + ); - // Find any sessions with +/-2hrs of this group. This is rather exhaustive, and could result in some issues - // if this query returns more than one session. We should account for that by smushing sessions together. - // As of now, we dont currently do it. @TODO #148. - const nearbySession = await db.sessions.findOne({ - userID, - game, - playtype, - importType, - $or: [ - { timeStarted: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } }, - { timeEnded: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } }, - ], - }); + // Find any sessions with +/-2hrs of this group. This is rather exhaustive, and could result in some issues + // if this query returns more than one session. We should account for that by smushing sessions together. + // As of now, we dont currently do it. @TODO #148. + const nearbySession = await db.sessions.findOne({ + userID, + game, + playtype, + importType, + $or: [ + { timeStarted: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } }, + { timeEnded: { $gte: startOfGroup - TWO_HOURS, $lt: endOfGroup + TWO_HOURS } }, + ], + }); - let infoReturn: SessionInfoReturn; + let infoReturn: SessionInfoReturn; - if (nearbySession) { - logger.verbose( - `Found nearby session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.` - ); + if (nearbySession) { + logger.verbose( + `Found nearby session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.` + ); - const oldScores = await GetScoresFromSession(nearbySession); + const oldScores = await GetScoresFromSession(nearbySession); - const session = UpdateExistingSession(nearbySession, groupInfo, oldScores, groupScores); + const session = UpdateExistingSession(nearbySession, groupInfo, oldScores, groupScores); - infoReturn = { sessionID: session.sessionID, type: "Appended" }; + infoReturn = { sessionID: session.sessionID, type: "Appended" }; - await db.sessions.update( - { - sessionID: session.sessionID, - }, - { - $set: session, - } - ); - } else { - logger.debug( - `Creating new session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.` - ); + await db.sessions.update( + { + sessionID: session.sessionID, + }, + { + $set: session, + } + ); + } else { + logger.debug( + `Creating new session for ${userID} (${game} ${playtype}) around ${startOfGroup} ${endOfGroup}.` + ); - const session = CreateSession( - userID, - importType, - groupInfo, - groupScores, - game, - playtype - ); + const session = CreateSession( + userID, + importType, + groupInfo, + groupScores, + game, + playtype + ); - infoReturn = { sessionID: session.sessionID, type: "Created" }; - await db.sessions.insert(session); - } + infoReturn = { sessionID: session.sessionID, type: "Created" }; + await db.sessions.insert(session); + } - sessionInfoReturns.push(infoReturn); - } - /* eslint-enable no-await-in-loop */ + sessionInfoReturns.push(infoReturn); + } + /* eslint-enable no-await-in-loop */ - return sessionInfoReturns; + return sessionInfoReturns; } diff --git a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts index c771479da..483ef5466 100644 --- a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts @@ -2,106 +2,106 @@ import t from "tap"; import { GitadoraColours, SDVXVFClasses } from "../../../constants/classes"; import CreateLogCtx from "../../../logger/logger"; import { - CalculateGitadoraColour, - GitadoraSkillToColour, - CalculateSDVXClass, - SDVXVF6ToClass, + CalculateGitadoraColour, + GitadoraSkillToColour, + CalculateSDVXClass, + SDVXVF6ToClass, } from "./builtin-class-handlers"; const logger = CreateLogCtx(__filename); t.test("#CalculateGitadoraColour", (t) => { - t.strictSame( - CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }, logger), - { - colour: GitadoraColours.ORANGE_GRADIENT, - }, - "Should wrap the skill colour." - ); + t.strictSame( + CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }, logger), + { + colour: GitadoraColours.ORANGE_GRADIENT, + }, + "Should wrap the skill colour." + ); - t.end(); + t.end(); }); t.test("#GitadoraSkillToColour", (t) => { - t.equal(GitadoraSkillToColour(0), GitadoraColours.WHITE); - t.equal(GitadoraSkillToColour(999), GitadoraColours.WHITE); - t.equal(GitadoraSkillToColour(1000), GitadoraColours.ORANGE); - t.equal(GitadoraSkillToColour(1500), GitadoraColours.ORANGE_GRADIENT); - t.equal(GitadoraSkillToColour(2000), GitadoraColours.YELLOW); - t.equal(GitadoraSkillToColour(2500), GitadoraColours.YELLOW_GRADIENT); - t.equal(GitadoraSkillToColour(3000), GitadoraColours.GREEN); - t.equal(GitadoraSkillToColour(3500), GitadoraColours.GREEN_GRADIENT); - t.equal(GitadoraSkillToColour(4000), GitadoraColours.BLUE); - t.equal(GitadoraSkillToColour(4500), GitadoraColours.BLUE_GRADIENT); - t.equal(GitadoraSkillToColour(5000), GitadoraColours.PURPLE); - t.equal(GitadoraSkillToColour(5500), GitadoraColours.PURPLE_GRADIENT); - t.equal(GitadoraSkillToColour(6000), GitadoraColours.RED); - t.equal(GitadoraSkillToColour(6500), GitadoraColours.RED_GRADIENT); - t.equal(GitadoraSkillToColour(7000), GitadoraColours.BRONZE); - t.equal(GitadoraSkillToColour(7500), GitadoraColours.SILVER); - t.equal(GitadoraSkillToColour(8000), GitadoraColours.GOLD); - t.equal(GitadoraSkillToColour(8500), GitadoraColours.RAINBOW); + t.equal(GitadoraSkillToColour(0), GitadoraColours.WHITE); + t.equal(GitadoraSkillToColour(999), GitadoraColours.WHITE); + t.equal(GitadoraSkillToColour(1000), GitadoraColours.ORANGE); + t.equal(GitadoraSkillToColour(1500), GitadoraColours.ORANGE_GRADIENT); + t.equal(GitadoraSkillToColour(2000), GitadoraColours.YELLOW); + t.equal(GitadoraSkillToColour(2500), GitadoraColours.YELLOW_GRADIENT); + t.equal(GitadoraSkillToColour(3000), GitadoraColours.GREEN); + t.equal(GitadoraSkillToColour(3500), GitadoraColours.GREEN_GRADIENT); + t.equal(GitadoraSkillToColour(4000), GitadoraColours.BLUE); + t.equal(GitadoraSkillToColour(4500), GitadoraColours.BLUE_GRADIENT); + t.equal(GitadoraSkillToColour(5000), GitadoraColours.PURPLE); + t.equal(GitadoraSkillToColour(5500), GitadoraColours.PURPLE_GRADIENT); + t.equal(GitadoraSkillToColour(6000), GitadoraColours.RED); + t.equal(GitadoraSkillToColour(6500), GitadoraColours.RED_GRADIENT); + t.equal(GitadoraSkillToColour(7000), GitadoraColours.BRONZE); + t.equal(GitadoraSkillToColour(7500), GitadoraColours.SILVER); + t.equal(GitadoraSkillToColour(8000), GitadoraColours.GOLD); + t.equal(GitadoraSkillToColour(8500), GitadoraColours.RAINBOW); - t.end(); + t.end(); }); t.test("#CalculateSDVXClass", (t) => { - t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), { - vfClass: SDVXVFClasses.COBALT_I, - }); + t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), { + vfClass: SDVXVFClasses.COBALT_I, + }); - t.end(); + t.end(); }); t.test("#SDVXVF6ToClass", (t) => { - function f(vf: number) { - return SDVXVF6ToClass(vf, logger); - } + function f(vf: number) { + return SDVXVF6ToClass(vf, logger); + } - t.equal(f(0), SDVXVFClasses.SIENNA_I); - t.equal(f(2.5), SDVXVFClasses.SIENNA_II); - t.equal(f(5), SDVXVFClasses.SIENNA_III); - t.equal(f(7.5), SDVXVFClasses.SIENNA_IV); - t.equal(f(10), SDVXVFClasses.COBALT_I); - t.equal(f(10.5), SDVXVFClasses.COBALT_II); - t.equal(f(11), SDVXVFClasses.COBALT_III); - t.equal(f(11.5), SDVXVFClasses.COBALT_IV); - t.equal(f(12), SDVXVFClasses.DANDELION_I); - t.equal(f(12.5), SDVXVFClasses.DANDELION_II); - t.equal(f(13), SDVXVFClasses.DANDELION_III); - t.equal(f(13.5), SDVXVFClasses.DANDELION_IV); + t.equal(f(0), SDVXVFClasses.SIENNA_I); + t.equal(f(2.5), SDVXVFClasses.SIENNA_II); + t.equal(f(5), SDVXVFClasses.SIENNA_III); + t.equal(f(7.5), SDVXVFClasses.SIENNA_IV); + t.equal(f(10), SDVXVFClasses.COBALT_I); + t.equal(f(10.5), SDVXVFClasses.COBALT_II); + t.equal(f(11), SDVXVFClasses.COBALT_III); + t.equal(f(11.5), SDVXVFClasses.COBALT_IV); + t.equal(f(12), SDVXVFClasses.DANDELION_I); + t.equal(f(12.5), SDVXVFClasses.DANDELION_II); + t.equal(f(13), SDVXVFClasses.DANDELION_III); + t.equal(f(13.5), SDVXVFClasses.DANDELION_IV); - t.equal(f(14), SDVXVFClasses.CYAN_I); - t.equal(f(14.25), SDVXVFClasses.CYAN_II); - t.equal(f(14.5), SDVXVFClasses.CYAN_III); - t.equal(f(14.75), SDVXVFClasses.CYAN_IV); - t.equal(f(15), SDVXVFClasses.SCARLET_I); - t.equal(f(15.25), SDVXVFClasses.SCARLET_II); - t.equal(f(15.5), SDVXVFClasses.SCARLET_III); - t.equal(f(15.75), SDVXVFClasses.SCARLET_IV); - t.equal(f(16), SDVXVFClasses.CORAL_I); - t.equal(f(16.25), SDVXVFClasses.CORAL_II); - t.equal(f(16.5), SDVXVFClasses.CORAL_III); - t.equal(f(16.75), SDVXVFClasses.CORAL_IV); - t.equal(f(17), SDVXVFClasses.ARGENTO_I); - t.equal(f(17.25), SDVXVFClasses.ARGENTO_II); - t.equal(f(17.5), SDVXVFClasses.ARGENTO_III); - t.equal(f(17.75), SDVXVFClasses.ARGENTO_IV); - t.equal(f(18), SDVXVFClasses.ELDORA_I); - t.equal(f(18.25), SDVXVFClasses.ELDORA_II); - t.equal(f(18.5), SDVXVFClasses.ELDORA_III); - t.equal(f(18.75), SDVXVFClasses.ELDORA_IV); - t.equal(f(19), SDVXVFClasses.CRIMSON_I); - t.equal(f(19.25), SDVXVFClasses.CRIMSON_II); - t.equal(f(19.5), SDVXVFClasses.CRIMSON_III); - t.equal(f(19.75), SDVXVFClasses.CRIMSON_IV); + t.equal(f(14), SDVXVFClasses.CYAN_I); + t.equal(f(14.25), SDVXVFClasses.CYAN_II); + t.equal(f(14.5), SDVXVFClasses.CYAN_III); + t.equal(f(14.75), SDVXVFClasses.CYAN_IV); + t.equal(f(15), SDVXVFClasses.SCARLET_I); + t.equal(f(15.25), SDVXVFClasses.SCARLET_II); + t.equal(f(15.5), SDVXVFClasses.SCARLET_III); + t.equal(f(15.75), SDVXVFClasses.SCARLET_IV); + t.equal(f(16), SDVXVFClasses.CORAL_I); + t.equal(f(16.25), SDVXVFClasses.CORAL_II); + t.equal(f(16.5), SDVXVFClasses.CORAL_III); + t.equal(f(16.75), SDVXVFClasses.CORAL_IV); + t.equal(f(17), SDVXVFClasses.ARGENTO_I); + t.equal(f(17.25), SDVXVFClasses.ARGENTO_II); + t.equal(f(17.5), SDVXVFClasses.ARGENTO_III); + t.equal(f(17.75), SDVXVFClasses.ARGENTO_IV); + t.equal(f(18), SDVXVFClasses.ELDORA_I); + t.equal(f(18.25), SDVXVFClasses.ELDORA_II); + t.equal(f(18.5), SDVXVFClasses.ELDORA_III); + t.equal(f(18.75), SDVXVFClasses.ELDORA_IV); + t.equal(f(19), SDVXVFClasses.CRIMSON_I); + t.equal(f(19.25), SDVXVFClasses.CRIMSON_II); + t.equal(f(19.5), SDVXVFClasses.CRIMSON_III); + t.equal(f(19.75), SDVXVFClasses.CRIMSON_IV); - t.equal(f(20), SDVXVFClasses.IMPERIAL_I); - t.equal(f(21), SDVXVFClasses.IMPERIAL_II); - t.equal(f(22), SDVXVFClasses.IMPERIAL_III); - t.equal(f(23), SDVXVFClasses.IMPERIAL_IV); - t.equal(f(24), SDVXVFClasses.IMPERIAL_IV); - t.equal(f(1000), SDVXVFClasses.IMPERIAL_IV); + t.equal(f(20), SDVXVFClasses.IMPERIAL_I); + t.equal(f(21), SDVXVFClasses.IMPERIAL_II); + t.equal(f(22), SDVXVFClasses.IMPERIAL_III); + t.equal(f(23), SDVXVFClasses.IMPERIAL_IV); + t.equal(f(24), SDVXVFClasses.IMPERIAL_IV); + t.equal(f(1000), SDVXVFClasses.IMPERIAL_IV); - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts index 844b7a01e..d99e469df 100644 --- a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts +++ b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts @@ -4,17 +4,17 @@ import { GitadoraColours, SDVXVFClasses } from "../../../constants/classes"; import { KtLogger } from "../../../logger/logger"; export function CalculateSDVXClass( - game: Game, - playtype: Playtypes[Game], - userID: integer, - ratings: Partial>, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + ratings: Partial>, + logger: KtLogger ): Partial> { - if (!ratings.VF6) { - return {}; - } + if (!ratings.VF6) { + return {}; + } - return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) }; + return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) }; } /** @@ -24,83 +24,83 @@ export function CalculateSDVXClass( * @see https://bemaniwiki.com/index.php?SOUND%20VOLTEX%20VIVID%20WAVE/VOLFORCE */ export function SDVXVF6ToClass(vf: number, logger: KtLogger) { - // This is impossible, but a failsafe regardless - if (vf >= 24) { - logger.warn(`User has excessive VF5 of ${vf}. Defaulting to Imperial IV.`); - return SDVXVFClasses.IMPERIAL_IV; - } else if (vf >= 20) { - // imperial i -> iv has gaps of 1 - return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20); - } else if (vf >= 14) { - // cyan i -> crimson iv has gaps of 0.25 - return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14)); - } else if (vf >= 10) { - // cobalt i -> dandelion iv have gaps of 0.5 - return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10)); - } + // This is impossible, but a failsafe regardless + if (vf >= 24) { + logger.warn(`User has excessive VF5 of ${vf}. Defaulting to Imperial IV.`); + return SDVXVFClasses.IMPERIAL_IV; + } else if (vf >= 20) { + // imperial i -> iv has gaps of 1 + return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20); + } else if (vf >= 14) { + // cyan i -> crimson iv has gaps of 0.25 + return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14)); + } else if (vf >= 10) { + // cobalt i -> dandelion iv have gaps of 0.5 + return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10)); + } - return Math.floor(vf / 2.5); + return Math.floor(vf / 2.5); } export function CalculateGitadoraColour( - game: Game, - playtype: Playtypes[Game], - userID: integer, - ratings: Record, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + ratings: Record, + logger: KtLogger ): Partial> { - const colour = GitadoraSkillToColour(ratings.skill); + const colour = GitadoraSkillToColour(ratings.skill); - return { - colour, - }; + return { + colour, + }; } export function GitadoraSkillToColour(sk: number) { - if (sk >= 8500) { - return GitadoraColours.RAINBOW; - } else if (sk >= 8000) { - return GitadoraColours.GOLD; - } else if (sk >= 7500) { - return GitadoraColours.SILVER; - } else if (sk >= 7000) { - return GitadoraColours.BRONZE; - } else if (sk >= 6500) { - return GitadoraColours.RED_GRADIENT; - } else if (sk >= 6000) { - return GitadoraColours.RED; - } else if (sk >= 5500) { - return GitadoraColours.PURPLE_GRADIENT; - } else if (sk >= 5000) { - return GitadoraColours.PURPLE; - } else if (sk >= 4500) { - return GitadoraColours.BLUE_GRADIENT; - } else if (sk >= 4000) { - return GitadoraColours.BLUE; - } else if (sk >= 3500) { - return GitadoraColours.GREEN_GRADIENT; - } else if (sk >= 3000) { - return GitadoraColours.GREEN; - } else if (sk >= 2500) { - return GitadoraColours.YELLOW_GRADIENT; - } else if (sk >= 2000) { - return GitadoraColours.YELLOW; - } else if (sk >= 1500) { - return GitadoraColours.ORANGE_GRADIENT; - } else if (sk >= 1000) { - return GitadoraColours.ORANGE; - } + if (sk >= 8500) { + return GitadoraColours.RAINBOW; + } else if (sk >= 8000) { + return GitadoraColours.GOLD; + } else if (sk >= 7500) { + return GitadoraColours.SILVER; + } else if (sk >= 7000) { + return GitadoraColours.BRONZE; + } else if (sk >= 6500) { + return GitadoraColours.RED_GRADIENT; + } else if (sk >= 6000) { + return GitadoraColours.RED; + } else if (sk >= 5500) { + return GitadoraColours.PURPLE_GRADIENT; + } else if (sk >= 5000) { + return GitadoraColours.PURPLE; + } else if (sk >= 4500) { + return GitadoraColours.BLUE_GRADIENT; + } else if (sk >= 4000) { + return GitadoraColours.BLUE; + } else if (sk >= 3500) { + return GitadoraColours.GREEN_GRADIENT; + } else if (sk >= 3000) { + return GitadoraColours.GREEN; + } else if (sk >= 2500) { + return GitadoraColours.YELLOW_GRADIENT; + } else if (sk >= 2000) { + return GitadoraColours.YELLOW; + } else if (sk >= 1500) { + return GitadoraColours.ORANGE_GRADIENT; + } else if (sk >= 1000) { + return GitadoraColours.ORANGE; + } - return GitadoraColours.WHITE; + return GitadoraColours.WHITE; } export function CalculateJubeatColour( - game: Game, - playtype: Playtypes[Game], - userID: integer, - ratings: Record + game: Game, + playtype: Playtypes[Game], + userID: integer, + ratings: Record ): GameClasses<"jubeat:Single"> { - throw new Error("Not implemented."); + throw new Error("Not implemented."); } // function JubilityToColour(jb: number) {} diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts index ef2adb87b..a08fdfd58 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts @@ -9,131 +9,131 @@ import { GitadoraColours } from "../../../constants/classes"; const logger = CreateLogCtx(__filename); t.test("#UpdateUGSClasses", (t) => { - t.test("Should produce an empty object by default", async (t) => { - const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger); + t.test("Should produce an empty object by default", async (t) => { + const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger); - t.strictSame(res, {}); + t.strictSame(res, {}); - t.end(); - }); + t.end(); + }); - t.test("Should call and merge the ClassHandler", async (t) => { - const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger); + t.test("Should call and merge the ClassHandler", async (t) => { + const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger); - t.strictSame(res, { dan: 2 }); + t.strictSame(res, { dan: 2 }); - t.end(); - }); + t.end(); + }); - t.test("Should call static handlers if there is one", async (t) => { - const res = await UpdateUGSClasses( - "gitadora", - "Dora", - 1, - { - skill: 9000, - }, - null, - logger - ); + t.test("Should call static handlers if there is one", async (t) => { + const res = await UpdateUGSClasses( + "gitadora", + "Dora", + 1, + { + skill: 9000, + }, + null, + logger + ); - t.strictSame(res, { colour: GitadoraColours.RAINBOW }); + t.strictSame(res, { colour: GitadoraColours.RAINBOW }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#CalculateClassDeltas", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return improved classes from null", (t) => { - const res = CalculateClassDeltas("SP", { dan: 18 }, null, 1, logger); + t.test("Should return improved classes from null", (t) => { + const res = CalculateClassDeltas("SP", { dan: 18 }, null, 1, logger); - t.strictSame(res, [ - { - set: "dan", - playtype: "SP", - old: null, - new: 18, - }, - ]); + t.strictSame(res, [ + { + set: "dan", + playtype: "SP", + old: null, + new: 18, + }, + ]); - t.end(); - }); + t.end(); + }); - t.test("Should return improved classes from null class", (t) => { - const res = CalculateClassDeltas( - "SP", - { dan: 18 }, - { classes: {} } as UserGameStats, - 1, - logger - ); + t.test("Should return improved classes from null class", (t) => { + const res = CalculateClassDeltas( + "SP", + { dan: 18 }, + { classes: {} } as UserGameStats, + 1, + logger + ); - t.strictSame(res, [ - { - set: "dan", - playtype: "SP", - old: null, - new: 18, - }, - ]); + t.strictSame(res, [ + { + set: "dan", + playtype: "SP", + old: null, + new: 18, + }, + ]); - t.end(); - }); + t.end(); + }); - t.test("Should return improved classes", (t) => { - const res = CalculateClassDeltas( - "SP", - { dan: 18 }, - { classes: { dan: 17 } } as unknown as UserGameStats, - 1, - logger - ); + t.test("Should return improved classes", (t) => { + const res = CalculateClassDeltas( + "SP", + { dan: 18 }, + { classes: { dan: 17 } } as unknown as UserGameStats, + 1, + logger + ); - t.strictSame(res, [ - { - set: "dan", - playtype: "SP", - old: 17, - new: 18, - }, - ]); + t.strictSame(res, [ + { + set: "dan", + playtype: "SP", + old: 17, + new: 18, + }, + ]); - t.end(); - }); + t.end(); + }); - t.test("Should not return identical classes", (t) => { - const res = CalculateClassDeltas( - "SP", - { dan: 18 }, - { classes: { dan: 18 } } as unknown as UserGameStats, - 1, - logger - ); + t.test("Should not return identical classes", (t) => { + const res = CalculateClassDeltas( + "SP", + { dan: 18 }, + { classes: { dan: 18 } } as unknown as UserGameStats, + 1, + logger + ); - t.strictSame(res, []); + t.strictSame(res, []); - t.end(); - }); + t.end(); + }); - t.test("Should not return worse classes", (t) => { - const res = CalculateClassDeltas( - "SP", - { dan: 16 }, - { classes: { dan: 18 } } as unknown as UserGameStats, - 1, - logger - ); + t.test("Should not return worse classes", (t) => { + const res = CalculateClassDeltas( + "SP", + { dan: 16 }, + { classes: { dan: 18 } } as unknown as UserGameStats, + 1, + logger + ); - t.strictSame(res, []); + t.strictSame(res, []); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.ts b/server/src/lib/score-import/framework/user-game-stats/classes.ts index 9d2cc9dc2..720d86b69 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.ts @@ -3,20 +3,20 @@ import { GameClasses } from "tachi-common/js/game-classes"; import deepmerge from "deepmerge"; import { KtLogger } from "../../../logger/logger"; import { - CalculateGitadoraColour, - CalculateJubeatColour, - CalculateSDVXClass, + CalculateGitadoraColour, + CalculateJubeatColour, + CalculateSDVXClass, } from "./builtin-class-handlers"; import { ReturnClassIfGreater } from "../../../../utils/class"; import { RedisPub } from "../../../../external/redis/redis-IPC"; import { ClassHandler, ScoreClasses } from "./types"; type ClassHandlerMap = { - [G in Game]: - | { - [P in Playtypes[G]]: ClassHandler; - } - | null; + [G in Game]: + | { + [P in Playtypes[G]]: ClassHandler; + } + | null; }; /** @@ -29,24 +29,24 @@ type ClassHandlerMap = { * on your profile skill level, which is known at the time this function is called. */ const STATIC_CLASS_HANDLERS: ClassHandlerMap = { - iidx: null, - bms: null, - chunithm: null, - ddr: null, - gitadora: { - Gita: CalculateGitadoraColour, - Dora: CalculateGitadoraColour, - }, - jubeat: { - Single: CalculateJubeatColour, - }, - maimai: null, - museca: null, - popn: null, - sdvx: { - Single: CalculateSDVXClass, - }, - usc: null, + iidx: null, + bms: null, + chunithm: null, + ddr: null, + gitadora: { + Gita: CalculateGitadoraColour, + Dora: CalculateGitadoraColour, + }, + jubeat: { + Single: CalculateJubeatColour, + }, + maimai: null, + museca: null, + popn: null, + sdvx: { + Single: CalculateSDVXClass, + }, + usc: null, }; /** @@ -71,35 +71,35 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = { * defaults. */ export async function UpdateUGSClasses( - game: Game, - playtype: Playtypes[Game], - userID: integer, - ratings: Record, - ClassHandler: ClassHandler | null, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + ratings: Record, + ClassHandler: ClassHandler | null, + logger: KtLogger ): Promise { - let classes: ScoreClasses = {}; + let classes: ScoreClasses = {}; - // @ts-expect-error This one sucks - I need to look into a better way of representing these types - if (STATIC_CLASS_HANDLERS[game] && STATIC_CLASS_HANDLERS[game][playtype]) { - // @ts-expect-error see above - classes = await STATIC_CLASS_HANDLERS[game][playtype]( - game, - playtype, - userID, - ratings, - logger - ); - } + // @ts-expect-error This one sucks - I need to look into a better way of representing these types + if (STATIC_CLASS_HANDLERS[game] && STATIC_CLASS_HANDLERS[game][playtype]) { + // @ts-expect-error see above + classes = await STATIC_CLASS_HANDLERS[game][playtype]( + game, + playtype, + userID, + ratings, + logger + ); + } - if (ClassHandler) { - logger.debug(`Calling custom class handler.`); - const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {}; + if (ClassHandler) { + logger.debug(`Calling custom class handler.`); + const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {}; - classes = deepmerge(customClasses, classes); - } + classes = deepmerge(customClasses, classes); + } - return classes; + return classes; } /** @@ -110,54 +110,54 @@ export async function UpdateUGSClasses( * so that other services can listen for it. In the future we might allow webhooks, too. */ export function CalculateClassDeltas( - playtype: Playtypes[Game], - classes: ScoreClasses, - userGameStats: UserGameStats | null, - userID: integer, - logger: KtLogger + playtype: Playtypes[Game], + classes: ScoreClasses, + userGameStats: UserGameStats | null, + userID: integer, + logger: KtLogger ): ClassDelta[] { - const deltas: ClassDelta[] = []; + const deltas: ClassDelta[] = []; - for (const s in classes) { - const classSet = s as keyof GameClasses; - const classVal = classes[classSet]; + for (const s in classes) { + const classSet = s as keyof GameClasses; + const classVal = classes[classSet]; - if (classVal === undefined) { - logger.debug(`Skipped deltaing-class ${classSet}.`); - continue; - } + if (classVal === undefined) { + logger.debug(`Skipped deltaing-class ${classSet}.`); + continue; + } - try { - const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); + try { + const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); - if (isGreater === false) { - continue; - } else { - let delta: ClassDelta; - if (isGreater === null) { - delta = { - set: classSet, - playtype, - old: null, - new: classVal, - }; - } else { - delta = { - set: classSet, - playtype, - old: userGameStats!.classes[classSet]!, - new: classVal, - }; - } + if (isGreater === false) { + continue; + } else { + let delta: ClassDelta; + if (isGreater === null) { + delta = { + set: classSet, + playtype, + old: null, + new: classVal, + }; + } else { + delta = { + set: classSet, + playtype, + old: userGameStats!.classes[classSet]!, + new: classVal, + }; + } - RedisPub("class-update", { userID, ...delta }); + RedisPub("class-update", { userID, ...delta }); - deltas.push(delta); - } - } catch (err) { - logger.error(err); - } - } + deltas.push(delta); + } + } catch (err) { + logger.error(err); + } + } - return deltas; + return deltas; } diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts index a9a769f38..4446ca27d 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts @@ -7,77 +7,77 @@ import { CloseAllConnections } from "../../../../test-utils/close-connections"; const logger = CreateLogCtx(__filename); t.test("#CalculateRatings", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should work for games with no custom ratings", async (t) => { - const res = await CalculateRatings("popn", "9B", 1, logger); + t.test("Should work for games with no custom ratings", async (t) => { + const res = await CalculateRatings("popn", "9B", 1, logger); - t.strictSame(res, {}, "Should return an empty object."); + t.strictSame(res, {}, "Should return an empty object."); - t.end(); - }); + t.end(); + }); - t.test("Should return BPI for IIDX", async (t) => { - const res = await CalculateRatings("iidx", "SP", 1, logger); + t.test("Should return BPI for IIDX", async (t) => { + const res = await CalculateRatings("iidx", "SP", 1, logger); - t.strictSame( - res, - { BPI: 0, ktRating: 0, ktLampRating: 0 }, - "Should return BPI as a custom key." - ); + t.strictSame( + res, + { BPI: 0, ktRating: 0, ktLampRating: 0 }, + "Should return BPI as a custom key." + ); - const resDP = await CalculateRatings("iidx", "DP", 1, logger); + const resDP = await CalculateRatings("iidx", "DP", 1, logger); - t.strictSame( - resDP, - { BPI: 0, ktRating: 0, ktLampRating: 0 }, - "Should return BPI as a custom key." - ); + t.strictSame( + resDP, + { BPI: 0, ktRating: 0, ktLampRating: 0 }, + "Should return BPI as a custom key." + ); - t.end(); - }); + t.end(); + }); - t.test("Should return VF4 and VF5 for SDVX", async (t) => { - const res = await CalculateRatings("sdvx", "Single", 1, logger); + t.test("Should return VF4 and VF5 for SDVX", async (t) => { + const res = await CalculateRatings("sdvx", "Single", 1, logger); - t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); + t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); - t.end(); - }); + t.end(); + }); - t.test("Should return VF4 and VF5 for USC", async (t) => { - const res = await CalculateRatings("usc", "Single", 1, logger); + t.test("Should return VF4 and VF5 for USC", async (t) => { + const res = await CalculateRatings("usc", "Single", 1, logger); - t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); + t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); - t.end(); - }); + t.end(); + }); - t.test("Should return MFCP for DDR", async (t) => { - const res = await CalculateRatings("ddr", "SP", 1, logger); + t.test("Should return MFCP for DDR", async (t) => { + const res = await CalculateRatings("ddr", "SP", 1, logger); - t.strictSame(res, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys."); + t.strictSame(res, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys."); - const resDP = await CalculateRatings("ddr", "DP", 1, logger); + const resDP = await CalculateRatings("ddr", "DP", 1, logger); - t.strictSame(resDP, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys."); + t.strictSame(resDP, { MFCP: 0, ktRating: 0 }, "Should return MFCP keys."); - t.end(); - }); + t.end(); + }); - t.test("Should return skill for Gitadora", async (t) => { - const res = await CalculateRatings("gitadora", "Dora", 1, logger); + t.test("Should return skill for Gitadora", async (t) => { + const res = await CalculateRatings("gitadora", "Dora", 1, logger); - t.strictSame(res, { skill: 0 }, "Should return skill keys."); + t.strictSame(res, { skill: 0 }, "Should return skill keys."); - const resDP = await CalculateRatings("gitadora", "Gita", 1, logger); + const resDP = await CalculateRatings("gitadora", "Gita", 1, logger); - t.strictSame(resDP, { skill: 0 }, "Should return skill keys."); + t.strictSame(resDP, { skill: 0 }, "Should return skill keys."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.ts b/server/src/lib/score-import/framework/user-game-stats/rating.ts index af7e46cc7..8d8cfef05 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.ts @@ -1,10 +1,10 @@ import { - Game, - Playtypes, - integer, - ScoreCalculatedDataLookup, - IDStrings, - UserGameStats, + Game, + Playtypes, + integer, + ScoreCalculatedDataLookup, + IDStrings, + UserGameStats, } from "tachi-common"; import db from "../../../../external/mongo/db"; import { KtLogger } from "../../../logger/logger"; @@ -12,219 +12,219 @@ import { KtLogger } from "../../../logger/logger"; type CustomCalcNames = ScoreCalculatedDataLookup[IDStrings]; function LazySumAll(key: CustomCalcNames) { - return async (game: Game, playtype: Playtypes[Game], userID: integer) => { - const sc = await db["personal-bests"].find({ - game: game, - playtype: playtype, - userID: userID, - isPrimary: true, - [`calculatedData.${key}`]: { $gt: 0 }, - }); + return async (game: Game, playtype: Playtypes[Game], userID: integer) => { + const sc = await db["personal-bests"].find({ + game: game, + playtype: playtype, + userID: userID, + isPrimary: true, + [`calculatedData.${key}`]: { $gt: 0 }, + }); - const result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0); + const result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0); - return result; - }; + return result; + }; } function LazyCalcN(key: CustomCalcNames, n: integer, returnMean?: boolean) { - return async (game: Game, playtype: Playtypes[Game], userID: integer) => { - const sc = await db["personal-bests"].find( - { - game: game, - playtype: playtype, - userID: userID, - isPrimary: true, - [`calculatedData.${key}`]: { $gt: 0 }, - }, - { - limit: n, - sort: { [`calculatedData.${key}`]: -1 }, - } - ); + return async (game: Game, playtype: Playtypes[Game], userID: integer) => { + const sc = await db["personal-bests"].find( + { + game: game, + playtype: playtype, + userID: userID, + isPrimary: true, + [`calculatedData.${key}`]: { $gt: 0 }, + }, + { + limit: n, + sort: { [`calculatedData.${key}`]: -1 }, + } + ); - let result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0); + let result = sc.reduce((a, b) => a + b.calculatedData[key]!, 0); - if (returnMean) { - result = result / n; - } + if (returnMean) { + result = result / n; + } - return result; - }; + return result; + }; } const LazySumN = (key: CustomCalcNames, n: integer) => LazyCalcN(key, n, false); const LazyMeanN = (key: CustomCalcNames, n: integer) => LazyCalcN(key, n, true); type RatingFunctions = { - [G in Game]: { - [P in Playtypes[G]]: ( - game: Game, - playtype: Playtypes[Game], - userID: integer, - logger: KtLogger - ) => Promise; - }; + [G in Game]: { + [P in Playtypes[G]]: ( + game: Game, + playtype: Playtypes[Game], + userID: integer, + logger: KtLogger + ) => Promise; + }; }; const RatingFunctions: RatingFunctions = { - iidx: { - SP: async (g, p, u) => ({ - BPI: await LazyMeanN("BPI", 20)(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - DP: async (g, p, u) => ({ - BPI: await LazyMeanN("BPI", 20)(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - }, - sdvx: { - Single: async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }), - }, - usc: { - Single: async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }), - }, - ddr: { - SP: async (g, p, u) => ({ - MFCP: await LazySumAll("MFCP")(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - DP: async (g, p, u) => ({ - MFCP: await LazySumAll("MFCP")(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - gitadora: { - Gita: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }), - Dora: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }), - }, - bms: { - "7K": async (g, p, u) => ({ - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - "14K": async (g, p, u) => ({ - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - }, - chunithm: { - Single: async (g, p, u) => ({ - naiveRating: await LazyMeanN("rating", 20)(g, p, u), - }), - }, - popn: { - "9B": async () => ({}), - }, - museca: { - Single: async (g, p, u) => ({ - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - maimai: { - Single: async (g, p, u) => ({ - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - jubeat: { - Single: async (g, p, u, l) => ({ - jubility: await CalculateJubility(g, p, u, l), - }), - }, + iidx: { + SP: async (g, p, u) => ({ + BPI: await LazyMeanN("BPI", 20)(g, p, u), + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), + }), + DP: async (g, p, u) => ({ + BPI: await LazyMeanN("BPI", 20)(g, p, u), + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), + }), + }, + sdvx: { + Single: async (g, p, u) => ({ + VF6: await LazySumN("VF6", 50)(g, p, u), + }), + }, + usc: { + Single: async (g, p, u) => ({ + VF6: await LazySumN("VF6", 50)(g, p, u), + }), + }, + ddr: { + SP: async (g, p, u) => ({ + MFCP: await LazySumAll("MFCP")(g, p, u), + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }), + DP: async (g, p, u) => ({ + MFCP: await LazySumAll("MFCP")(g, p, u), + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }), + }, + gitadora: { + Gita: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }), + Dora: async (g, p, u, l) => ({ skill: await CalculateGitadoraSkill(g, p, u, l) }), + }, + bms: { + "7K": async (g, p, u) => ({ + ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), + }), + "14K": async (g, p, u) => ({ + ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), + }), + }, + chunithm: { + Single: async (g, p, u) => ({ + naiveRating: await LazyMeanN("rating", 20)(g, p, u), + }), + }, + popn: { + "9B": async () => ({}), + }, + museca: { + Single: async (g, p, u) => ({ + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }), + }, + maimai: { + Single: async (g, p, u) => ({ + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }), + }, + jubeat: { + Single: async (g, p, u, l) => ({ + jubility: await CalculateJubility(g, p, u, l), + }), + }, }; export function CalculateRatings( - game: Game, - playtype: Playtypes[Game], - userID: integer, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + logger: KtLogger ): Promise>> { - // @ts-expect-error too lazy to type this properly - return RatingFunctions[game][playtype](game, playtype, userID, logger); + // @ts-expect-error too lazy to type this properly + return RatingFunctions[game][playtype](game, playtype, userID, logger); } async function CalculateGitadoraSkill( - game: Game, - playtype: Playtypes[Game], - userID: integer, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + logger: KtLogger ) { - const hotCharts = await db.charts.gitadora.find( - { "flags.HOT N-1": true }, - { projection: { chartID: 1 } } - ); + const hotCharts = await db.charts.gitadora.find( + { "flags.HOT N-1": true }, + { projection: { chartID: 1 } } + ); - const hotChartIDs = hotCharts.map((e) => e.chartID); + const hotChartIDs = hotCharts.map((e) => e.chartID); - const [bestHotScores, bestScores] = await Promise.all([ - db["personal-bests"].find( - { userID, chartID: { $in: hotChartIDs } }, - { - sort: { "calculatedData.skill": -1 }, - limit: 25, - projection: { "calculatedData.skill": 1 }, - } - ), - // @inefficient - // $nin is VERY expensive, there might be a better way to do this. - db["personal-bests"].find( - { userID, chartID: { $nin: hotChartIDs } }, - { - sort: { "calculatedData.skill": -1 }, - limit: 25, - projection: { "calculatedData.skill": 1 }, - } - ), - ]); + const [bestHotScores, bestScores] = await Promise.all([ + db["personal-bests"].find( + { userID, chartID: { $in: hotChartIDs } }, + { + sort: { "calculatedData.skill": -1 }, + limit: 25, + projection: { "calculatedData.skill": 1 }, + } + ), + // @inefficient + // $nin is VERY expensive, there might be a better way to do this. + db["personal-bests"].find( + { userID, chartID: { $nin: hotChartIDs } }, + { + sort: { "calculatedData.skill": -1 }, + limit: 25, + projection: { "calculatedData.skill": 1 }, + } + ), + ]); - let skill = 0; - skill += bestHotScores.reduce((a, r) => a + r.calculatedData.skill!, 0); - skill += bestScores.reduce((a, r) => a + r.calculatedData.skill!, 0); + let skill = 0; + skill += bestHotScores.reduce((a, r) => a + r.calculatedData.skill!, 0); + skill += bestScores.reduce((a, r) => a + r.calculatedData.skill!, 0); - return skill; + return skill; } async function CalculateJubility( - game: Game, - playtype: Playtypes[Game], - userID: integer, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + logger: KtLogger ): Promise { - const hotCharts = await db.charts.jubeat.find( - { "flags.HOT N-1": true }, - { projection: { chartID: 1 } } - ); + const hotCharts = await db.charts.jubeat.find( + { "flags.HOT N-1": true }, + { projection: { chartID: 1 } } + ); - const hotChartIDs = hotCharts.map((e) => e.chartID); + const hotChartIDs = hotCharts.map((e) => e.chartID); - const [bestHotScores, bestScores] = await Promise.all([ - db["personal-bests"].find( - { userID, chartID: { $in: hotChartIDs } }, - { - sort: { "calculatedData.jubility": -1 }, - limit: 25, - projection: { "calculatedData.jubility": 1 }, - } - ), - // @inefficient - // see gitadoraskillcalc - db["personal-bests"].find( - { userID, chartID: { $nin: hotChartIDs } }, - { - sort: { "calculatedData.jubility": -1 }, - limit: 25, - projection: { "calculatedData.jubility": 1 }, - } - ), - ]); + const [bestHotScores, bestScores] = await Promise.all([ + db["personal-bests"].find( + { userID, chartID: { $in: hotChartIDs } }, + { + sort: { "calculatedData.jubility": -1 }, + limit: 25, + projection: { "calculatedData.jubility": 1 }, + } + ), + // @inefficient + // see gitadoraskillcalc + db["personal-bests"].find( + { userID, chartID: { $nin: hotChartIDs } }, + { + sort: { "calculatedData.jubility": -1 }, + limit: 25, + projection: { "calculatedData.jubility": 1 }, + } + ), + ]); - let skill = 0; - skill += bestHotScores.reduce((a, r) => a + r.calculatedData.jubility!, 0); - skill += bestScores.reduce((a, r) => a + r.calculatedData.jubility!, 0); + let skill = 0; + skill += bestHotScores.reduce((a, r) => a + r.calculatedData.jubility!, 0); + skill += bestScores.reduce((a, r) => a + r.calculatedData.jubility!, 0); - return skill; + return skill; } diff --git a/server/src/lib/score-import/framework/user-game-stats/types.ts b/server/src/lib/score-import/framework/user-game-stats/types.ts index eb562a04e..0266cf701 100644 --- a/server/src/lib/score-import/framework/user-game-stats/types.ts +++ b/server/src/lib/score-import/framework/user-game-stats/types.ts @@ -6,11 +6,11 @@ import { KtLogger } from "../../../logger/logger"; export type ScoreClasses = Partial>; export interface ClassHandler { - ( - game: Game, - playtype: Playtypes[Game], - userID: integer, - ratings: Record, - logger: KtLogger - ): Promise | ScoreClasses | undefined; + ( + game: Game, + playtype: Playtypes[Game], + userID: integer, + ratings: Record, + logger: KtLogger + ): Promise | ScoreClasses | undefined; } diff --git a/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts b/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts index 420d0c034..119fe494c 100644 --- a/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts @@ -12,185 +12,185 @@ const logger = CreateLogCtx(__filename); // more of an integration test t.test("#UpdateUsersGamePlaytypeStats", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should create new UserGameStats if the user has none", async (t) => { - await db["game-stats"].remove({}); + t.test("Should create new UserGameStats if the user has none", async (t) => { + await db["game-stats"].remove({}); - const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger); + const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger); - t.strictSame(res, [], "Should return an empty object"); + t.strictSame(res, [], "Should return an empty object"); - const gs = await db["game-stats"].findOne(); + const gs = await db["game-stats"].findOne(); - t.hasStrict( - gs, - { - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, - classes: {}, - }, - "Should insert an appropriate game-stats object" - ); + t.hasStrict( + gs, + { + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, + classes: {}, + }, + "Should insert an appropriate game-stats object" + ); - t.end(); - }); + t.end(); + }); - t.test("Should update UserGameStats if the user has one", async (t) => { - await db["game-stats"].remove({}); + t.test("Should update UserGameStats if the user has one", async (t) => { + await db["game-stats"].remove({}); - await db["game-stats"].insert({ - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, - classes: {}, - }); + await db["game-stats"].insert({ + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, + classes: {}, + }); - // insert some mock scores - const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + // insert some mock scores + const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - await db["personal-bests"].insert( - ratings.map((e) => - deepmerge(TestingIIDXSPScorePB, { - chartID: crypto.randomBytes(20).toString("hex"), - calculatedData: { - ktRating: e, - ktLampRating: 0, - }, - }) - ) - ); + await db["personal-bests"].insert( + ratings.map((e) => + deepmerge(TestingIIDXSPScorePB, { + chartID: crypto.randomBytes(20).toString("hex"), + calculatedData: { + ktRating: e, + ktLampRating: 0, + }, + }) + ) + ); - const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger); + const res = await UpdateUsersGamePlaytypeStats("iidx", "SP", 1, null, logger); - t.strictSame(res, [], "Should return an empty object"); + t.strictSame(res, [], "Should return an empty object"); - const gs = await db["game-stats"].findOne(); + const gs = await db["game-stats"].findOne(); - t.hasStrict( - gs, - { - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: ratings.reduce((a, r) => a + r, 0) / 20, ktLampRating: 0 }, - classes: {}, - }, - "Should update the game-stats object" - ); + t.hasStrict( + gs, + { + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: ratings.reduce((a, r) => a + r, 0) / 20, ktLampRating: 0 }, + classes: {}, + }, + "Should update the game-stats object" + ); - t.end(); - }); + t.end(); + }); - t.test("Should return class deltas", async (t) => { - await db["game-stats"].remove({}); + t.test("Should return class deltas", async (t) => { + await db["game-stats"].remove({}); - await db["game-stats"].insert({ - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, + await db["game-stats"].insert({ + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, - classes: {}, - }); + classes: {}, + }); - const res = await UpdateUsersGamePlaytypeStats( - "iidx", - "SP", - 1, - () => ({ dan: 18 }), // lmao - logger - ); + const res = await UpdateUsersGamePlaytypeStats( + "iidx", + "SP", + 1, + () => ({ dan: 18 }), // lmao + logger + ); - t.strictSame( - res, - [ - { - set: "dan", - playtype: "SP", - old: null, - new: 18, - }, - ], - "Should return the class delta" - ); + t.strictSame( + res, + [ + { + set: "dan", + playtype: "SP", + old: null, + new: 18, + }, + ], + "Should return the class delta" + ); - const gs = await db["game-stats"].findOne(); + const gs = await db["game-stats"].findOne(); - t.hasStrict( - gs, - { - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, - classes: { - dan: 18, - }, - }, - "Should update the game-stats object" - ); + t.hasStrict( + gs, + { + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, + classes: { + dan: 18, + }, + }, + "Should update the game-stats object" + ); - t.end(); - }); + t.end(); + }); - t.test("Should return updated class deltas", async (t) => { - await db["game-stats"].remove({}); + t.test("Should return updated class deltas", async (t) => { + await db["game-stats"].remove({}); - await db["game-stats"].insert({ - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, - classes: { - dan: 17, - }, - }); + await db["game-stats"].insert({ + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, + classes: { + dan: 17, + }, + }); - const res = await UpdateUsersGamePlaytypeStats( - "iidx", - "SP", - 1, - () => ({ dan: 18 }), // lmao - logger - ); + const res = await UpdateUsersGamePlaytypeStats( + "iidx", + "SP", + 1, + () => ({ dan: 18 }), // lmao + logger + ); - t.strictSame( - res, - [ - { - set: "dan", - playtype: "SP", - old: 17, - new: 18, - }, - ], - "Should return the updated class delta" - ); + t.strictSame( + res, + [ + { + set: "dan", + playtype: "SP", + old: 17, + new: 18, + }, + ], + "Should return the updated class delta" + ); - const gs = await db["game-stats"].findOne(); + const gs = await db["game-stats"].findOne(); - t.hasStrict( - gs, - { - game: "iidx", - playtype: "SP", - userID: 1, - ratings: { ktRating: 0, ktLampRating: 0 }, - classes: { - dan: 18, - }, - }, - "Should update the game-stats object" - ); + t.hasStrict( + gs, + { + game: "iidx", + playtype: "SP", + userID: 1, + ratings: { ktRating: 0, ktLampRating: 0 }, + classes: { + dan: 18, + }, + }, + "Should update the game-stats object" + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts index 0d9f92894..b2f9680c5 100644 --- a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts +++ b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts @@ -10,61 +10,61 @@ import { CalculateRatings } from "./rating"; import { ClassHandler } from "./types"; export async function UpdateUsersGamePlaytypeStats( - game: Game, - playtype: Playtypes[Game], - userID: integer, - classHandler: ClassHandler | null, - logger: KtLogger + game: Game, + playtype: Playtypes[Game], + userID: integer, + classHandler: ClassHandler | null, + logger: KtLogger ): Promise { - const ratings = await CalculateRatings(game, playtype, userID, logger); + const ratings = await CalculateRatings(game, playtype, userID, logger); - // Attempt to find a users game stats if one already exists. If one doesn't exist, - // this is this players first import for this game! - const userGameStats = await db["game-stats"].findOne({ - game, - playtype, - userID, - }); + // Attempt to find a users game stats if one already exists. If one doesn't exist, + // this is this players first import for this game! + const userGameStats = await db["game-stats"].findOne({ + game, + playtype, + userID, + }); - logger.debug(`Calculating UGSClasses...`); + logger.debug(`Calculating UGSClasses...`); - const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger); + const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger); - logger.debug(`Finished Calculating UGSClasses`); + logger.debug(`Finished Calculating UGSClasses`); - logger.debug(`Calculating Class Deltas...`); + logger.debug(`Calculating Class Deltas...`); - const deltas = CalculateClassDeltas(playtype, classes, userGameStats, userID, logger); + const deltas = CalculateClassDeltas(playtype, classes, userGameStats, userID, logger); - logger.debug(`Had ${deltas.length} deltas.`); + logger.debug(`Had ${deltas.length} deltas.`); - if (userGameStats) { - logger.debug(`Updated player gamestats for ${game} (${playtype})`); - await db["game-stats"].update( - { - game, - playtype, - userID, - }, - { - $set: { - ratings, - classes, - }, - } - ); - } else { - const newStats: UserGameStats = { - game, - playtype, - userID, - ratings, - classes, - }; + if (userGameStats) { + logger.debug(`Updated player gamestats for ${game} (${playtype})`); + await db["game-stats"].update( + { + game, + playtype, + userID, + }, + { + $set: { + ratings, + classes, + }, + } + ); + } else { + const newStats: UserGameStats = { + game, + playtype, + userID, + ratings, + classes, + }; - logger.info(`Created new gamestats for ${game} (${playtype})`); - await db["game-stats"].insert(newStats); - } + logger.info(`Created new gamestats for ${game} (${playtype})`); + await db["game-stats"].insert(newStats); + } - return deltas; + return deltas; } diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/converter.test.ts b/server/src/lib/score-import/import-types/api/arc-ddr/converter.test.ts index 2b86e81e3..2d8964c03 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/converter.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/converter.test.ts @@ -14,96 +14,96 @@ const putySong = GetKTDataJSON("./tachi/ddr-puty-song.json") as ARCDDRScore; const putyChart = GetKTDataJSON("./tachi/ddr-puty-chart.json") as ARCDDRScore; t.test("#ConvertAPIArcDDR", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should convert a valid score", async (t) => { - const res = await ConvertAPIArcDDR(arcScore, {}, "api/arc-ddr", logger); + t.test("Should convert a valid score", async (t) => { + const res = await ConvertAPIArcDDR(arcScore, {}, "api/arc-ddr", logger); - t.hasStrict(res, { - song: putySong, - chart: putyChart, - dryScore: { - comment: null, - game: "ddr", - importType: "api/arc-ddr", - timeAchieved: 1591822550346, - service: "ARC DDR Ace", - scoreData: { - grade: "AAA", - percent: 99.976, - score: 807, - hitData: { - marvelous: 236, - perfect: 24, - great: 0, - good: 0, - boo: 0, - miss: 0, - ok: 17, - ng: 0, - }, - hitMeta: { - fast: 10, - slow: 14, - maxCombo: 260, - exScore: 807, - }, - lamp: "PERFECT FULL COMBO", - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + song: putySong, + chart: putyChart, + dryScore: { + comment: null, + game: "ddr", + importType: "api/arc-ddr", + timeAchieved: 1591822550346, + service: "ARC DDR Ace", + scoreData: { + grade: "AAA", + percent: 99.976, + score: 807, + hitData: { + marvelous: 236, + perfect: 24, + great: 0, + good: 0, + boo: 0, + miss: 0, + ok: 17, + ng: 0, + }, + hitMeta: { + fast: 10, + slow: 14, + maxCombo: 260, + exScore: 807, + }, + lamp: "PERFECT FULL COMBO", + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw on an invalid score", (t) => { - t.rejects( - () => - ConvertAPIArcDDR( - deepmerge(arcScore, { ex_score: "foo" }), - {}, - "api/arc-ddr", - logger - ), - { - message: /Invalid ARC Score:/iu, - } as any - ); + t.test("Should throw on an invalid score", (t) => { + t.rejects( + () => + ConvertAPIArcDDR( + deepmerge(arcScore, { ex_score: "foo" }), + {}, + "api/arc-ddr", + logger + ), + { + message: /Invalid ARC Score:/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw on no chart", (t) => { - t.rejects( - () => - ConvertAPIArcDDR( - deepmerge(arcScore, { chart_id: "foo" }), - {}, - "api/arc-ddr", - logger - ), - { - message: /Could not find chart/iu, - } as any - ); + t.test("Should throw on no chart", (t) => { + t.rejects( + () => + ConvertAPIArcDDR( + deepmerge(arcScore, { chart_id: "foo" }), + {}, + "api/arc-ddr", + logger + ), + { + message: /Could not find chart/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ResolveARCDDRLamp", (t) => { - t.equal(ResolveARCDDRLamp("FAIL"), "FAILED"); - t.equal(ResolveARCDDRLamp("CLEAR"), "CLEAR"); - t.equal(ResolveARCDDRLamp("CLEAR_3LIFE"), "LIFE4"); - t.equal(ResolveARCDDRLamp("GOOD_FC"), "FULL COMBO"); - t.equal(ResolveARCDDRLamp("GREAT_FC"), "GREAT FULL COMBO"); - t.equal(ResolveARCDDRLamp("PERFECT_FC"), "PERFECT FULL COMBO"); - t.equal(ResolveARCDDRLamp("MARVELOUS_FC"), "MARVELOUS FULL COMBO"); + t.equal(ResolveARCDDRLamp("FAIL"), "FAILED"); + t.equal(ResolveARCDDRLamp("CLEAR"), "CLEAR"); + t.equal(ResolveARCDDRLamp("CLEAR_3LIFE"), "LIFE4"); + t.equal(ResolveARCDDRLamp("GOOD_FC"), "FULL COMBO"); + t.equal(ResolveARCDDRLamp("GREAT_FC"), "GREAT FULL COMBO"); + t.equal(ResolveARCDDRLamp("PERFECT_FC"), "PERFECT FULL COMBO"); + t.equal(ResolveARCDDRLamp("MARVELOUS_FC"), "MARVELOUS FULL COMBO"); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/converter.ts b/server/src/lib/score-import/import-types/api/arc-ddr/converter.ts index ded9f42b9..dceb130eb 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/converter.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/converter.ts @@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types"; import { ConverterFunction } from "../../common/types"; import p, { PrudenceSchema } from "prudence"; import { - InvalidScoreFailure, - KTDataNotFoundFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { FormatPrError } from "../../../../../utils/prudence"; import { ARCDDRScore } from "./types"; @@ -11,128 +11,128 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts"; import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs"; import { DryScore } from "../../../framework/common/types"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { Lamps } from "tachi-common"; // There's a bunch of other useless fields but we don't care const PR_ArcDDRScore: PrudenceSchema = { - chart_id: "string", - lamp: p.isIn( - "MARVELOUS_FC", - "PERFECT_FC", - "GREAT_FC", - "GOOD_FC", - "CLEAR_3LIFE", - "CLEAR", - "FAIL" - ), - score: p.isBoundedInteger(0, 1_000_000), - ex_score: p.isPositiveInteger, - max_combo: p.isPositiveInteger, - judgments: { - marvelous: p.isPositiveInteger, - perfect: p.isPositiveInteger, - great: p.isPositiveInteger, - good: p.isPositiveInteger, - boo: p.isPositiveInteger, - miss: p.isPositiveInteger, - ok: p.isPositiveInteger, - ng: p.isPositiveInteger, - }, - fast: p.isPositiveInteger, - slow: p.isPositiveInteger, - timestamp: "string", + chart_id: "string", + lamp: p.isIn( + "MARVELOUS_FC", + "PERFECT_FC", + "GREAT_FC", + "GOOD_FC", + "CLEAR_3LIFE", + "CLEAR", + "FAIL" + ), + score: p.isBoundedInteger(0, 1_000_000), + ex_score: p.isPositiveInteger, + max_combo: p.isPositiveInteger, + judgments: { + marvelous: p.isPositiveInteger, + perfect: p.isPositiveInteger, + great: p.isPositiveInteger, + good: p.isPositiveInteger, + boo: p.isPositiveInteger, + miss: p.isPositiveInteger, + ok: p.isPositiveInteger, + ng: p.isPositiveInteger, + }, + fast: p.isPositiveInteger, + slow: p.isPositiveInteger, + timestamp: "string", }; export const ConvertAPIArcDDR: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const err = p(data, PR_ArcDDRScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); + const err = p(data, PR_ArcDDRScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); - if (err) { - throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); - } + if (err) { + throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); + } - // confirmed by Prudence above. - const score = data as ARCDDRScore; + // confirmed by Prudence above. + const score = data as ARCDDRScore; - const chart = await FindChartOnARCID("ddr", score.chart_id); + const chart = await FindChartOnARCID("ddr", score.chart_id); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with chart_id ${score.chart_id}.`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with chart_id ${score.chart_id}.`, + importType, + data, + context + ); + } - const song = await FindSongOnIDGuaranteed("ddr", chart.songID, logger); + const song = await FindSongOnIDGuaranteed("ddr", chart.songID, logger); - const { grade, percent } = GenericGetGradeAndPercent("ddr", score.score, chart); + const { grade, percent } = GenericGetGradeAndPercent("ddr", score.score, chart); - const timeAchieved = ParseDateFromString(score.timestamp); + const timeAchieved = ParseDateFromString(score.timestamp); - const lamp = ResolveARCDDRLamp(score.lamp); + const lamp = ResolveARCDDRLamp(score.lamp); - const dryScore: DryScore<"ddr:SP" | "ddr:DP"> = { - comment: null, - game: "ddr", - importType, - timeAchieved, - service: "ARC DDR Ace", - scoreData: { - grade, - percent, - score: score.ex_score, - hitData: { - marvelous: score.judgments.marvelous, - perfect: score.judgments.perfect, - great: score.judgments.great, - good: score.judgments.good, - boo: score.judgments.boo, - miss: score.judgments.miss, - ok: score.judgments.ok, - ng: score.judgments.ng, - }, - hitMeta: { - fast: score.fast, - slow: score.slow, - maxCombo: score.max_combo, - exScore: score.ex_score, - }, - lamp, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"ddr:SP" | "ddr:DP"> = { + comment: null, + game: "ddr", + importType, + timeAchieved, + service: "ARC DDR Ace", + scoreData: { + grade, + percent, + score: score.ex_score, + hitData: { + marvelous: score.judgments.marvelous, + perfect: score.judgments.perfect, + great: score.judgments.great, + good: score.judgments.good, + boo: score.judgments.boo, + miss: score.judgments.miss, + ok: score.judgments.ok, + ng: score.judgments.ng, + }, + hitMeta: { + fast: score.fast, + slow: score.slow, + maxCombo: score.max_combo, + exScore: score.ex_score, + }, + lamp, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; export function ResolveARCDDRLamp(lamp: ARCDDRScore["lamp"]): Lamps["ddr:SP" | "ddr:DP"] { - switch (lamp) { - case "FAIL": - return "FAILED"; - case "CLEAR": - return "CLEAR"; - case "CLEAR_3LIFE": - return "LIFE4"; - case "GOOD_FC": - return "FULL COMBO"; - case "GREAT_FC": - return "GREAT FULL COMBO"; - case "PERFECT_FC": - return "PERFECT FULL COMBO"; - case "MARVELOUS_FC": - return "MARVELOUS FULL COMBO"; - } + switch (lamp) { + case "FAIL": + return "FAILED"; + case "CLEAR": + return "CLEAR"; + case "CLEAR_3LIFE": + return "LIFE4"; + case "GOOD_FC": + return "FULL COMBO"; + case "GREAT_FC": + return "GREAT FULL COMBO"; + case "PERFECT_FC": + return "PERFECT FULL COMBO"; + case "MARVELOUS_FC": + return "MARVELOUS FULL COMBO"; + } - // failsafe - /* istanbul ignore next */ - throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`); + // failsafe + /* istanbul ignore next */ + throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`); } diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts index 7fd97d619..895d040e2 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts @@ -9,38 +9,38 @@ import { ParseArcDDR } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseArcDDR", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, - }); + t.test("Should iterate over the API.", async (t) => { + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); - const res = ParseArcDDR("profile", logger, mockArcAPI); + const res = ParseArcDDR("profile", logger, mockArcAPI); - t.equal(res.game, "ddr"); - t.strictSame(res.context, {}); - t.equal(res.classHandler, null); + t.equal(res.game, "ddr"); + t.strictSame(res.context, {}); + t.equal(res.classHandler, null); - const iter = await agta(res.iterable); + const iter = await agta(res.iterable); - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts b/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts index b2b0dc9e5..935e7a6e5 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts @@ -6,21 +6,21 @@ import { EmptyObject } from "../../../../../utils/types"; import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config"; export function ParseArcDDR( - arcProfileID: string, - logger: KtLogger, - fetch = nodeFetch + arcProfileID: string, + logger: KtLogger, + fetch = nodeFetch ): ParserFunctionReturns { - return { - iterable: TraverseKaiAPI( - ARC_API_URL, - // DDR Ace. - `/api/v1/ddr/16/player_bests?profile_id=${arcProfileID}`, - ARC_AUTH_TOKEN, - logger, - fetch - ), - context: {}, - classHandler: null, - game: "ddr", - }; + return { + iterable: TraverseKaiAPI( + ARC_API_URL, + // DDR Ace. + `/api/v1/ddr/16/player_bests?profile_id=${arcProfileID}`, + ARC_AUTH_TOKEN, + logger, + fetch + ), + context: {}, + classHandler: null, + game: "ddr", + }; } diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/types.ts b/server/src/lib/score-import/import-types/api/arc-ddr/types.ts index feae029c7..b34476c03 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/types.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/types.ts @@ -1,22 +1,22 @@ import { integer } from "tachi-common"; export interface ARCDDRScore { - chart_id: string; - lamp: "MARVELOUS_FC" | "PERFECT_FC" | "GREAT_FC" | "GOOD_FC" | "CLEAR_3LIFE" | "CLEAR" | "FAIL"; - score: integer; - ex_score: integer; - max_combo: integer; - judgments: { - marvelous: integer; - perfect: integer; - great: integer; - good: integer; - boo: integer; - miss: integer; - ok: integer; - ng: integer; - }; - fast: integer; - slow: integer; - timestamp: string; + chart_id: string; + lamp: "MARVELOUS_FC" | "PERFECT_FC" | "GREAT_FC" | "GOOD_FC" | "CLEAR_3LIFE" | "CLEAR" | "FAIL"; + score: integer; + ex_score: integer; + max_combo: integer; + judgments: { + marvelous: integer; + perfect: integer; + great: integer; + good: integer; + boo: integer; + miss: integer; + ok: integer; + ng: integer; + }; + fast: integer; + slow: integer; + timestamp: string; } diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts index 479317e30..01a37ed3c 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts @@ -9,139 +9,139 @@ import { CreateArcIIDXClassHandler } from "./class-handler"; const logger = CreateLogCtx(__filename); t.test("#CreateArcIIDXClassHandler", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a working ClassHandler.", async (t) => { - const mockFetch = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { - _items: [ - { - sp: { - rank: "中伝", - }, - dp: { - rank: "ニ段", - }, - }, - ], - }, - }); + t.test("Should return a working ClassHandler.", async (t) => { + const mockFetch = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { + _items: [ + { + sp: { + rank: "中伝", + }, + dp: { + rank: "ニ段", + }, + }, + ], + }, + }); - const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); + const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); - t.equal(fn.length, 5); + t.equal(fn.length, 5); - // its not async but typescript complains - const res = await fn("iidx", "SP", 1, {}, logger); + // its not async but typescript complains + const res = await fn("iidx", "SP", 1, {}, logger); - t.equal(res!.dan, IIDXClasses.CHUUDEN); + t.equal(res!.dan, IIDXClasses.CHUUDEN); - const res2 = await fn("iidx", "DP", 1, {}, logger); - t.equal(res2!.dan, IIDXClasses.DAN_2); + const res2 = await fn("iidx", "DP", 1, {}, logger); + t.equal(res2!.dan, IIDXClasses.DAN_2); - t.end(); - }); + t.end(); + }); - t.test("Should handle null.", async (t) => { - const mockFetch = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { - _items: [ - { - sp: { - rank: null, - }, - dp: { - rank: null, - }, - }, - ], - }, - }); + t.test("Should handle null.", async (t) => { + const mockFetch = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { + _items: [ + { + sp: { + rank: null, + }, + dp: { + rank: null, + }, + }, + ], + }, + }); - const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); + const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); - t.equal(fn.length, 5); + t.equal(fn.length, 5); - const res = await fn("iidx", "SP", 1, {}, logger); + const res = await fn("iidx", "SP", 1, {}, logger); - t.strictSame(res, {}); + t.strictSame(res, {}); - const res2 = await fn("iidx", "DP", 1, {}, logger); - t.strictSame(res2, {}); + const res2 = await fn("iidx", "DP", 1, {}, logger); + t.strictSame(res2, {}); - t.end(); - }); + t.end(); + }); - t.test("Should handle undefined.", async (t) => { - const mockFetch = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { - _items: [{}], - }, - }); + t.test("Should handle undefined.", async (t) => { + const mockFetch = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { + _items: [{}], + }, + }); - const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); + const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); - t.equal(fn.length, 5); + t.equal(fn.length, 5); - const res = await fn("iidx", "SP", 1, {}, logger); + const res = await fn("iidx", "SP", 1, {}, logger); - t.strictSame(res, {}); + t.strictSame(res, {}); - const res2 = await fn("iidx", "DP", 1, {}, logger); - t.strictSame(res2, {}); + const res2 = await fn("iidx", "DP", 1, {}, logger); + t.strictSame(res2, {}); - t.end(); - }); + t.end(); + }); - t.test("Should handle invalid classes.", async (t) => { - const mockFetch = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { - _items: [ - { - sp: { - rank: "invalid", - }, - }, - ], - }, - }); + t.test("Should handle invalid classes.", async (t) => { + const mockFetch = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { + _items: [ + { + sp: { + rank: "invalid", + }, + }, + ], + }, + }); - const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); + const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); - t.equal(fn.length, 5); + t.equal(fn.length, 5); - const res = await fn("iidx", "SP", 1, {}, logger); + const res = await fn("iidx", "SP", 1, {}, logger); - t.strictSame(res, {}); + t.strictSame(res, {}); - t.end(); - }); + t.end(); + }); - t.test("Should trigger failsafe if invalid playtype is used.", async (t) => { - const mockFetch = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { - _items: [ - { - sp: { - rank: "中伝", - }, - }, - ], - }, - }); + t.test("Should trigger failsafe if invalid playtype is used.", async (t) => { + const mockFetch = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/profiles?_id=profile": { + _items: [ + { + sp: { + rank: "中伝", + }, + }, + ], + }, + }); - const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); + const fn = await CreateArcIIDXClassHandler("profile", "token", mockFetch); - t.equal(fn.length, 5); + t.equal(fn.length, 5); - const res = await fn("iidx", "14K", 1, {}, logger); + const res = await fn("iidx", "14K", 1, {}, logger); - t.strictSame(res, {}); + t.strictSame(res, {}); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts index 99ea373b1..1e1826e73 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts @@ -5,86 +5,86 @@ import { ARC_API_URL } from "../../../../setup/config"; import { ClassHandler } from "../../../framework/user-game-stats/types"; export async function CreateArcIIDXClassHandler( - profileID: string, - token: string, - fetch = nodeFetch + profileID: string, + token: string, + fetch = nodeFetch ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let json: any; - let err: Error | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let json: any; + let err: Error | undefined; - // SP and DP dans are located in the same place, - // fetch once, then return a function that traverses this data. - try { - const res = await fetch(`${ARC_API_URL}/api/v1/iidx/27/profiles?_id=${profileID}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + // SP and DP dans are located in the same place, + // fetch once, then return a function that traverses this data. + try { + const res = await fetch(`${ARC_API_URL}/api/v1/iidx/27/profiles?_id=${profileID}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); - json = await res.json(); - } catch (e) { - err = e; - } + json = await res.json(); + } catch (e) { + err = e; + } - return (game, playtype, userID, ratings, logger) => { - /* istanbul ignore next */ - if (err) { - logger.error(`An error occured while updating classes for ${ARC_API_URL}.`, { err }); - return {}; - } + return (game, playtype, userID, ratings, logger) => { + /* istanbul ignore next */ + if (err) { + logger.error(`An error occured while updating classes for ${ARC_API_URL}.`, { err }); + return {}; + } - // we're just going to lazily path directly towards the rank. - // if ARC sends us an unexpected JSON structure or whatever - // we wont crash. - let arcClass: string | undefined | null; + // we're just going to lazily path directly towards the rank. + // if ARC sends us an unexpected JSON structure or whatever + // we wont crash. + let arcClass: string | undefined | null; - if (playtype === "SP") { - arcClass = json?._items?.[0]?.sp?.rank; - } else if (playtype === "DP") { - arcClass = json?._items?.[0]?.dp?.rank; - } else { - logger.error(`ARCIIDXClassUpdater called with invalid playtype of ${playtype}.`); - return {}; - } + if (playtype === "SP") { + arcClass = json?._items?.[0]?.sp?.rank; + } else if (playtype === "DP") { + arcClass = json?._items?.[0]?.dp?.rank; + } else { + logger.error(`ARCIIDXClassUpdater called with invalid playtype of ${playtype}.`); + return {}; + } - if (arcClass === null) { - return {}; - } + if (arcClass === null) { + return {}; + } - // arc's classes sometimes have trailing/leading spaces. - const trimmedArcClass = (arcClass ?? "").trim(); + // arc's classes sometimes have trailing/leading spaces. + const trimmedArcClass = (arcClass ?? "").trim(); - if (!HasOwnProperty(ARCClasses, trimmedArcClass)) { - logger.warn(`Invalid dan sent from ARC ${trimmedArcClass}. Ignoring.`); - return {}; - } + if (!HasOwnProperty(ARCClasses, trimmedArcClass)) { + logger.warn(`Invalid dan sent from ARC ${trimmedArcClass}. Ignoring.`); + return {}; + } - return { - dan: ARCClasses[trimmedArcClass], - }; - }; + return { + dan: ARCClasses[trimmedArcClass], + }; + }; } const ARCClasses = { - 皆伝: IIDXClasses.KAIDEN, - 中伝: IIDXClasses.CHUUDEN, - 十段: IIDXClasses.DAN_10, - 九段: IIDXClasses.DAN_9, - 八段: IIDXClasses.DAN_8, - 七段: IIDXClasses.DAN_7, - 六段: IIDXClasses.DAN_6, - 五段: IIDXClasses.DAN_5, - 四段: IIDXClasses.DAN_4, - 三段: IIDXClasses.DAN_3, - 二段: IIDXClasses.DAN_2, // These two look very similar but they aren't - ニ段: IIDXClasses.DAN_2, // and ARC uses both, from what I can tell. - 初段: IIDXClasses.DAN_1, - 一級: IIDXClasses.KYU_1, - 二級: IIDXClasses.KYU_2, - 三級: IIDXClasses.KYU_3, - 四級: IIDXClasses.KYU_4, - 五級: IIDXClasses.KYU_5, - 六級: IIDXClasses.KYU_6, - 七級: IIDXClasses.KYU_7, + 皆伝: IIDXClasses.KAIDEN, + 中伝: IIDXClasses.CHUUDEN, + 十段: IIDXClasses.DAN_10, + 九段: IIDXClasses.DAN_9, + 八段: IIDXClasses.DAN_8, + 七段: IIDXClasses.DAN_7, + 六段: IIDXClasses.DAN_6, + 五段: IIDXClasses.DAN_5, + 四段: IIDXClasses.DAN_4, + 三段: IIDXClasses.DAN_3, + 二段: IIDXClasses.DAN_2, // These two look very similar but they aren't + ニ段: IIDXClasses.DAN_2, // and ARC uses both, from what I can tell. + 初段: IIDXClasses.DAN_1, + 一級: IIDXClasses.KYU_1, + 二級: IIDXClasses.KYU_2, + 三級: IIDXClasses.KYU_3, + 四級: IIDXClasses.KYU_4, + 五級: IIDXClasses.KYU_5, + 六級: IIDXClasses.KYU_6, + 七級: IIDXClasses.KYU_7, }; diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts index be72c75fc..dbf5385ac 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts @@ -12,84 +12,84 @@ const logger = CreateLogCtx(__filename); const arcScore = GetKTDataJSON("./api-arc/iidx-score.json") as ARCIIDXScore; t.test("#ConvertAPIArcIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should convert a valid score", async (t) => { - const res = await ConvertAPIArcIIDX(arcScore, {}, "api/arc-iidx", logger); + t.test("Should convert a valid score", async (t) => { + const res = await ConvertAPIArcIIDX(arcScore, {}, "api/arc-iidx", logger); - t.hasStrict(res, { - song: Testing511Song, - chart: Testing511SPA, - dryScore: { - comment: null, - game: "iidx", - importType: "api/arc-iidx", - timeAchieved: 1604784681894, - service: "ARC IIDX27", - scoreData: { - grade: "A", - score: 1200, - hitData: {}, - hitMeta: { - bp: 43, - }, - lamp: "HARD CLEAR", - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + song: Testing511Song, + chart: Testing511SPA, + dryScore: { + comment: null, + game: "iidx", + importType: "api/arc-iidx", + timeAchieved: 1604784681894, + service: "ARC IIDX27", + scoreData: { + grade: "A", + score: 1200, + hitData: {}, + hitMeta: { + bp: 43, + }, + lamp: "HARD CLEAR", + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw on an invalid score", (t) => { - t.rejects( - () => - ConvertAPIArcIIDX( - deepmerge(arcScore, { ex_score: "foo" }), - {}, - "api/arc-iidx", - logger - ), - { - message: /Invalid ARC Score:/iu, - } as any - ); + t.test("Should throw on an invalid score", (t) => { + t.rejects( + () => + ConvertAPIArcIIDX( + deepmerge(arcScore, { ex_score: "foo" }), + {}, + "api/arc-iidx", + logger + ), + { + message: /Invalid ARC Score:/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw on no chart", (t) => { - t.rejects( - () => - ConvertAPIArcIIDX( - deepmerge(arcScore, { chart_id: "foo" }), - {}, - "api/arc-iidx", - logger - ), - { - message: /Could not find chart/iu, - } as any - ); + t.test("Should throw on no chart", (t) => { + t.rejects( + () => + ConvertAPIArcIIDX( + deepmerge(arcScore, { chart_id: "foo" }), + {}, + "api/arc-iidx", + logger + ), + { + message: /Could not find chart/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ResolveARCIIDXLamp", (t) => { - t.equal(ResolveARCIIDXLamp("NO_PLAY"), "NO PLAY"); - t.equal(ResolveARCIIDXLamp("FAILED"), "FAILED"); - t.equal(ResolveARCIIDXLamp("ASSIST_CLEAR"), "ASSIST CLEAR"); - t.equal(ResolveARCIIDXLamp("EASY_CLEAR"), "EASY CLEAR"); - t.equal(ResolveARCIIDXLamp("CLEAR"), "CLEAR"); - t.equal(ResolveARCIIDXLamp("HARD_CLEAR"), "HARD CLEAR"); - t.equal(ResolveARCIIDXLamp("EX_HARD_CLEAR"), "EX HARD CLEAR"); - t.equal(ResolveARCIIDXLamp("FULL_COMBO"), "FULL COMBO"); + t.equal(ResolveARCIIDXLamp("NO_PLAY"), "NO PLAY"); + t.equal(ResolveARCIIDXLamp("FAILED"), "FAILED"); + t.equal(ResolveARCIIDXLamp("ASSIST_CLEAR"), "ASSIST CLEAR"); + t.equal(ResolveARCIIDXLamp("EASY_CLEAR"), "EASY CLEAR"); + t.equal(ResolveARCIIDXLamp("CLEAR"), "CLEAR"); + t.equal(ResolveARCIIDXLamp("HARD_CLEAR"), "HARD CLEAR"); + t.equal(ResolveARCIIDXLamp("EX_HARD_CLEAR"), "EX HARD CLEAR"); + t.equal(ResolveARCIIDXLamp("FULL_COMBO"), "FULL COMBO"); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts b/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts index dfc704832..e0eb8cf05 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts @@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types"; import { ConverterFunction } from "../../common/types"; import p, { PrudenceSchema } from "prudence"; import { - InvalidScoreFailure, - KTDataNotFoundFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { FormatPrError } from "../../../../../utils/prudence"; import { ARCIIDXScore } from "./types"; @@ -11,106 +11,106 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts"; import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs"; import { DryScore } from "../../../framework/common/types"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { Lamps } from "tachi-common"; // There's a bunch of other useless fields but we don't care const PR_ArcIIDXScore: PrudenceSchema = { - chart_id: "string", - lamp: p.isIn( - "FULL_COMBO", - "EX_HARD_CLEAR", - "HARD_CLEAR", - "CLEAR", - "EASY_CLEAR", - "ASSIST_CLEAR", - "FAILED", - "NO_PLAY" - ), - ex_score: p.isPositiveInteger, - miss_count: p.nullable(p.isPositiveInteger), - timestamp: "string", + chart_id: "string", + lamp: p.isIn( + "FULL_COMBO", + "EX_HARD_CLEAR", + "HARD_CLEAR", + "CLEAR", + "EASY_CLEAR", + "ASSIST_CLEAR", + "FAILED", + "NO_PLAY" + ), + ex_score: p.isPositiveInteger, + miss_count: p.nullable(p.isPositiveInteger), + timestamp: "string", }; export const ConvertAPIArcIIDX: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const err = p(data, PR_ArcIIDXScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); + const err = p(data, PR_ArcIIDXScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); - if (err) { - throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); - } + if (err) { + throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); + } - // confirmed by Prudence above. - const score = data as ARCIIDXScore; + // confirmed by Prudence above. + const score = data as ARCIIDXScore; - const chart = await FindChartOnARCID("iidx", score.chart_id); + const chart = await FindChartOnARCID("iidx", score.chart_id); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with chart_id ${score.chart_id}.`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with chart_id ${score.chart_id}.`, + importType, + data, + context + ); + } - const song = await FindSongOnIDGuaranteed("iidx", chart.songID, logger); + const song = await FindSongOnIDGuaranteed("iidx", chart.songID, logger); - const { grade, percent } = GenericGetGradeAndPercent("iidx", score.ex_score, chart); + const { grade, percent } = GenericGetGradeAndPercent("iidx", score.ex_score, chart); - const timeAchieved = ParseDateFromString(score.timestamp); + const timeAchieved = ParseDateFromString(score.timestamp); - const lamp = ResolveARCIIDXLamp(score.lamp); + const lamp = ResolveARCIIDXLamp(score.lamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - comment: null, - game: "iidx", - importType, - timeAchieved, - service: "ARC IIDX27", - scoreData: { - grade, - percent, - score: score.ex_score, - hitData: {}, - hitMeta: { - bp: score.miss_count, - }, - lamp, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + comment: null, + game: "iidx", + importType, + timeAchieved, + service: "ARC IIDX27", + scoreData: { + grade, + percent, + score: score.ex_score, + hitData: {}, + hitMeta: { + bp: score.miss_count, + }, + lamp, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; export function ResolveARCIIDXLamp(lamp: ARCIIDXScore["lamp"]): Lamps["iidx:SP" | "iidx:DP"] { - switch (lamp) { - case "NO_PLAY": - return "NO PLAY"; - case "FAILED": - return "FAILED"; - case "ASSIST_CLEAR": - return "ASSIST CLEAR"; - case "EASY_CLEAR": - return "EASY CLEAR"; - case "CLEAR": - return "CLEAR"; - case "HARD_CLEAR": - return "HARD CLEAR"; - case "EX_HARD_CLEAR": - return "EX HARD CLEAR"; - case "FULL_COMBO": - return "FULL COMBO"; - } + switch (lamp) { + case "NO_PLAY": + return "NO PLAY"; + case "FAILED": + return "FAILED"; + case "ASSIST_CLEAR": + return "ASSIST CLEAR"; + case "EASY_CLEAR": + return "EASY CLEAR"; + case "CLEAR": + return "CLEAR"; + case "HARD_CLEAR": + return "HARD CLEAR"; + case "EX_HARD_CLEAR": + return "EX HARD CLEAR"; + case "FULL_COMBO": + return "FULL COMBO"; + } - // theoretically impossible, but a failsafe regardless. - /* istanbul ignore next */ - throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`); + // theoretically impossible, but a failsafe regardless. + /* istanbul ignore next */ + throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not resolve.`); } diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts index 9d6de57fc..8afc9444a 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts @@ -9,37 +9,37 @@ import { ParseArcIIDX } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseArcIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, - }); + t.test("Should iterate over the API.", async (t) => { + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); - const res = await ParseArcIIDX("profile", logger, mockArcAPI); + const res = await ParseArcIIDX("profile", logger, mockArcAPI); - t.equal(res.game, "iidx"); - t.strictSame(res.context, {}); + t.equal(res.game, "iidx"); + t.strictSame(res.context, {}); - const iter = await agta(res.iterable); + const iter = await agta(res.iterable); - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts b/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts index 3933c64ae..8f606eb6e 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts @@ -7,21 +7,21 @@ import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config"; import { CreateArcIIDXClassHandler } from "./class-handler"; export async function ParseArcIIDX( - arcProfileID: string, - logger: KtLogger, - fetch = nodeFetch + arcProfileID: string, + logger: KtLogger, + fetch = nodeFetch ): Promise> { - return { - iterable: TraverseKaiAPI( - ARC_API_URL, - // HEROIC VERSE - `/api/v1/iidx/27/player_bests?profile_id=${arcProfileID}`, - ARC_AUTH_TOKEN, - logger, - fetch - ), - context: {}, - classHandler: await CreateArcIIDXClassHandler(arcProfileID, ARC_AUTH_TOKEN), - game: "iidx", - }; + return { + iterable: TraverseKaiAPI( + ARC_API_URL, + // HEROIC VERSE + `/api/v1/iidx/27/player_bests?profile_id=${arcProfileID}`, + ARC_AUTH_TOKEN, + logger, + fetch + ), + context: {}, + classHandler: await CreateArcIIDXClassHandler(arcProfileID, ARC_AUTH_TOKEN), + game: "iidx", + }; } diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/types.ts b/server/src/lib/score-import/import-types/api/arc-iidx/types.ts index f6d8d1c04..a6a17a12a 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/types.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/types.ts @@ -1,17 +1,17 @@ import { integer } from "tachi-common"; export interface ARCIIDXScore { - chart_id: string; - lamp: - | "NO_PLAY" - | "FAILED" - | "ASSIST_CLEAR" - | "EASY_CLEAR" - | "CLEAR" - | "HARD_CLEAR" - | "EX_HARD_CLEAR" - | "FULL_COMBO"; - ex_score: integer; - miss_count: integer | null; - timestamp: string; + chart_id: string; + lamp: + | "NO_PLAY" + | "FAILED" + | "ASSIST_CLEAR" + | "EASY_CLEAR" + | "CLEAR" + | "HARD_CLEAR" + | "EX_HARD_CLEAR" + | "FULL_COMBO"; + ex_score: integer; + miss_count: integer | null; + timestamp: string; } diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts index 78c099a53..12ddcb639 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts @@ -2,9 +2,9 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; import ResetDBState from "../../../../../test-utils/resets"; import { - GetKTDataJSON, - TestingAlbidaADV, - TestingSDVXAlbidaSong, + GetKTDataJSON, + TestingAlbidaADV, + TestingSDVXAlbidaSong, } from "../../../../../test-utils/test-data"; import CreateLogCtx from "../../../../logger/logger"; import { ConvertAPIArcSDVX, ResolveARCSDVXLamp } from "./converter"; @@ -16,89 +16,89 @@ const logger = CreateLogCtx(__filename); const arcScore = GetKTDataJSON("./api-arc/sdvx-score.json") as ARCSDVXScore; t.test("#ConvertAPIArcSDVX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should convert a valid score", async (t) => { - const res = await ConvertAPIArcSDVX(arcScore, {}, "api/arc-sdvx", logger); + t.test("Should convert a valid score", async (t) => { + const res = await ConvertAPIArcSDVX(arcScore, {}, "api/arc-sdvx", logger); - t.hasStrict(res, { - song: TestingSDVXAlbidaSong, - chart: TestingAlbidaADV, - dryScore: { - comment: null, - game: "sdvx", - importType: "api/arc-sdvx", - timeAchieved: 1567685885077, - service: "ARC SDVX V", - scoreData: { - grade: "AA", - percent: 93.125, - score: 9312500, - hitData: { - critical: 946, - near: 45, - miss: 49, - }, - hitMeta: { - btnRate: 89.5, - holdRate: 92.5, - laserRate: 96.5, - maxCombo: 184, - }, - lamp: "CLEAR", - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + song: TestingSDVXAlbidaSong, + chart: TestingAlbidaADV, + dryScore: { + comment: null, + game: "sdvx", + importType: "api/arc-sdvx", + timeAchieved: 1567685885077, + service: "ARC SDVX V", + scoreData: { + grade: "AA", + percent: 93.125, + score: 9312500, + hitData: { + critical: 946, + near: 45, + miss: 49, + }, + hitMeta: { + btnRate: 89.5, + holdRate: 92.5, + laserRate: 96.5, + maxCombo: 184, + }, + lamp: "CLEAR", + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw on an invalid score", (t) => { - t.rejects( - () => - ConvertAPIArcSDVX( - deepmerge(arcScore, { score: "foo" }), - {}, - "api/arc-sdvx", - logger - ), - { - message: /Invalid ARC Score:/iu, - } as any - ); + t.test("Should throw on an invalid score", (t) => { + t.rejects( + () => + ConvertAPIArcSDVX( + deepmerge(arcScore, { score: "foo" }), + {}, + "api/arc-sdvx", + logger + ), + { + message: /Invalid ARC Score:/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw on no chart", (t) => { - t.rejects( - () => - ConvertAPIArcSDVX( - deepmerge(arcScore, { chart_id: "foo" }), - {}, - "api/arc-sdvx", - logger - ), - { - message: /Could not find chart/iu, - } as any - ); + t.test("Should throw on no chart", (t) => { + t.rejects( + () => + ConvertAPIArcSDVX( + deepmerge(arcScore, { chart_id: "foo" }), + {}, + "api/arc-sdvx", + logger + ), + { + message: /Could not find chart/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ResolveARCSDVXLamp", (t) => { - t.equal(ResolveARCSDVXLamp("PLAY"), "FAILED"); - t.equal(ResolveARCSDVXLamp("CLEAR"), "CLEAR"); - t.equal(ResolveARCSDVXLamp("HC"), "EXCESSIVE CLEAR"); - t.equal(ResolveARCSDVXLamp("UC"), "ULTIMATE CHAIN"); - t.equal(ResolveARCSDVXLamp("PUC"), "PERFECT ULTIMATE CHAIN"); + t.equal(ResolveARCSDVXLamp("PLAY"), "FAILED"); + t.equal(ResolveARCSDVXLamp("CLEAR"), "CLEAR"); + t.equal(ResolveARCSDVXLamp("HC"), "EXCESSIVE CLEAR"); + t.equal(ResolveARCSDVXLamp("UC"), "ULTIMATE CHAIN"); + t.equal(ResolveARCSDVXLamp("PUC"), "PERFECT ULTIMATE CHAIN"); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts index ef7fc687f..446ce1782 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts @@ -2,8 +2,8 @@ import { EmptyObject } from "../../../../../utils/types"; import { ConverterFunction } from "../../common/types"; import p, { PrudenceSchema } from "prudence"; import { - InvalidScoreFailure, - KTDataNotFoundFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { FormatPrError } from "../../../../../utils/prudence"; import { ARCSDVXScore } from "./types"; @@ -11,86 +11,86 @@ import { FindChartOnARCID } from "../../../../../utils/queries/charts"; import { FindSongOnIDGuaranteed } from "../../../../../utils/queries/songs"; import { DryScore } from "../../../framework/common/types"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { Lamps } from "tachi-common"; // There's a bunch of other useless fields but we don't care const PR_ArcSDVXScore: PrudenceSchema = { - chart_id: "string", - score: p.isBoundedInteger(0, 10_000_000), - lamp: p.isIn("UC", "PUC", "HC", "PLAY", "CLEAR"), - btn_rate: p.isBetween(0, 100), - long_rate: p.isBetween(0, 100), - vol_rate: p.isBetween(0, 100), - critical: p.isPositiveInteger, - near: p.isPositiveInteger, - error: p.isPositiveInteger, - timestamp: "string", + chart_id: "string", + score: p.isBoundedInteger(0, 10_000_000), + lamp: p.isIn("UC", "PUC", "HC", "PLAY", "CLEAR"), + btn_rate: p.isBetween(0, 100), + long_rate: p.isBetween(0, 100), + vol_rate: p.isBetween(0, 100), + critical: p.isPositiveInteger, + near: p.isPositiveInteger, + error: p.isPositiveInteger, + timestamp: "string", }; export const ConvertAPIArcSDVX: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const err = p(data, PR_ArcSDVXScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); + const err = p(data, PR_ArcSDVXScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); - if (err) { - throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); - } + if (err) { + throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); + } - // confirmed by Prudence above. - const score = data as ARCSDVXScore; + // confirmed by Prudence above. + const score = data as ARCSDVXScore; - const chart = await FindChartOnARCID("sdvx", score.chart_id); + const chart = await FindChartOnARCID("sdvx", score.chart_id); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with chart_id ${score.chart_id}.`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with chart_id ${score.chart_id}.`, + importType, + data, + context + ); + } - const song = await FindSongOnIDGuaranteed("sdvx", chart.songID, logger); + const song = await FindSongOnIDGuaranteed("sdvx", chart.songID, logger); - const { grade, percent } = GenericGetGradeAndPercent("sdvx", score.score, chart); + const { grade, percent } = GenericGetGradeAndPercent("sdvx", score.score, chart); - const timeAchieved = ParseDateFromString(score.timestamp); + const timeAchieved = ParseDateFromString(score.timestamp); - const lamp = ResolveARCSDVXLamp(score.lamp); + const lamp = ResolveARCSDVXLamp(score.lamp); - const dryScore: DryScore<"sdvx:Single"> = { - comment: null, - game: "sdvx", - importType, - timeAchieved, - service: "ARC SDVX V", - scoreData: { - grade, - percent, - score: score.score, - hitData: { - critical: score.critical, - near: score.near, - miss: score.error, - }, - hitMeta: { - btnRate: score.btn_rate, - holdRate: score.long_rate, - laserRate: score.vol_rate, - maxCombo: score.max_chain, - }, - lamp, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"sdvx:Single"> = { + comment: null, + game: "sdvx", + importType, + timeAchieved, + service: "ARC SDVX V", + scoreData: { + grade, + percent, + score: score.score, + hitData: { + critical: score.critical, + near: score.near, + miss: score.error, + }, + hitMeta: { + btnRate: score.btn_rate, + holdRate: score.long_rate, + laserRate: score.vol_rate, + maxCombo: score.max_chain, + }, + lamp, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; /** @@ -98,18 +98,18 @@ export const ConvertAPIArcSDVX: ConverterFunction = async * This function takes the score and lamp and rederives the lamp. */ export function ResolveARCSDVXLamp(lamp: ARCSDVXScore["lamp"]): Lamps["sdvx:Single"] { - switch (lamp) { - case "PLAY": - return "FAILED"; - case "CLEAR": - return "CLEAR"; - case "HC": - return "EXCESSIVE CLEAR"; - case "UC": - return "ULTIMATE CHAIN"; - case "PUC": - return "PERFECT ULTIMATE CHAIN"; - } + switch (lamp) { + case "PLAY": + return "FAILED"; + case "CLEAR": + return "CLEAR"; + case "HC": + return "EXCESSIVE CLEAR"; + case "UC": + return "ULTIMATE CHAIN"; + case "PUC": + return "PERFECT ULTIMATE CHAIN"; + } - throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not convert.`); + throw new InvalidScoreFailure(`Invalid lamp ${lamp} - Could not convert.`); } diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts index 780497ca2..ac6b491f5 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts @@ -9,38 +9,38 @@ import { ParseArcSDVX } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseArcSDVX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, - }); + t.test("Should iterate over the API.", async (t) => { + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); - const res = ParseArcSDVX("profile", logger, mockArcAPI); + const res = ParseArcSDVX("profile", logger, mockArcAPI); - t.equal(res.game, "sdvx"); - t.strictSame(res.context, {}); - t.equal(res.classHandler, null); + t.equal(res.game, "sdvx"); + t.strictSame(res.context, {}); + t.equal(res.classHandler, null); - const iter = await agta(res.iterable); + const iter = await agta(res.iterable); - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts index 37f5b1d99..6a6f4e9c1 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts @@ -6,21 +6,21 @@ import { EmptyObject } from "../../../../../utils/types"; import { ARC_API_URL, ARC_AUTH_TOKEN } from "../../../../setup/config"; export function ParseArcSDVX( - arcProfileID: string, - logger: KtLogger, - fetch = nodeFetch + arcProfileID: string, + logger: KtLogger, + fetch = nodeFetch ): ParserFunctionReturns { - return { - iterable: TraverseKaiAPI( - ARC_API_URL, - // VIVID WAVE. - `/api/v1/sdvx/5/player_bests?profile_id=${arcProfileID}`, - ARC_AUTH_TOKEN, - logger, - fetch - ), - context: {}, - classHandler: null, - game: "sdvx", - }; + return { + iterable: TraverseKaiAPI( + ARC_API_URL, + // VIVID WAVE. + `/api/v1/sdvx/5/player_bests?profile_id=${arcProfileID}`, + ARC_AUTH_TOKEN, + logger, + fetch + ), + context: {}, + classHandler: null, + game: "sdvx", + }; } diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts index c62eae190..14928d5c6 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts @@ -1,15 +1,15 @@ import { integer } from "tachi-common"; export interface ARCSDVXScore { - chart_id: string; - score: integer; - lamp: "CLEAR" | "UC" | "PUC" | "PLAY" | "HC"; - btn_rate: number; - long_rate: number; - vol_rate: number; - critical: integer; - near: integer; - error: integer; - max_chain: integer; - timestamp: string; + chart_id: string; + score: integer; + lamp: "CLEAR" | "UC" | "PUC" | "PLAY" | "HC"; + btn_rate: number; + long_rate: number; + vol_rate: number; + critical: integer; + near: integer; + error: integer; + max_chain: integer; + timestamp: string; } diff --git a/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts b/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts index 2fb4a4c81..81f9ca42b 100644 --- a/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts @@ -3,5 +3,5 @@ import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; import { KaiAuthDocument } from "tachi-common"; export function ParseEagIIDX(authDoc: KaiAuthDocument, logger: KtLogger) { - return ParseKaiIIDX("EAG", authDoc, logger); + return ParseKaiIIDX("EAG", authDoc, logger); } diff --git a/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts index 9b6971855..a94fbb848 100644 --- a/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts @@ -3,5 +3,5 @@ import { KaiAuthDocument } from "tachi-common"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; export function ParseEagSDVX(authDoc: KaiAuthDocument, logger: KtLogger) { - return ParseKaiSDVX("EAG", authDoc, logger); + return ParseKaiSDVX("EAG", authDoc, logger); } diff --git a/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts b/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts index f57eb2c95..16c628aa2 100644 --- a/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts @@ -3,5 +3,5 @@ import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; import { KaiAuthDocument } from "tachi-common"; export function ParseFloIIDX(authDoc: KaiAuthDocument, logger: KtLogger) { - return ParseKaiIIDX("FLO", authDoc, logger); + return ParseKaiIIDX("FLO", authDoc, logger); } diff --git a/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts index 22e39f92e..4be799276 100644 --- a/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts @@ -3,5 +3,5 @@ import { KaiAuthDocument } from "tachi-common"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; export function ParseFloSDVX(authDoc: KaiAuthDocument, logger: KtLogger) { - return ParseKaiSDVX("FLO", authDoc, logger); + return ParseKaiSDVX("FLO", authDoc, logger); } diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts index 2a321583d..6f9a7ae08 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts @@ -2,9 +2,9 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../../test-utils/close-connections"; import ResetDBState from "../../../../../../test-utils/resets"; import { - GetKTDataJSON, - Testing511Song, - Testing511SPA, + GetKTDataJSON, + Testing511Song, + Testing511SPA, } from "../../../../../../test-utils/test-data"; import CreateLogCtx from "../../../../../logger/logger"; import { ConvertAPIKaiIIDX } from "./converter"; @@ -16,84 +16,84 @@ const logger = CreateLogCtx(__filename); const iidxScore = GetKTDataJSON("./api-kai/iidx-score.json"); t.test("#ConvertAPIKaiIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a dryScore on valid input.", async (t) => { - const res = await ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger); + t.test("Should return a dryScore on valid input.", async (t) => { + const res = await ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger); - t.hasStrict(res, { - song: Testing511Song, - chart: Testing511SPA, - dryScore: { - comment: null, - game: "iidx", - importType: "api/flo-iidx", - timeAchieved: 1604171450000, - service: "FLO", - scoreData: { - grade: "MAX-", - // percent: 99.87277353689568 floating point, - score: 1570, - lamp: "HARD CLEAR", - hitData: {}, - hitMeta: { - fast: null, - slow: null, - bp: 24, - }, - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + song: Testing511Song, + chart: Testing511SPA, + dryScore: { + comment: null, + game: "iidx", + importType: "api/flo-iidx", + timeAchieved: 1604171450000, + service: "FLO", + scoreData: { + grade: "MAX-", + // percent: 99.87277353689568 floating point, + score: 1570, + lamp: "HARD CLEAR", + hitData: {}, + hitMeta: { + fast: null, + slow: null, + bp: 24, + }, + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw KTDataNotFound on unknown chart", (t) => { - t.rejects( - () => - ConvertAPIKaiIIDX( - deepmerge(iidxScore, { music_id: 0 }), - { service: "FLO" }, - "api/flo-iidx", - logger - ), - { - message: /Could not find chart with songID 0 \(SP ANOTHER - Version 26\)/u, - } as any - ); + t.test("Should throw KTDataNotFound on unknown chart", (t) => { + t.rejects( + () => + ConvertAPIKaiIIDX( + deepmerge(iidxScore, { music_id: 0 }), + { service: "FLO" }, + "api/flo-iidx", + logger + ), + { + message: /Could not find chart with songID 0 \(SP ANOTHER - Version 26\)/u, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw InvalidScoreFailure on invalid score", (t) => { - t.rejects( - () => - ConvertAPIKaiIIDX( - deepmerge(iidxScore, { music_id: "foo" }), - { service: "FLO" }, - "api/flo-iidx", - logger - ), - { - message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu, - } as any - ); + t.test("Should throw InvalidScoreFailure on invalid score", (t) => { + t.rejects( + () => + ConvertAPIKaiIIDX( + deepmerge(iidxScore, { music_id: "foo" }), + { service: "FLO" }, + "api/flo-iidx", + logger + ), + { + message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw InternalFailure on song-chart desync", async (t) => { - await db.songs.iidx.remove({}); + t.test("Should throw InternalFailure on song-chart desync", async (t) => { + await db.songs.iidx.remove({}); - t.rejects(() => ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger), { - message: /Song-Chart desync/u, - } as any); + t.rejects(() => ConvertAPIKaiIIDX(iidxScore, { service: "FLO" }, "api/flo-iidx", logger), { + message: /Song-Chart desync/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts index 0c575566f..ed1f5c698 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts @@ -2,121 +2,121 @@ import { FindIIDXChartOnInGameIDVersion } from "../../../../../../utils/queries/ import { KaiContext, KaiIIDXScore } from "../types"; import p from "prudence"; import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../../framework/common/converter-failures"; import { FormatPrError } from "../../../../../../utils/prudence"; import { FindSongOnID } from "../../../../../../utils/queries/songs"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../../framework/common/score-utils"; import { Lamps } from "tachi-common"; import { ConverterFunction } from "../../types"; import { DryScore } from "../../../../framework/common/types"; const PR_KaiIIDXScore = { - music_id: p.isPositiveInteger, - play_style: p.isIn("SINGLE", "DOUBLE"), - difficulty: p.isIn("BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"), - version_played: p.isBoundedInteger(9, 28), - lamp: p.isBoundedInteger(0, 7), - ex_score: p.isPositiveInteger, - miss_count: p.or(p.isPositiveInteger, p.is(-1)), - fast_count: p.nullable(p.isPositiveInteger), - slow_count: p.nullable(p.isPositiveInteger), - timestamp: "string", + music_id: p.isPositiveInteger, + play_style: p.isIn("SINGLE", "DOUBLE"), + difficulty: p.isIn("BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"), + version_played: p.isBoundedInteger(9, 28), + lamp: p.isBoundedInteger(0, 7), + ex_score: p.isPositiveInteger, + miss_count: p.or(p.isPositiveInteger, p.is(-1)), + fast_count: p.nullable(p.isPositiveInteger), + slow_count: p.nullable(p.isPositiveInteger), + timestamp: "string", }; function ResolveKaiLamp(lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): Lamps["iidx:SP" | "iidx:DP"] { - switch (lamp) { - case 0: - return "NO PLAY"; - case 1: - return "FAILED"; - case 3: - return "EASY CLEAR"; - case 2: - return "ASSIST CLEAR"; - case 4: - return "CLEAR"; - case 5: - return "HARD CLEAR"; - case 6: - return "EX HARD CLEAR"; - case 7: - return "FULL COMBO"; - } + switch (lamp) { + case 0: + return "NO PLAY"; + case 1: + return "FAILED"; + case 3: + return "EASY CLEAR"; + case 2: + return "ASSIST CLEAR"; + case 4: + return "CLEAR"; + case 5: + return "HARD CLEAR"; + case 6: + return "EX HARD CLEAR"; + case 7: + return "FULL COMBO"; + } } export const ConvertAPIKaiIIDX: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const err = p(data, PR_KaiIIDXScore, {}, { allowExcessKeys: true }); + const err = p(data, PR_KaiIIDXScore, {}, { allowExcessKeys: true }); - if (err) { - throw new InvalidScoreFailure(FormatPrError(err)); - } + if (err) { + throw new InvalidScoreFailure(FormatPrError(err)); + } - // prudence checks this above. - const score = data as KaiIIDXScore; + // prudence checks this above. + const score = data as KaiIIDXScore; - const playtype = score.play_style === "SINGLE" ? "SP" : "DP"; + const playtype = score.play_style === "SINGLE" ? "SP" : "DP"; - const chart = await FindIIDXChartOnInGameIDVersion( - score.music_id, - playtype, - score.difficulty, - // they send integers like 25, 27 - this will convert to our versions. - score.version_played.toString() - ); + const chart = await FindIIDXChartOnInGameIDVersion( + score.music_id, + playtype, + score.difficulty, + // they send integers like 25, 27 - this will convert to our versions. + score.version_played.toString() + ); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with songID ${score.music_id} (${playtype} ${score.difficulty} - Version ${score.version_played})`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${score.music_id} (${playtype} ${score.difficulty} - Version ${score.version_played})`, + importType, + data, + context + ); + } - const song = await FindSongOnID("iidx", chart.songID); + const song = await FindSongOnID("iidx", chart.songID); - if (!song) { - logger.severe(`Song-Chart desync with song ID ${chart.songID} (iidx).`); - throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (iidx).`); - } + if (!song) { + logger.severe(`Song-Chart desync with song ID ${chart.songID} (iidx).`); + throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (iidx).`); + } - const lamp = ResolveKaiLamp(score.lamp); + const lamp = ResolveKaiLamp(score.lamp); - const { percent, grade } = GenericGetGradeAndPercent("iidx", score.ex_score, chart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", score.ex_score, chart); - const timeAchieved = ParseDateFromString(score.timestamp); + const timeAchieved = ParseDateFromString(score.timestamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - comment: null, - game: "iidx", - importType, - timeAchieved, - service: context.service, - scoreData: { - grade, - percent, - score: score.ex_score, - lamp, - hitData: {}, - hitMeta: { - fast: score.fast_count, - slow: score.slow_count, - bp: score.miss_count === -1 ? null : score.miss_count, - }, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + comment: null, + game: "iidx", + importType, + timeAchieved, + service: context.service, + scoreData: { + grade, + percent, + score: score.ex_score, + lamp, + hitData: {}, + hitMeta: { + fast: score.fast_count, + slow: score.slow_count, + bp: score.miss_count === -1 ? null : score.miss_count, + }, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts index 93d29d081..049ce0ed2 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts @@ -6,84 +6,84 @@ import CreateLogCtx from "../../../../../logger/logger"; import { ParseKaiIIDX } from "./parser"; const fakeAuth = { - userID: 1, - refreshToken: "foo", - service: "FLO" as const, - token: "bar", + userID: 1, + refreshToken: "foo", + service: "FLO" as const, + token: "bar", }; const logger = CreateLogCtx(__filename); t.test("#ParseKaiIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should iterate over the IIDX FLO API asynchronously.", async (t) => { - const mockFloAPI = MockJSONFetch({ - "https://flo.example.com/api/iidx/v2/play_history": { - _links: { - _next: "https://flo.example.com/api/iidx/v2/play_history?page=2", - }, - _items: [1, 2, 3, 4], - }, - "https://flo.example.com/api/iidx/v2/play_history?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); + t.test("Should iterate over the IIDX FLO API asynchronously.", async (t) => { + const mockFloAPI = MockJSONFetch({ + "https://flo.example.com/api/iidx/v2/play_history": { + _links: { + _next: "https://flo.example.com/api/iidx/v2/play_history?page=2", + }, + _items: [1, 2, 3, 4], + }, + "https://flo.example.com/api/iidx/v2/play_history?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); - const res = ParseKaiIIDX("FLO", fakeAuth, logger, mockFloAPI); + const res = ParseKaiIIDX("FLO", fakeAuth, logger, mockFloAPI); - t.equal(res.game, "iidx"); - t.equal(res.classHandler, null); - t.strictSame(res.context, { service: "FLO" }); + t.equal(res.game, "iidx"); + t.equal(res.classHandler, null); + t.strictSame(res.context, { service: "FLO" }); - const iter = []; + const iter = []; - for await (const el of res.iterable) { - iter.push(el); - } + for await (const el of res.iterable) { + iter.push(el); + } - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.test("Should iterate over the IIDX EAG API asynchronously.", async (t) => { - const mockEagAPI = MockJSONFetch({ - "https://eag.example.com/api/iidx/v2/play_history": { - _links: { - _next: "https://eag.example.com/api/iidx/v2/play_history?page=2", - }, - _items: [1, 2, 3, 4], - }, - "https://eag.example.com/api/iidx/v2/play_history?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); + t.test("Should iterate over the IIDX EAG API asynchronously.", async (t) => { + const mockEagAPI = MockJSONFetch({ + "https://eag.example.com/api/iidx/v2/play_history": { + _links: { + _next: "https://eag.example.com/api/iidx/v2/play_history?page=2", + }, + _items: [1, 2, 3, 4], + }, + "https://eag.example.com/api/iidx/v2/play_history?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); - const res = ParseKaiIIDX("EAG", fakeAuth, logger, mockEagAPI); + const res = ParseKaiIIDX("EAG", fakeAuth, logger, mockEagAPI); - t.equal(res.game, "iidx"); - t.equal(res.classHandler, null); - t.strictSame(res.context, { service: "EAG" }); + t.equal(res.game, "iidx"); + t.equal(res.classHandler, null); + t.strictSame(res.context, { service: "EAG" }); - const iter = []; + const iter = []; - for await (const el of res.iterable) { - iter.push(el); - } + for await (const el of res.iterable) { + iter.push(el); + } - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts index b2cef62e6..0bbd0fafd 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts @@ -7,25 +7,25 @@ import { ParserFunctionReturns } from "../../types"; import { FLO_API_URL, EAG_API_URL } from "../../../../../setup/config"; export function ParseKaiIIDX( - service: "FLO" | "EAG", - authDoc: KaiAuthDocument, - logger: KtLogger, - fetch = nodeFetch + service: "FLO" | "EAG", + authDoc: KaiAuthDocument, + logger: KtLogger, + fetch = nodeFetch ): ParserFunctionReturns { - const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL; + const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL; - return { - iterable: TraverseKaiAPI( - baseUrl, - "/api/iidx/v2/play_history", - authDoc.token, - logger, - fetch - ), - context: { - service, - }, - classHandler: null, - game: "iidx", - }; + return { + iterable: TraverseKaiAPI( + baseUrl, + "/api/iidx/v2/play_history", + authDoc.token, + logger, + fetch + ), + context: { + service, + }, + classHandler: null, + game: "iidx", + }; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts index 698a28e91..2604280cb 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts @@ -2,9 +2,9 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../../test-utils/close-connections"; import ResetDBState from "../../../../../../test-utils/resets"; import { - GetKTDataJSON, - TestingAlbidaADV, - TestingSDVXAlbidaSong, + GetKTDataJSON, + TestingAlbidaADV, + TestingSDVXAlbidaSong, } from "../../../../../../test-utils/test-data"; import CreateLogCtx from "../../../../../logger/logger"; import { ConvertAPIKaiSDVX, ConvertDifficulty, ConvertVersion, ResolveKaiLamp } from "./converter"; @@ -16,118 +16,118 @@ const logger = CreateLogCtx(__filename); const sdvxScore = GetKTDataJSON("./api-kai/sdvx-score.json"); t.test("#ConvertAPIKaiSDVX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a dryScore on valid input.", async (t) => { - const res = await ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger); + t.test("Should return a dryScore on valid input.", async (t) => { + const res = await ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger); - t.hasStrict(res, { - song: TestingSDVXAlbidaSong, - chart: TestingAlbidaADV, - dryScore: { - comment: null, - game: "sdvx", - importType: "api/flo-sdvx", - timeAchieved: 1598792891000, - service: "FLO", - scoreData: { - grade: "AA", - // percent: 93.10699, floating point - score: 9310699, - lamp: "EXCESSIVE CLEAR", - hitData: {}, - hitMeta: { - fast: 70, - slow: 42, - gauge: 90, - maxCombo: 179, - }, - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + song: TestingSDVXAlbidaSong, + chart: TestingAlbidaADV, + dryScore: { + comment: null, + game: "sdvx", + importType: "api/flo-sdvx", + timeAchieved: 1598792891000, + service: "FLO", + scoreData: { + grade: "AA", + // percent: 93.10699, floating point + score: 9310699, + lamp: "EXCESSIVE CLEAR", + hitData: {}, + hitMeta: { + fast: 70, + slow: 42, + gauge: 90, + maxCombo: 179, + }, + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw KTDataNotFound on unknown chart", (t) => { - t.rejects( - () => - ConvertAPIKaiSDVX( - deepmerge(sdvxScore, { music_id: 0 }), - { service: "FLO" }, - "api/flo-sdvx", - logger - ), - { - message: /Could not find chart with songID 0 \(ADV - Version vivid\)/u, - } as any - ); + t.test("Should throw KTDataNotFound on unknown chart", (t) => { + t.rejects( + () => + ConvertAPIKaiSDVX( + deepmerge(sdvxScore, { music_id: 0 }), + { service: "FLO" }, + "api/flo-sdvx", + logger + ), + { + message: /Could not find chart with songID 0 \(ADV - Version vivid\)/u, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw InvalidScoreFailure on invalid score", (t) => { - t.rejects( - () => - ConvertAPIKaiSDVX( - deepmerge(sdvxScore, { music_id: "foo" }), - { service: "FLO" }, - "api/flo-sdvx", - logger - ), - { - message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu, - } as any - ); + t.test("Should throw InvalidScoreFailure on invalid score", (t) => { + t.rejects( + () => + ConvertAPIKaiSDVX( + deepmerge(sdvxScore, { music_id: "foo" }), + { service: "FLO" }, + "api/flo-sdvx", + logger + ), + { + message: /Error: music_id.*Expected a positive integer.* received foo \[string\]/iu, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw InternalFailure on song-chart desync", async (t) => { - await db.songs.sdvx.remove({}); + t.test("Should throw InternalFailure on song-chart desync", async (t) => { + await db.songs.sdvx.remove({}); - t.rejects(() => ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger), { - message: /Song-Chart desync/u, - } as any); + t.rejects(() => ConvertAPIKaiSDVX(sdvxScore, { service: "FLO" }, "api/flo-sdvx", logger), { + message: /Song-Chart desync/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ConvertDifficulty", (t) => { - t.equal(ConvertDifficulty(0), "NOV"); - t.equal(ConvertDifficulty(1), "ADV"); - t.equal(ConvertDifficulty(2), "EXH"); - t.equal(ConvertDifficulty(3), "ANY_INF"); - t.equal(ConvertDifficulty(4), "MXM"); - t.throws(() => ConvertDifficulty(5)); + t.equal(ConvertDifficulty(0), "NOV"); + t.equal(ConvertDifficulty(1), "ADV"); + t.equal(ConvertDifficulty(2), "EXH"); + t.equal(ConvertDifficulty(3), "ANY_INF"); + t.equal(ConvertDifficulty(4), "MXM"); + t.throws(() => ConvertDifficulty(5)); - t.end(); + t.end(); }); t.test("#ConvertVersion", (t) => { - t.equal(ConvertVersion(0), "booth"); - t.equal(ConvertVersion(1), "inf"); - t.equal(ConvertVersion(2), "gw"); - t.equal(ConvertVersion(3), "heaven"); - t.equal(ConvertVersion(4), "vivid"); - t.throws(() => ConvertVersion(5)); + t.equal(ConvertVersion(0), "booth"); + t.equal(ConvertVersion(1), "inf"); + t.equal(ConvertVersion(2), "gw"); + t.equal(ConvertVersion(3), "heaven"); + t.equal(ConvertVersion(4), "vivid"); + t.throws(() => ConvertVersion(5)); - t.end(); + t.end(); }); t.test("#ResolveKaiLamp", (t) => { - t.equal(ResolveKaiLamp(0), "FAILED"); - t.equal(ResolveKaiLamp(1), "CLEAR"); - t.equal(ResolveKaiLamp(2), "EXCESSIVE CLEAR"); - t.equal(ResolveKaiLamp(3), "ULTIMATE CHAIN"); - t.equal(ResolveKaiLamp(4), "PERFECT ULTIMATE CHAIN"); - t.throws(() => ConvertDifficulty(5)); + t.equal(ResolveKaiLamp(0), "FAILED"); + t.equal(ResolveKaiLamp(1), "CLEAR"); + t.equal(ResolveKaiLamp(2), "EXCESSIVE CLEAR"); + t.equal(ResolveKaiLamp(3), "ULTIMATE CHAIN"); + t.equal(ResolveKaiLamp(4), "PERFECT ULTIMATE CHAIN"); + t.throws(() => ConvertDifficulty(5)); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts index b5d4d64b6..cab33607d 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts @@ -2,150 +2,150 @@ import { FindSDVXChartOnInGameIDVersion } from "../../../../../../utils/queries/ import { KaiContext, KaiSDVXScore } from "../types"; import p from "prudence"; import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../../framework/common/converter-failures"; import { FormatPrError } from "../../../../../../utils/prudence"; import { FindSongOnID } from "../../../../../../utils/queries/songs"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../../framework/common/score-utils"; import { Lamps } from "tachi-common"; import { ConverterFunction } from "../../types"; import { DryScore } from "../../../../framework/common/types"; const PR_KaiSDVXScore = { - music_id: p.isPositiveInteger, - music_difficulty: p.isBoundedInteger(0, 4), - played_version: p.isBoundedInteger(1, 5), - clear_type: p.isBoundedInteger(0, 4), - max_chain: p.isPositiveInteger, - score: p.isBoundedInteger(0, 10_000_000), - critical: p.isPositiveInteger, - near: p.isPositiveInteger, - error: p.isPositiveInteger, - early: p.isPositiveInteger, - late: p.isPositiveInteger, - gauge_rate: p.isBoundedInteger(0, 100), - timestamp: "string", + music_id: p.isPositiveInteger, + music_difficulty: p.isBoundedInteger(0, 4), + played_version: p.isBoundedInteger(1, 5), + clear_type: p.isBoundedInteger(0, 4), + max_chain: p.isPositiveInteger, + score: p.isBoundedInteger(0, 10_000_000), + critical: p.isPositiveInteger, + near: p.isPositiveInteger, + error: p.isPositiveInteger, + early: p.isPositiveInteger, + late: p.isPositiveInteger, + gauge_rate: p.isBoundedInteger(0, 100), + timestamp: "string", }; export const ConvertAPIKaiSDVX: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const err = p(data, PR_KaiSDVXScore, {}, { allowExcessKeys: true }); + const err = p(data, PR_KaiSDVXScore, {}, { allowExcessKeys: true }); - if (err) { - throw new InvalidScoreFailure(FormatPrError(err)); - } + if (err) { + throw new InvalidScoreFailure(FormatPrError(err)); + } - // prudence checks this above. - const score = data as KaiSDVXScore; + // prudence checks this above. + const score = data as KaiSDVXScore; - const difficulty = ConvertDifficulty(score.music_difficulty); - const version = ConvertVersion(score.played_version); + const difficulty = ConvertDifficulty(score.music_difficulty); + const version = ConvertVersion(score.played_version); - const chart = await FindSDVXChartOnInGameIDVersion(score.music_id, difficulty, version); + const chart = await FindSDVXChartOnInGameIDVersion(score.music_id, difficulty, version); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with songID ${score.music_id} (${difficulty} - Version ${version})`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${score.music_id} (${difficulty} - Version ${version})`, + importType, + data, + context + ); + } - const song = await FindSongOnID("sdvx", chart.songID); + const song = await FindSongOnID("sdvx", chart.songID); - if (!song) { - logger.severe(`Song-Chart desync with song ID ${chart.songID} (sdvx).`); - throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (sdvx).`); - } + if (!song) { + logger.severe(`Song-Chart desync with song ID ${chart.songID} (sdvx).`); + throw new InternalFailure(`Song-Chart desync with song ID ${chart.songID} (sdvx).`); + } - const lamp = ResolveKaiLamp(score.clear_type); + const lamp = ResolveKaiLamp(score.clear_type); - const { percent, grade } = GenericGetGradeAndPercent("sdvx", score.score, chart); + const { percent, grade } = GenericGetGradeAndPercent("sdvx", score.score, chart); - const timeAchieved = ParseDateFromString(score.timestamp); + const timeAchieved = ParseDateFromString(score.timestamp); - const dryScore: DryScore<"sdvx:Single"> = { - comment: null, - game: "sdvx", - importType, - timeAchieved, - service: context.service, - scoreData: { - grade, - percent, - score: score.score, - lamp, - hitData: {}, - hitMeta: { - fast: score.early, - slow: score.late, - gauge: score.gauge_rate, - maxCombo: score.max_chain, - }, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"sdvx:Single"> = { + comment: null, + game: "sdvx", + importType, + timeAchieved, + service: context.service, + scoreData: { + grade, + percent, + score: score.score, + lamp, + hitData: {}, + hitMeta: { + fast: score.early, + slow: score.late, + gauge: score.gauge_rate, + maxCombo: score.max_chain, + }, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; export function ConvertDifficulty(diff: number) { - switch (diff) { - case 0: - return "NOV"; - case 1: - return "ADV"; - case 2: - return "EXH"; - case 3: - return "ANY_INF"; - case 4: - return "MXM"; - } + switch (diff) { + case 0: + return "NOV"; + case 1: + return "ADV"; + case 2: + return "EXH"; + case 3: + return "ANY_INF"; + case 4: + return "MXM"; + } - throw new InvalidScoreFailure(`Invalid difficulty of ${diff} - Could not convert.`); + throw new InvalidScoreFailure(`Invalid difficulty of ${diff} - Could not convert.`); } export function ConvertVersion(ver: number) { - switch (ver) { - case 0: - return "booth"; - case 1: - return "inf"; - case 2: - return "gw"; - case 3: - return "heaven"; - case 4: - return "vivid"; - } + switch (ver) { + case 0: + return "booth"; + case 1: + return "inf"; + case 2: + return "gw"; + case 3: + return "heaven"; + case 4: + return "vivid"; + } - throw new InvalidScoreFailure(`Unknown Game Version ${ver}.`); + throw new InvalidScoreFailure(`Unknown Game Version ${ver}.`); } export function ResolveKaiLamp(clear: number): Lamps["sdvx:Single"] { - switch (clear) { - case 0: - return "FAILED"; - case 1: - return "CLEAR"; - case 2: - return "EXCESSIVE CLEAR"; - case 3: - return "ULTIMATE CHAIN"; - case 4: - return "PERFECT ULTIMATE CHAIN"; - } + switch (clear) { + case 0: + return "FAILED"; + case 1: + return "CLEAR"; + case 2: + return "EXCESSIVE CLEAR"; + case 3: + return "ULTIMATE CHAIN"; + case 4: + return "PERFECT ULTIMATE CHAIN"; + } - throw new InvalidScoreFailure(`Invalid lamp of ${clear} - Could not convert.`); + throw new InvalidScoreFailure(`Invalid lamp of ${clear} - Could not convert.`); } diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts index 9e19ab026..fd84b4890 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts @@ -6,84 +6,84 @@ import CreateLogCtx from "../../../../../logger/logger"; import { ParseKaiSDVX } from "./parser"; const fakeAuth = { - userID: 1, - refreshToken: "foo", - service: "FLO" as const, - token: "bar", + userID: 1, + refreshToken: "foo", + service: "FLO" as const, + token: "bar", }; const logger = CreateLogCtx(__filename); t.test("#ParseKaiSDVX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should iterate over the SDVX FLO API asynchronously.", async (t) => { - const mockFloAPI = MockJSONFetch({ - "https://flo.example.com/api/sdvx/v1/play_history": { - _links: { - _next: "https://flo.example.com/api/sdvx/v1/play_history?page=2", - }, - _items: [1, 2, 3, 4], - }, - "https://flo.example.com/api/sdvx/v1/play_history?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); + t.test("Should iterate over the SDVX FLO API asynchronously.", async (t) => { + const mockFloAPI = MockJSONFetch({ + "https://flo.example.com/api/sdvx/v1/play_history": { + _links: { + _next: "https://flo.example.com/api/sdvx/v1/play_history?page=2", + }, + _items: [1, 2, 3, 4], + }, + "https://flo.example.com/api/sdvx/v1/play_history?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); - const res = ParseKaiSDVX("FLO", fakeAuth, logger, mockFloAPI); + const res = ParseKaiSDVX("FLO", fakeAuth, logger, mockFloAPI); - t.equal(res.game, "sdvx"); - t.equal(res.classHandler, null); - t.strictSame(res.context, { service: "FLO" }); + t.equal(res.game, "sdvx"); + t.equal(res.classHandler, null); + t.strictSame(res.context, { service: "FLO" }); - const iter = []; + const iter = []; - for await (const el of res.iterable) { - iter.push(el); - } + for await (const el of res.iterable) { + iter.push(el); + } - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.test("Should iterate over the SDVX EAG API asynchronously.", async (t) => { - const mockEagAPI = MockJSONFetch({ - "https://eag.example.com/api/sdvx/v1/play_history": { - _links: { - _next: "https://eag.example.com/api/sdvx/v1/play_history?page=2", - }, - _items: [1, 2, 3, 4], - }, - "https://eag.example.com/api/sdvx/v1/play_history?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); + t.test("Should iterate over the SDVX EAG API asynchronously.", async (t) => { + const mockEagAPI = MockJSONFetch({ + "https://eag.example.com/api/sdvx/v1/play_history": { + _links: { + _next: "https://eag.example.com/api/sdvx/v1/play_history?page=2", + }, + _items: [1, 2, 3, 4], + }, + "https://eag.example.com/api/sdvx/v1/play_history?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); - const res = ParseKaiSDVX("EAG", fakeAuth, logger, mockEagAPI); + const res = ParseKaiSDVX("EAG", fakeAuth, logger, mockEagAPI); - t.equal(res.game, "sdvx"); - t.equal(res.classHandler, null); - t.strictSame(res.context, { service: "EAG" }); + t.equal(res.game, "sdvx"); + t.equal(res.classHandler, null); + t.strictSame(res.context, { service: "EAG" }); - const iter = []; + const iter = []; - for await (const el of res.iterable) { - iter.push(el); - } + for await (const el of res.iterable) { + iter.push(el); + } - t.strictSame(iter, [1, 2, 3, 4, 5, 6]); + t.strictSame(iter, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts index 8c4cf8f7d..4eb94c30e 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts @@ -7,25 +7,25 @@ import { ParserFunctionReturns } from "../../types"; import { EAG_API_URL, FLO_API_URL } from "../../../../../setup/config"; export function ParseKaiSDVX( - service: "FLO" | "EAG", - authDoc: KaiAuthDocument, - logger: KtLogger, - fetch = nodeFetch + service: "FLO" | "EAG", + authDoc: KaiAuthDocument, + logger: KtLogger, + fetch = nodeFetch ): ParserFunctionReturns { - const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL; + const baseUrl = service === "FLO" ? FLO_API_URL : EAG_API_URL; - return { - iterable: TraverseKaiAPI( - baseUrl, - "/api/sdvx/v1/play_history", - authDoc.token, - logger, - fetch - ), - context: { - service, - }, - classHandler: null, - game: "sdvx", - }; + return { + iterable: TraverseKaiAPI( + baseUrl, + "/api/sdvx/v1/play_history", + authDoc.token, + logger, + fetch + ), + context: { + service, + }, + classHandler: null, + game: "sdvx", + }; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts index fa454535e..6c7041c92 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts @@ -12,146 +12,146 @@ const logger = CreateLogCtx(__filename); const fakeAuth = "bar"; t.test("#TraverseKaiAPI", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should traverse the API", async (t) => { - const mockKaiAPI = MockJSONFetch({ - "http://url.com/sub": { - _links: { - _next: "http://url.com/sub?page=2", - }, - _items: [1, 2, 3, 4], - }, - "http://url.com/sub?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); + t.test("Should traverse the API", async (t) => { + const mockKaiAPI = MockJSONFetch({ + "http://url.com/sub": { + _links: { + _next: "http://url.com/sub?page=2", + }, + _items: [1, 2, 3, 4], + }, + "http://url.com/sub?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - const elements = await agta(res); + const elements = await agta(res); - t.strictSame(elements, [1, 2, 3, 4, 5, 6]); + t.strictSame(elements, [1, 2, 3, 4, 5, 6]); - t.end(); - }); + t.end(); + }); - t.test("Should throw on attempted SSRF", (t) => { - const mockKaiAPI = MockJSONFetch({ - "http://url.com/sub": { - _links: { - _next: "http://evil.com/sub?page=2", - }, - _items: [1, 2, 3, 4], - }, - "http://url.com/sub?page=2": { - _links: { - _next: null, - }, - _items: [5, 6], - }, - }); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + t.test("Should throw on attempted SSRF", (t) => { + const mockKaiAPI = MockJSONFetch({ + "http://url.com/sub": { + _links: { + _next: "http://evil.com/sub?page=2", + }, + _items: [1, 2, 3, 4], + }, + "http://url.com/sub?page=2": { + _links: { + _next: null, + }, + _items: [5, 6], + }, + }); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects( - () => agta(res), - new ScoreImportFatalError(500, `http://url.com returned invalid data.`) - ); + t.rejects( + () => agta(res), + new ScoreImportFatalError(500, `http://url.com returned invalid data.`) + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid response JSON", (t) => { - const mockKaiAPI = () => ({ json: null } as any); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + t.test("Should throw on invalid response JSON", (t) => { + const mockKaiAPI = () => ({ json: null } as any); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects(() => agta(res)); + t.rejects(() => agta(res)); - t.end(); - }); + t.end(); + }); - t.test("Should throw on request failure", (t) => { - const mockKaiAPI = () => { - throw new Error("Fake Request timeout..."); - }; - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + t.test("Should throw on request failure", (t) => { + const mockKaiAPI = () => { + throw new Error("Fake Request timeout..."); + }; + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects(() => agta(res)); + t.rejects(() => agta(res)); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid _links", (t) => { - const mockKaiAPI = MockJSONFetch({ - "http://url.com/sub": { - _links: null, - }, - }); + t.test("Should throw on invalid _links", (t) => { + const mockKaiAPI = MockJSONFetch({ + "http://url.com/sub": { + _links: null, + }, + }); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects(() => agta(res)); + t.rejects(() => agta(res)); - const mockKaiAPI2 = MockJSONFetch({ - "http://url.com/sub": { - _links: "foo", - }, - }); + const mockKaiAPI2 = MockJSONFetch({ + "http://url.com/sub": { + _links: "foo", + }, + }); - const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2); + const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2); - t.rejects(() => agta(res2)); + t.rejects(() => agta(res2)); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid _links._next", (t) => { - const mockKaiAPI = MockJSONFetch({ - "http://url.com/sub": { - _links: {}, - }, - }); + t.test("Should throw on invalid _links._next", (t) => { + const mockKaiAPI = MockJSONFetch({ + "http://url.com/sub": { + _links: {}, + }, + }); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects(() => agta(res)); + t.rejects(() => agta(res)); - const mockKaiAPI2 = MockJSONFetch({ - "http://url.com/sub": { - _links: { - _next: {}, - }, - }, - }); + const mockKaiAPI2 = MockJSONFetch({ + "http://url.com/sub": { + _links: { + _next: {}, + }, + }, + }); - const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2); + const res2 = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI2); - t.rejects(() => agta(res2)); + t.rejects(() => agta(res2)); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid _items", (t) => { - const mockKaiAPI = MockJSONFetch({ - "http://url.com/sub": { - _links: { - _next: null, - }, - _items: {}, - }, - }); + t.test("Should throw on invalid _items", (t) => { + const mockKaiAPI = MockJSONFetch({ + "http://url.com/sub": { + _links: { + _next: null, + }, + _items: {}, + }, + }); - const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, mockKaiAPI); - t.rejects(() => agta(res)); + t.rejects(() => agta(res)); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts index 220bcec0b..68a52bf41 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts @@ -13,75 +13,75 @@ import { CONF_INFO } from "../../../../setup/config"; * that wont make http requests during tests. */ export async function* TraverseKaiAPI( - baseUrl: string, - subUrl: string, - token: string, - logger: KtLogger, - fetch = nodeFetch + baseUrl: string, + subUrl: string, + token: string, + logger: KtLogger, + fetch = nodeFetch ) { - let fetchMoreData = true; - let url = `${baseUrl}${subUrl}`; + let fetchMoreData = true; + let url = `${baseUrl}${subUrl}`; - while (fetchMoreData) { - // Rough SSRF check - KAI-like apis could potentially get us to perform - // SSRF. We check if the origin of the next request is exactly the - // original, expected, base URL. - const origin = new URL(url).origin; + while (fetchMoreData) { + // Rough SSRF check - KAI-like apis could potentially get us to perform + // SSRF. We check if the origin of the next request is exactly the + // original, expected, base URL. + const origin = new URL(url).origin; - if (origin !== baseUrl) { - logger.severe(`${baseUrl} attempted SSRF with url ${url}?`); - throw new ScoreImportFatalError(500, `${baseUrl} returned invalid data.`); - } + if (origin !== baseUrl) { + logger.severe(`${baseUrl} attempted SSRF with url ${url}?`); + throw new ScoreImportFatalError(500, `${baseUrl} returned invalid data.`); + } - let json; - // wrap all this in a try catch just incase the fetch or the res - // json call fails. - try { - // eslint-disable-next-line no-await-in-loop - const res = await fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - "User-Agent": `${CONF_INFO.NAME}/${VERSION_STR}`, - }, - }); + let json; + // wrap all this in a try catch just incase the fetch or the res + // json call fails. + try { + // eslint-disable-next-line no-await-in-loop + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + "User-Agent": `${CONF_INFO.NAME}/${VERSION_STR}`, + }, + }); - // eslint-disable-next-line no-await-in-loop - json = await res.json(); - } catch (err) { - logger.error(`Recieved invalid response from ${url}.`, { err }); - throw new ScoreImportFatalError( - 500, - `Recieved invalid response from ${url}. Are they down?` - ); - } + // eslint-disable-next-line no-await-in-loop + json = await res.json(); + } catch (err) { + logger.error(`Recieved invalid response from ${url}.`, { err }); + throw new ScoreImportFatalError( + 500, + `Recieved invalid response from ${url}. Are they down?` + ); + } - if (json._links === null || typeof json._links !== "object") { - logger.error(`Recieved invalid JSON from ${url}. Invalid _links.`); + if (json._links === null || typeof json._links !== "object") { + logger.error(`Recieved invalid JSON from ${url}. Invalid _links.`); - throw new ScoreImportFatalError( - 500, - `Recieved no _links prop from ${url}. This is not an error with Kamaitachi.` - ); - } + throw new ScoreImportFatalError( + 500, + `Recieved no _links prop from ${url}. This is not an error with Kamaitachi.` + ); + } - if (typeof json._links._next === "string") { - url = json._links._next; - } else if (json._links._next === null) { - // exit the loop after this, we're on the last page. - fetchMoreData = false; - } else { - logger.error(`Recieved invalid response from ${url}. Invalid _links._next.`); + if (typeof json._links._next === "string") { + url = json._links._next; + } else if (json._links._next === null) { + // exit the loop after this, we're on the last page. + fetchMoreData = false; + } else { + logger.error(`Recieved invalid response from ${url}. Invalid _links._next.`); - throw new ScoreImportFatalError(500, `Recieved invalid _links._next prop from ${url}.`); - } + throw new ScoreImportFatalError(500, `Recieved invalid _links._next prop from ${url}.`); + } - if (!Array.isArray(json._items)) { - throw new ScoreImportFatalError(500, `Recieved invalid _items from ${url}.`); - } + if (!Array.isArray(json._items)) { + throw new ScoreImportFatalError(500, `Recieved invalid _items from ${url}.`); + } - // yield everything out of the score array - for (const score of json._items) { - yield score as unknown; - } - } + // yield everything out of the score array + for (const score of json._items) { + yield score as unknown; + } + } } diff --git a/server/src/lib/score-import/import-types/common/api-kai/types.ts b/server/src/lib/score-import/import-types/common/api-kai/types.ts index e145ed120..0ac4d99e1 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/types.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/types.ts @@ -1,35 +1,35 @@ import { integer } from "tachi-common"; export interface KaiIIDXScore { - music_id: integer; - play_style: "SINGLE" | "DOUBLE"; - difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; - version_played: integer; - lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - ex_score: integer; - miss_count: integer; // -1 => null - fast_count: integer | null; - slow_count: integer | null; - timestamp: string; + music_id: integer; + play_style: "SINGLE" | "DOUBLE"; + difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + version_played: integer; + lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + ex_score: integer; + miss_count: integer; // -1 => null + fast_count: integer | null; + slow_count: integer | null; + timestamp: string; } export interface KaiSDVXScore { - music_id: integer; - music_difficulty: 0 | 1 | 2 | 3 | 4; - played_version: integer; - clear_type: 0 | 1 | 2 | 3 | 4; // hm - score: integer; - max_chain: integer; - critical: integer; - near: integer; - error: integer; - early: integer; - late: integer; - gauge_type: 0 | 1 | 2 | 3; - gauge_rate: integer; - timestamp: string; + music_id: integer; + music_difficulty: 0 | 1 | 2 | 3 | 4; + played_version: integer; + clear_type: 0 | 1 | 2 | 3 | 4; // hm + score: integer; + max_chain: integer; + critical: integer; + near: integer; + error: integer; + early: integer; + late: integer; + gauge_type: 0 | 1 | 2 | 3; + gauge_rate: integer; + timestamp: string; } export interface KaiContext { - service: "FLO" | "EAG"; + service: "FLO" | "EAG"; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts index e9376e013..0414a0a27 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts @@ -10,25 +10,25 @@ import { InvalidScoreFailure } from "../../../framework/common/converter-failure import { CloseAllConnections } from "../../../../../test-utils/close-connections"; const baseBatchManualScore = { - score: 500, - lamp: "HARD CLEAR" as const, - matchType: "kamaitachiSongID" as const, - identifier: "1", - playtype: "SP" as const, - difficulty: "ANOTHER", + score: 500, + lamp: "HARD CLEAR" as const, + matchType: "kamaitachiSongID" as const, + identifier: "1", + playtype: "SP" as const, + difficulty: "ANOTHER", }; const context = { - game: "iidx" as const, - service: "foo", - version: null, + game: "iidx" as const, + service: "foo", + version: null, }; const ktdWrap = (msg: string, game: Game = "iidx", version = null): any => ({ - importType: "file/batch-manual", - message: new RegExp(EscapeStringRegexp(msg), "u"), - converterContext: { game, service: "foo", version }, - data: {}, // any under t.match rules. + importType: "file/batch-manual", + message: new RegExp(EscapeStringRegexp(msg), "u"), + converterContext: { game, service: "foo", version }, + data: {}, // any under t.match rules. }); const logger = CreateLogCtx(__filename); @@ -36,361 +36,361 @@ const logger = CreateLogCtx(__filename); const importType = "file/batch-manual" as const; t.test("#ResolveMatchTypeToKTData", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should resolve for the songID if the matchType is songID", async (t) => { - const res = await ResolveMatchTypeToKTData( - baseBatchManualScore, - context, - importType, - logger - ); + t.test("Should resolve for the songID if the matchType is songID", async (t) => { + const res = await ResolveMatchTypeToKTData( + baseBatchManualScore, + context, + importType, + logger + ); - t.hasStrict( - res, - { song: { id: 1 }, chart: Testing511SPA }, - "Should return the right song and chart." - ); + t.hasStrict( + res, + { song: { id: 1 }, chart: Testing511SPA }, + "Should return the right song and chart." + ); - t.rejects( - () => - ResolveMatchTypeToKTData( - // @ts-expect-error bad - deepmerge(baseBatchManualScore, { identifier: "90000" }), - context, - importType, - logger - ), - ktdWrap("Cannot find song with songID 90000") - ); + t.rejects( + () => + ResolveMatchTypeToKTData( + // @ts-expect-error bad + deepmerge(baseBatchManualScore, { identifier: "90000" }), + context, + importType, + logger + ), + ktdWrap("Cannot find song with songID 90000") + ); - t.end(); - }); + t.end(); + }); - t.test("Should resolve for the song title if the matchType is songTitle", async (t) => { - const res = await ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { matchType: "songTitle", identifier: "5.1.1." }), - context, - importType, - logger - ); + t.test("Should resolve for the song title if the matchType is songTitle", async (t) => { + const res = await ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { matchType: "songTitle", identifier: "5.1.1." }), + context, + importType, + logger + ); - t.hasStrict( - res, - { song: { id: 1 }, chart: Testing511SPA }, - "Should return the right song and chart." - ); + t.hasStrict( + res, + { song: { id: 1 }, chart: Testing511SPA }, + "Should return the right song and chart." + ); - t.rejects( - () => - ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "songTitle", - identifier: "INVALID_TITLE", - }), - context, - importType, - logger - ), - ktdWrap("Cannot find song with title INVALID_TITLE") - ); + t.rejects( + () => + ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "songTitle", + identifier: "INVALID_TITLE", + }), + context, + importType, + logger + ), + ktdWrap("Cannot find song with title INVALID_TITLE") + ); - t.end(); - }); + t.end(); + }); - t.test("Should resolve for the bms chartHash if the matchType is bmsChartHash", async (t) => { - const GAZER17MD5 = "38616b85332037cc12924f2ae2840262"; - const GAZER17SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; + t.test("Should resolve for the bms chartHash if the matchType is bmsChartHash", async (t) => { + const GAZER17MD5 = "38616b85332037cc12924f2ae2840262"; + const GAZER17SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; - const gazerSong = GetKTDataJSON("./tachi/bms-gazer-song.json"); - const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json"); + const gazerSong = GetKTDataJSON("./tachi/bms-gazer-song.json"); + const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json"); - const bmsContext: any = deepmerge(context, { game: "bms" }); + const bmsContext: any = deepmerge(context, { game: "bms" }); - const resMD5 = await ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "bmsChartHash", - identifier: GAZER17MD5, - }), - bmsContext, - importType, - logger - ); + const resMD5 = await ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "bmsChartHash", + identifier: GAZER17MD5, + }), + bmsContext, + importType, + logger + ); - t.hasStrict( - resMD5, - { song: gazerSong, chart: gazerChart }, - "Should return the right song and chart." - ); + t.hasStrict( + resMD5, + { song: gazerSong, chart: gazerChart }, + "Should return the right song and chart." + ); - const resSHA256 = await ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "bmsChartHash", - identifier: GAZER17SHA256, - }), - bmsContext, - importType, - logger - ); + const resSHA256 = await ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "bmsChartHash", + identifier: GAZER17SHA256, + }), + bmsContext, + importType, + logger + ); - t.hasStrict( - resSHA256, - { song: gazerSong, chart: gazerChart }, - "Should return the right song and chart." - ); + t.hasStrict( + resSHA256, + { song: gazerSong, chart: gazerChart }, + "Should return the right song and chart." + ); - t.rejects( - () => - ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "bmsChartHash", - identifier: "bad_hash", - }), - bmsContext, - importType, - logger - ), - ktdWrap("Cannot find chart for hash ", "bms") - ); + t.rejects( + () => + ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "bmsChartHash", + identifier: "bad_hash", + }), + bmsContext, + importType, + logger + ), + ktdWrap("Cannot find chart for hash ", "bms") + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject if ddrSongHash is called without game = ddr", (t) => { - t.rejects( - ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "ddrSongHash", - playtype: "SP", - difficulty: "DIFFICULT", - }), - context, - importType, - logger - ), - new InvalidScoreFailure("Cannot use ddrSongHash lookup on iidx.") as any - ); + t.test("Should reject if ddrSongHash is called without game = ddr", (t) => { + t.rejects( + ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "ddrSongHash", + playtype: "SP", + difficulty: "DIFFICULT", + }), + context, + importType, + logger + ), + new InvalidScoreFailure("Cannot use ddrSongHash lookup on iidx.") as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should resolve for the ddr songHash if the matchType is ddrSongHash", async (t) => { - const PUTY_ID = "DQlQ1DlPbq900oqdOo8l0d6I1lIOl99l"; + t.test("Should resolve for the ddr songHash if the matchType is ddrSongHash", async (t) => { + const PUTY_ID = "DQlQ1DlPbq900oqdOo8l0d6I1lIOl99l"; - const res = await ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "ddrSongHash", - identifier: PUTY_ID, - playtype: "SP", - difficulty: "DIFFICULT", - }), - deepmerge(context, { game: "ddr" }), - importType, - logger - ); + const res = await ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "ddrSongHash", + identifier: PUTY_ID, + playtype: "SP", + difficulty: "DIFFICULT", + }), + deepmerge(context, { game: "ddr" }), + importType, + logger + ); - t.hasStrict( - res, - { - song: { id: 10 }, - chart: { chartID: "48024d36bbe76c9fed09c3ffdc19412925d1efd3" }, - }, - "Should return the right song and chart." - ); + t.hasStrict( + res, + { + song: { id: 10 }, + chart: { chartID: "48024d36bbe76c9fed09c3ffdc19412925d1efd3" }, + }, + "Should return the right song and chart." + ); - t.rejects( - () => - ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "ddrSongHash", - identifier: "Bad_ID", - playtype: "SP", - difficulty: "EXPERT", - }), - deepmerge(context, { game: "ddr" }), - importType, - logger - ), - ktdWrap("Cannot find chart for songHash", "ddr") - ); + t.rejects( + () => + ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "ddrSongHash", + identifier: "Bad_ID", + playtype: "SP", + difficulty: "EXPERT", + }), + deepmerge(context, { game: "ddr" }), + importType, + logger + ), + ktdWrap("Cannot find chart for songHash", "ddr") + ); - t.end(); - }); + t.end(); + }); - t.test("Should trigger failsave if invalid matchType is provided.", (t) => { - t.rejects( - () => - ResolveMatchTypeToKTData( - deepmerge(baseBatchManualScore, { - matchType: "BAD_MATCHTYPE", - }), - context, - importType, - logger - ), - new InvalidScoreFailure(`Invalid matchType BAD_MATCHTYPE`) as any - ); + t.test("Should trigger failsave if invalid matchType is provided.", (t) => { + t.rejects( + () => + ResolveMatchTypeToKTData( + deepmerge(baseBatchManualScore, { + matchType: "BAD_MATCHTYPE", + }), + context, + importType, + logger + ), + new InvalidScoreFailure(`Invalid matchType BAD_MATCHTYPE`) as any + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ResolveChartFromSong", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return the chart for the song + ptdf", async (t) => { - const res = await ResolveChartFromSong( - Testing511Song, - baseBatchManualScore, // has playtype + diff - { game: "iidx", service: "foo", version: null }, - importType - ); + t.test("Should return the chart for the song + ptdf", async (t) => { + const res = await ResolveChartFromSong( + Testing511Song, + baseBatchManualScore, // has playtype + diff + { game: "iidx", service: "foo", version: null }, + importType + ); - t.hasStrict(res, Testing511SPA); + t.hasStrict(res, Testing511SPA); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if no playtype is provided.", (t) => { - t.rejects( - () => - ResolveChartFromSong( - Testing511Song, - deepmerge(baseBatchManualScore, { playtype: null }), - { game: "iidx", service: "foo", version: null }, - importType - ), - new InvalidScoreFailure( - `Missing 'playtype' field, but was necessary for this lookup.` - ) as any - ); + t.test("Should throw an error if no playtype is provided.", (t) => { + t.rejects( + () => + ResolveChartFromSong( + Testing511Song, + deepmerge(baseBatchManualScore, { playtype: null }), + { game: "iidx", service: "foo", version: null }, + importType + ), + new InvalidScoreFailure( + `Missing 'playtype' field, but was necessary for this lookup.` + ) as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if no difficulty is provided.", (t) => { - t.rejects( - () => - ResolveChartFromSong( - Testing511Song, - deepmerge(baseBatchManualScore, { difficulty: null }), - { game: "iidx", service: "foo", version: null }, - importType - ), - new InvalidScoreFailure( - `Missing 'difficulty' field, but was necessary for this lookup.` - ) as any - ); + t.test("Should throw an error if no difficulty is provided.", (t) => { + t.rejects( + () => + ResolveChartFromSong( + Testing511Song, + deepmerge(baseBatchManualScore, { difficulty: null }), + { game: "iidx", service: "foo", version: null }, + importType + ), + new InvalidScoreFailure( + `Missing 'difficulty' field, but was necessary for this lookup.` + ) as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if an invalid difficulty is provided.", (t) => { - t.rejects( - () => - ResolveChartFromSong( - Testing511Song, - // @ts-expect-error faulty deepmerge types - deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }), - { game: "iidx", service: "foo", version: null }, - importType - ), - new InvalidScoreFailure( - `Invalid Difficulty for iidx SP - Expected any of BEGINNER, NORMAL, HYPER, ANOTHER, LEGGENDARIA` - ) as any - ); + t.test("Should throw an error if an invalid difficulty is provided.", (t) => { + t.rejects( + () => + ResolveChartFromSong( + Testing511Song, + // @ts-expect-error faulty deepmerge types + deepmerge(baseBatchManualScore, { difficulty: "NOT_VALID_DIFFICULTY" }), + { game: "iidx", service: "foo", version: null }, + importType + ), + new InvalidScoreFailure( + `Invalid Difficulty for iidx SP - Expected any of BEGINNER, NORMAL, HYPER, ANOTHER, LEGGENDARIA` + ) as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if no chart could be found.", (t) => { - t.rejects( - () => - ResolveChartFromSong( - Testing511Song, - // @ts-expect-error faulty deepmerge types - deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol) - { game: "iidx", service: "foo", version: null }, - importType - ), - ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)") - ); + t.test("Should throw an error if no chart could be found.", (t) => { + t.rejects( + () => + ResolveChartFromSong( + Testing511Song, + // @ts-expect-error faulty deepmerge types + deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol) + { game: "iidx", service: "foo", version: null }, + importType + ), + ktdWrap("Cannot find chart for 5.1.1. (SP LEGGENDARIA)") + ); - t.end(); - }); + t.end(); + }); - t.test("Should successfully lookup if version is provided.", async (t) => { - const res = await ResolveChartFromSong( - Testing511Song, - baseBatchManualScore, - { - game: "iidx", - service: "foo", - version: "27", - }, - importType - ); + t.test("Should successfully lookup if version is provided.", async (t) => { + const res = await ResolveChartFromSong( + Testing511Song, + baseBatchManualScore, + { + game: "iidx", + service: "foo", + version: "27", + }, + importType + ); - t.hasStrict(res, Testing511SPA); + t.hasStrict(res, Testing511SPA); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ConverterFn", (t) => { - t.test("Should produce a DryScore", async (t) => { - const res = await ConverterBatchManual( - baseBatchManualScore, - { game: "iidx", service: "foo", version: null }, - importType, - logger - ); + t.test("Should produce a DryScore", async (t) => { + const res = await ConverterBatchManual( + baseBatchManualScore, + { game: "iidx", service: "foo", version: null }, + importType, + logger + ); - t.hasStrict(res, { - chart: Testing511SPA, - song: { id: 1 }, - dryScore: { - game: "iidx", - service: "foo (BATCH-MANUAL)", - comment: null, - importType: "file/batch-manual", - timeAchieved: null, - scoreData: { - lamp: "HARD CLEAR", - score: 500, - grade: "E", - // percent: 31.5, -- ish, FPA is hard. - hitData: {}, - hitMeta: {}, - }, - scoreMeta: {}, - }, - }); + t.hasStrict(res, { + chart: Testing511SPA, + song: { id: 1 }, + dryScore: { + game: "iidx", + service: "foo (BATCH-MANUAL)", + comment: null, + importType: "file/batch-manual", + timeAchieved: null, + scoreData: { + lamp: "HARD CLEAR", + score: 500, + grade: "E", + // percent: 31.5, -- ish, FPA is hard. + hitData: {}, + hitMeta: {}, + }, + scoreMeta: {}, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject a score with > 100%", (t) => { - t.rejects( - () => - ConverterBatchManual( - // @ts-expect-error broken deepmerge - deepmerge(baseBatchManualScore, { score: 2000 }), - { game: "iidx", service: "foo", version: null }, - importType, - logger - ), - { message: /Invalid percent/u } as any - ); + t.test("Should reject a score with > 100%", (t) => { + t.rejects( + () => + ConverterBatchManual( + // @ts-expect-error broken deepmerge + deepmerge(baseBatchManualScore, { score: 2000 }), + { game: "iidx", service: "foo", version: null }, + importType, + logger + ), + { message: /Invalid percent/u } as any + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index 7eaedb4f9..062ce5366 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -1,20 +1,20 @@ import { BatchManualContext, BatchManualScore } from "./types"; import { AnyChartDocument, AnySongDocument, ImportTypes } from "tachi-common"; import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { FindSongOnID, FindSongOnTitleInsensitive } from "../../../../../utils/queries/songs"; import { - FindBMSChartOnHash, - FindChartWithPTDF, - FindChartWithPTDFVersion, - FindDDRChartOnSongHash, + FindBMSChartOnHash, + FindChartWithPTDF, + FindChartWithPTDFVersion, + FindDDRChartOnSongHash, } from "../../../../../utils/queries/charts"; import { - AssertStrAsDifficulty, - AssertStrAsPositiveInt, + AssertStrAsDifficulty, + AssertStrAsPositiveInt, } from "../../../framework/common/string-asserts"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { KtLogger } from "../../../../logger/logger"; @@ -28,207 +28,207 @@ import { ConverterFunction } from "../types"; * @returns A BatchManualScore Converter. */ export const ConverterBatchManual: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const game = context.game; + const game = context.game; - const { song, chart } = await ResolveMatchTypeToKTData(data, context, importType, logger); + const { song, chart } = await ResolveMatchTypeToKTData(data, context, importType, logger); - const { percent, grade } = GenericGetGradeAndPercent(context.game, data.score, chart); + const { percent, grade } = GenericGetGradeAndPercent(context.game, data.score, chart); - let service = context.service; + let service = context.service; - if (importType === "ir/direct-manual") { - service += " (DIRECT-MANUAL)"; - } else if (importType === "file/batch-manual") { - service += " (BATCH-MANUAL)"; - } + if (importType === "ir/direct-manual") { + service += " (DIRECT-MANUAL)"; + } else if (importType === "file/batch-manual") { + service += " (BATCH-MANUAL)"; + } - const dryScore: DryScore = { - game: game, - service, - comment: data.comment ?? null, - importType, - timeAchieved: data.timeAchieved ?? null, - scoreData: { - lamp: data.lamp, - score: data.score, - grade, - percent, - hitData: data.hitData ?? {}, - hitMeta: data.hitMeta ?? {}, - }, - scoreMeta: {}, // @todo #74 - }; + const dryScore: DryScore = { + game: game, + service, + comment: data.comment ?? null, + importType, + timeAchieved: data.timeAchieved ?? null, + scoreData: { + lamp: data.lamp, + score: data.score, + grade, + percent, + hitData: data.hitData ?? {}, + hitMeta: data.hitMeta ?? {}, + }, + scoreMeta: {}, // @todo #74 + }; - return { - chart, - song, - dryScore, - }; + return { + chart, + song, + dryScore, + }; }; export async function ResolveMatchTypeToKTData( - data: BatchManualScore, - context: BatchManualContext, - importType: ImportTypes, - logger: KtLogger + data: BatchManualScore, + context: BatchManualContext, + importType: ImportTypes, + logger: KtLogger ): Promise<{ song: AnySongDocument; chart: AnyChartDocument }> { - const game = context.game; + const game = context.game; - if (data.matchType === "bmsChartHash" || data.matchType === "hash") { - if (game !== "bms") { - throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`); - } + if (data.matchType === "bmsChartHash" || data.matchType === "hash") { + if (game !== "bms") { + throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`); + } - const chart = await FindBMSChartOnHash(data.identifier); + const chart = await FindBMSChartOnHash(data.identifier); - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for hash ${data.identifier}.`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for hash ${data.identifier}.`, + importType, + data, + context + ); + } - const song = await FindSongOnID(game, chart.songID); + const song = await FindSongOnID(game, chart.songID); - if (!song) { - logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`); - throw new InternalFailure(`BMS songID ${chart.songID} has charts but no parent song.`); - } + if (!song) { + logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`); + throw new InternalFailure(`BMS songID ${chart.songID} has charts but no parent song.`); + } - return { chart, song }; - } else if (data.matchType === "ddrSongHash" || data.matchType === "songHash") { - if (game !== "ddr") { - throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`); - } + return { chart, song }; + } else if (data.matchType === "ddrSongHash" || data.matchType === "songHash") { + if (game !== "ddr") { + throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`); + } - if (!data.difficulty) { - throw new InvalidScoreFailure( - `Missing 'difficulty' field, but is needed for ddrSongHash lookup.` - ); - } + if (!data.difficulty) { + throw new InvalidScoreFailure( + `Missing 'difficulty' field, but is needed for ddrSongHash lookup.` + ); + } - if (!data.playtype) { - throw new InvalidScoreFailure( - `Missing 'playtype' field, but is needed for ddrSongHash lookup.` - ); - } + if (!data.playtype) { + throw new InvalidScoreFailure( + `Missing 'playtype' field, but is needed for ddrSongHash lookup.` + ); + } - const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype); + const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype); - const chart = await FindDDRChartOnSongHash(data.identifier, data.playtype, difficulty); + const chart = await FindDDRChartOnSongHash(data.identifier, data.playtype, difficulty); - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for songHash ${data.identifier} (${data.playtype} ${difficulty}).`, + importType, + data, + context + ); + } - const song = await FindSongOnID(game, chart.songID); + const song = await FindSongOnID(game, chart.songID); - if (!song) { - logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`); - throw new InternalFailure(`DDR songID ${chart.songID} has charts but no parent song.`); - } + if (!song) { + logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`); + throw new InternalFailure(`DDR songID ${chart.songID} has charts but no parent song.`); + } - return { song, chart }; - } else if (data.matchType === "kamaitachiSongID" || data.matchType === "songID") { - const songID = AssertStrAsPositiveInt( - data.identifier, - "Invalid songID - must be a stringified positive integer." - ); + return { song, chart }; + } else if (data.matchType === "kamaitachiSongID" || data.matchType === "songID") { + const songID = AssertStrAsPositiveInt( + data.identifier, + "Invalid songID - must be a stringified positive integer." + ); - const song = await FindSongOnID(game, songID); + const song = await FindSongOnID(game, songID); - if (!song) { - throw new KTDataNotFoundFailure( - `Cannot find song with songID ${data.identifier}.`, - importType, - data, - context - ); - } + if (!song) { + throw new KTDataNotFoundFailure( + `Cannot find song with songID ${data.identifier}.`, + importType, + data, + context + ); + } - const chart = await ResolveChartFromSong(song, data, context, importType); + const chart = await ResolveChartFromSong(song, data, context, importType); - return { song, chart }; - } else if (data.matchType === "songTitle" || data.matchType === "title") { - const song = await FindSongOnTitleInsensitive(game, data.identifier); + return { song, chart }; + } else if (data.matchType === "songTitle" || data.matchType === "title") { + const song = await FindSongOnTitleInsensitive(game, data.identifier); - if (!song) { - throw new KTDataNotFoundFailure( - `Cannot find song with title ${data.identifier}.`, - importType, - data, - context - ); - } + if (!song) { + throw new KTDataNotFoundFailure( + `Cannot find song with title ${data.identifier}.`, + importType, + data, + context + ); + } - const chart = await ResolveChartFromSong(song, data, context, importType); + const chart = await ResolveChartFromSong(song, data, context, importType); - return { song, chart }; - } + return { song, chart }; + } - logger.error( - `Invalid matchType ${data.matchType} ended up in conversion - should have been rejected by prudence?` - ); - // really, this could be a larger error. - it's an internal failure because prudence should reject this. - throw new InvalidScoreFailure(`Invalid matchType ${data.matchType}.`); + logger.error( + `Invalid matchType ${data.matchType} ended up in conversion - should have been rejected by prudence?` + ); + // really, this could be a larger error. - it's an internal failure because prudence should reject this. + throw new InvalidScoreFailure(`Invalid matchType ${data.matchType}.`); } export async function ResolveChartFromSong( - song: AnySongDocument, - data: BatchManualScore, - context: BatchManualContext, - importType: ImportTypes + song: AnySongDocument, + data: BatchManualScore, + context: BatchManualContext, + importType: ImportTypes ) { - const game = context.game; + const game = context.game; - if (!data.difficulty) { - throw new InvalidScoreFailure( - `Missing 'difficulty' field, but was necessary for this lookup.` - ); - } + if (!data.difficulty) { + throw new InvalidScoreFailure( + `Missing 'difficulty' field, but was necessary for this lookup.` + ); + } - if (!data.playtype) { - throw new InvalidScoreFailure( - `Missing 'playtype' field, but was necessary for this lookup.` - ); - } + if (!data.playtype) { + throw new InvalidScoreFailure( + `Missing 'playtype' field, but was necessary for this lookup.` + ); + } - const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype); + const difficulty = AssertStrAsDifficulty(data.difficulty, game, data.playtype); - let chart; + let chart; - if (context.version) { - chart = await FindChartWithPTDFVersion( - game, - song.id, - data.playtype, - difficulty, - context.version - ); - } else { - chart = await FindChartWithPTDF(game, song.id, data.playtype, difficulty); - } + if (context.version) { + chart = await FindChartWithPTDFVersion( + game, + song.id, + data.playtype, + difficulty, + context.version + ); + } else { + chart = await FindChartWithPTDF(game, song.id, data.playtype, difficulty); + } - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for ${song.title} (${data.playtype} ${difficulty})`, + importType, + data, + context + ); + } - return chart; + return chart; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts index f63f63d36..d5628844d 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts @@ -8,456 +8,456 @@ import deepmerge from "deepmerge"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; const mockErr = (...msg: string[]) => - ({ - statusCode: 400, - message: new RegExp(msg.map((e) => `${EscapeStringRegexp(e)}.*`).join(""), "u"), - name: "Error", - } as unknown as ScoreImportFatalError); + ({ + statusCode: 400, + message: new RegExp(msg.map((e) => `${EscapeStringRegexp(e)}.*`).join(""), "u"), + name: "Error", + } as unknown as ScoreImportFatalError); const logger = CreateLogCtx(__filename); const baseBatchManual = { - body: [], - head: { service: "foo", game: "iidx" }, + body: [], + head: { service: "foo", game: "iidx" }, }; const baseBatchManualScore = { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", + score: 1000, + lamp: "HARD CLEAR", + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function dm(sc: any) { - return deepmerge( - baseBatchManual, - { body: [deepmerge(baseBatchManualScore, sc)] }, - { arrayMerge: (r, c) => c } - ); + return deepmerge( + baseBatchManual, + { body: [deepmerge(baseBatchManualScore, sc)] }, + { arrayMerge: (r, c) => c } + ); } t.test("#ParserFn", (t) => { - t.test("Non-Object", (t) => { - t.throws( - () => ParserFn(false, "file/batch-manual", logger), - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL (Not an object, recieved boolean.)" - ), - "Should throw an error." - ); + t.test("Non-Object", (t) => { + t.throws( + () => ParserFn(false, "file/batch-manual", logger), + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL (Not an object, recieved boolean.)" + ), + "Should throw an error." + ); - t.end(); - }); + t.end(); + }); - t.test("No Header", (t) => { - t.throws( - () => ParserFn({ body: [] }, "file/batch-manual", logger), - new ScoreImportFatalError( - 400, - "Could not retrieve head.game - is this valid BATCH-MANUAL?" - ), - "Should throw an error." - ); + t.test("No Header", (t) => { + t.throws( + () => ParserFn({ body: [] }, "file/batch-manual", logger), + new ScoreImportFatalError( + 400, + "Could not retrieve head.game - is this valid BATCH-MANUAL?" + ), + "Should throw an error." + ); - t.end(); - }); + t.end(); + }); - t.test("No Game", (t) => { - t.throws( - () => ParserFn({ body: [], head: { service: "foo" } }, "file/batch-manual", logger), - new ScoreImportFatalError( - 400, - "Could not retrieve head.game - is this valid BATCH-MANUAL?" - ), - "Should throw an error." - ); + t.test("No Game", (t) => { + t.throws( + () => ParserFn({ body: [], head: { service: "foo" } }, "file/batch-manual", logger), + new ScoreImportFatalError( + 400, + "Could not retrieve head.game - is this valid BATCH-MANUAL?" + ), + "Should throw an error." + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid Game", (t) => { - t.throws( - () => - ParserFn( - { body: [], head: { service: "foo", game: "invalid_game" } }, - "file/batch-manual", - logger - ), - "Should throw an error." - ); + t.test("Invalid Game", (t) => { + t.throws( + () => + ParserFn( + { body: [], head: { service: "foo", game: "invalid_game" } }, + "file/batch-manual", + logger + ), + "Should throw an error." + ); - t.throws( - () => - ParserFn( - { body: [], head: { service: "foo", game: 123 } }, - "file/batch-manual", - logger - ), - "Should throw an error." - ); + t.throws( + () => + ParserFn( + { body: [], head: { service: "foo", game: 123 } }, + "file/batch-manual", + logger + ), + "Should throw an error." + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid Service", (t) => { - t.throws( - () => - ParserFn( - { body: [], head: { service: "1", game: "iidx" } }, - "file/batch-manual", - logger - ), - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [string]." - ), - "Should throw an error." - ); + t.test("Invalid Service", (t) => { + t.throws( + () => + ParserFn( + { body: [], head: { service: "1", game: "iidx" } }, + "file/batch-manual", + logger + ), + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [string]." + ), + "Should throw an error." + ); - t.throws( - () => - ParserFn( - { body: [], head: { service: 1, game: "iidx" } }, - "file/batch-manual", - logger - ), - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [number]." - ), - "Should throw an error." - ); + t.throws( + () => + ParserFn( + { body: [], head: { service: 1, game: "iidx" } }, + "file/batch-manual", + logger + ), + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: head.service | Expected a string with length between 3 and 15. | Received 1 [number]." + ), + "Should throw an error." + ); - t.end(); - }); + t.end(); + }); - t.test("Valid Empty BATCH-MANUAL", (t) => { - const res = ParserFn( - { body: [], head: { service: "foo", game: "iidx" } }, - "file/batch-manual", - logger - ); + t.test("Valid Empty BATCH-MANUAL", (t) => { + const res = ParserFn( + { body: [], head: { service: "foo", game: "iidx" } }, + "file/batch-manual", + logger + ); - t.hasStrict(res, { - game: "iidx", - context: { - service: "foo", - game: "iidx", - version: null, - }, - iterable: [], - }); + t.hasStrict(res, { + game: "iidx", + context: { + service: "foo", + game: "iidx", + version: null, + }, + iterable: [], + }); - t.end(); - }); + t.end(); + }); - t.test("Valid BATCH-MANUAL", (t) => { - t.test("Basic BATCH-MANUAL", (t) => { - const res = ParserFn( - { - body: [ - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "kamaitachiSongID", - identifier: "123", - playtype: "DP", - difficulty: "HYPER", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songTitle", - identifier: "5.1.1.", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "title", - identifier: "5.1.1.", - }, - ], - head: { service: "foo", game: "iidx" }, - } as BatchManual, - "file/batch-manual", - logger - ); + t.test("Valid BATCH-MANUAL", (t) => { + t.test("Basic BATCH-MANUAL", (t) => { + const res = ParserFn( + { + body: [ + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "kamaitachiSongID", + identifier: "123", + playtype: "DP", + difficulty: "HYPER", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songTitle", + identifier: "5.1.1.", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "title", + identifier: "5.1.1.", + }, + ], + head: { service: "foo", game: "iidx" }, + } as BatchManual, + "file/batch-manual", + logger + ); - t.hasStrict(res, { - game: "iidx", - context: { - service: "foo", - game: "iidx", - version: null, - }, - iterable: [ - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "kamaitachiSongID", - identifier: "123", - playtype: "DP", - difficulty: "HYPER", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songTitle", - identifier: "5.1.1.", - }, - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "title", - identifier: "5.1.1.", - }, - ], - }); + t.hasStrict(res, { + game: "iidx", + context: { + service: "foo", + game: "iidx", + version: null, + }, + iterable: [ + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "kamaitachiSongID", + identifier: "123", + playtype: "DP", + difficulty: "HYPER", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songTitle", + identifier: "5.1.1.", + }, + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "title", + identifier: "5.1.1.", + }, + ], + }); - t.end(); - }); + t.end(); + }); - t.test("Valid HitMeta", (t) => { - const res = ParserFn( - dm({ hitMeta: { bp: 10, gauge: 100, gaugeHistory: null, comboBreak: 7 } }), - "file/batch-manual", - logger - ); + t.test("Valid HitMeta", (t) => { + const res = ParserFn( + dm({ hitMeta: { bp: 10, gauge: 100, gaugeHistory: null, comboBreak: 7 } }), + "file/batch-manual", + logger + ); - t.hasStrict(res, { - game: "iidx", - context: { - service: "foo", - game: "iidx", - version: null, - }, - iterable: [ - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", - hitMeta: { - bp: 10, - gauge: 100, - gaugeHistory: null, - comboBreak: 7, - }, - }, - ], - }); + t.hasStrict(res, { + game: "iidx", + context: { + service: "foo", + game: "iidx", + version: null, + }, + iterable: [ + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", + hitMeta: { + bp: 10, + gauge: 100, + gaugeHistory: null, + comboBreak: 7, + }, + }, + ], + }); - t.end(); - }); + t.end(); + }); - t.test("Valid HitData", (t) => { - const res = ParserFn( - dm({ hitData: { pgreat: 1, great: null, bad: 0 } }), - "file/batch-manual", - logger - ); + t.test("Valid HitData", (t) => { + const res = ParserFn( + dm({ hitData: { pgreat: 1, great: null, bad: 0 } }), + "file/batch-manual", + logger + ); - t.hasStrict(res, { - game: "iidx", - context: { - service: "foo", - game: "iidx", - version: null, - }, - iterable: [ - { - score: 1000, - lamp: "HARD CLEAR", - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", - hitData: { - pgreat: 1, - great: null, - bad: 0, - }, - }, - ], - }); + t.hasStrict(res, { + game: "iidx", + context: { + service: "foo", + game: "iidx", + version: null, + }, + iterable: [ + { + score: 1000, + lamp: "HARD CLEAR", + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", + hitData: { + pgreat: 1, + great: null, + bad: 0, + }, + }, + ], + }); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Invalid BATCH-MANUAL", (t) => { - t.test("Invalid Lamp For Game", (t) => { - const fn = () => - ParserFn( - { - body: [ - { - score: 1000, - lamp: "ALL JUSTICE", // not an iidx lamp - matchType: "songID", - identifier: "123", - playtype: "SP", - difficulty: "ANOTHER", - }, - ], - head: { service: "foo", game: "iidx" }, - }, - "file/batch-manual", - logger - ); + t.test("Invalid BATCH-MANUAL", (t) => { + t.test("Invalid Lamp For Game", (t) => { + const fn = () => + ParserFn( + { + body: [ + { + score: 1000, + lamp: "ALL JUSTICE", // not an iidx lamp + matchType: "songID", + identifier: "123", + playtype: "SP", + difficulty: "ANOTHER", + }, + ], + head: { service: "foo", game: "iidx" }, + }, + "file/batch-manual", + logger + ); - t.throws( - fn, - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: body[0].lamp | Expected any of NO PLAY, FAILED, ASSIST CLEAR, EASY CLEAR, CLEAR, HARD CLEAR, EX HARD CLEAR, FULL COMBO. | Received ALL JUSTICE [string]." - ) - ); + t.throws( + fn, + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: body[0].lamp | Expected any of NO PLAY, FAILED, ASSIST CLEAR, EASY CLEAR, CLEAR, HARD CLEAR, EX HARD CLEAR, FULL COMBO. | Received ALL JUSTICE [string]." + ) + ); - t.end(); - }); + t.end(); + }); - t.test("Non-numeric score", (t) => { - const fn = () => ParserFn(dm({ score: "123" }), "file/batch-manual", logger); + t.test("Non-numeric score", (t) => { + const fn = () => ParserFn(dm({ score: "123" }), "file/batch-manual", logger); - t.throws( - fn, - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: body[0].score | Expected number. | Received 123 [string]." - ) - ); + t.throws( + fn, + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: body[0].score | Expected number. | Received 123 [string]." + ) + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid timeAchieved", (t) => { - const fn = () => ParserFn(dm({ timeAchieved: "string" }), "file/batch-manual", logger); + t.test("Invalid timeAchieved", (t) => { + const fn = () => ParserFn(dm({ timeAchieved: "string" }), "file/batch-manual", logger); - t.throws( - fn, - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: body[0].timeAchieved | Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds? | Received string [string]." - ) - ); + t.throws( + fn, + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: body[0].timeAchieved | Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds? | Received string [string]." + ) + ); - const fn2 = () => - ParserFn( - dm({ timeAchieved: 1_620_768_609_637 / 1000 }), - "file/batch-manual", - logger - ); + const fn2 = () => + ParserFn( + dm({ timeAchieved: 1_620_768_609_637 / 1000 }), + "file/batch-manual", + logger + ); - t.throws( - fn2, - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: body[0].timeAchieved | Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds? | Received 1620768609.637 [number]." - ), - "Should throw if timeAchieved is less than 10_000_000_000." - ); + t.throws( + fn2, + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: body[0].timeAchieved | Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds? | Received 1620768609.637 [number]." + ), + "Should throw if timeAchieved is less than 10_000_000_000." + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid Playtype", (t) => { - // this is not a valid playtype for IIDX - const fn = () => ParserFn(dm({ playtype: "Single" }), "file/batch-manual", logger); + t.test("Invalid Playtype", (t) => { + // this is not a valid playtype for IIDX + const fn = () => ParserFn(dm({ playtype: "Single" }), "file/batch-manual", logger); - t.throws( - fn, - new ScoreImportFatalError( - 400, - "Invalid BATCH-MANUAL: body[0].playtype | Expected any of SP, DP. | Received Single [string]." - ) - ); + t.throws( + fn, + new ScoreImportFatalError( + 400, + "Invalid BATCH-MANUAL: body[0].playtype | Expected any of SP, DP. | Received Single [string]." + ) + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid Identifier", (t) => { - // this is not a valid playtype for IIDX - const fn = () => ParserFn(dm({ identifier: null }), "file/batch-manual", logger); + t.test("Invalid Identifier", (t) => { + // this is not a valid playtype for IIDX + const fn = () => ParserFn(dm({ identifier: null }), "file/batch-manual", logger); - t.throws(fn, mockErr("body[0].identifier | Expected string", "Received null [null]")); + t.throws(fn, mockErr("body[0].identifier | Expected string", "Received null [null]")); - t.end(); - }); + t.end(); + }); - t.test("Invalid MatchType", (t) => { - const fn = () => - ParserFn(dm({ matchType: "Invalid_MatchType" }), "file/batch-manual", logger); + t.test("Invalid MatchType", (t) => { + const fn = () => + ParserFn(dm({ matchType: "Invalid_MatchType" }), "file/batch-manual", logger); - t.throws( - fn, - mockErr( - "body[0].matchType | Expected any of", - "Received Invalid_MatchType [string]" - ) - ); + t.throws( + fn, + mockErr( + "body[0].matchType | Expected any of", + "Received Invalid_MatchType [string]" + ) + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid HitData", (t) => { - const fn = () => - ParserFn(dm({ hitData: { not_key: 123 } }), "file/batch-manual", logger); + t.test("Invalid HitData", (t) => { + const fn = () => + ParserFn(dm({ hitData: { not_key: 123 } }), "file/batch-manual", logger); - t.throws(fn, mockErr("body[0].hitData | Invalid Key not_key")); + t.throws(fn, mockErr("body[0].hitData | Invalid Key not_key")); - const fn2 = () => - ParserFn(dm({ hitData: { pgreat: "123" } }), "file/batch-manual", logger); + const fn2 = () => + ParserFn(dm({ hitData: { pgreat: "123" } }), "file/batch-manual", logger); - t.throws( - fn2, - mockErr("body[0].hitData | Key pgreat had an invalid value of 123 [string]") - ); + t.throws( + fn2, + mockErr("body[0].hitData | Key pgreat had an invalid value of 123 [string]") + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid HitMeta", (t) => { - const fn = () => - ParserFn(dm({ hitMeta: { not_key: 123 } }), "file/batch-manual", logger); + t.test("Invalid HitMeta", (t) => { + const fn = () => + ParserFn(dm({ hitMeta: { not_key: 123 } }), "file/batch-manual", logger); - t.throws(fn, mockErr("body[0].hitMeta | Unexpected")); + t.throws(fn, mockErr("body[0].hitMeta | Unexpected")); - const fn2 = () => ParserFn(dm({ hitMeta: { bp: -1 } }), "file/batch-manual", logger); + const fn2 = () => ParserFn(dm({ hitMeta: { bp: -1 } }), "file/batch-manual", logger); - t.throws(fn2, mockErr("body[0].hitMeta.bp | Expected a positive integer")); + t.throws(fn2, mockErr("body[0].hitMeta.bp | Expected a positive integer")); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts index 2eded9eea..deedf1368 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts @@ -12,99 +12,99 @@ import { CONF_INFO } from "../../../../setup/config"; const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v)); const BaseValidHitMeta = { - fast: optNull(p.isPositiveInteger), - slow: optNull(p.isPositiveInteger), - maxCombo: optNull(p.isPositiveInteger), + fast: optNull(p.isPositiveInteger), + slow: optNull(p.isPositiveInteger), + maxCombo: optNull(p.isPositiveInteger), }; const PR_HitMeta = (game: Game): PrudenceSchema => { - if (game === "iidx") { - return { - bp: optNull(p.isPositiveInteger), - gauge: optNull(p.isBoundedInteger(0, 100)), - gaugeHistory: optNull([p.isBoundedInteger(0, 100)]), - comboBreak: optNull(p.isPositiveInteger), - }; - } else if (game === "popn" || game === "sdvx" || game === "usc") { - return { - gauge: optNull(p.isBoundedInteger(0, 100)), - }; - } else if (game === "bms") { - return { - bp: optNull(p.isPositiveInteger), - gauge: optNull(p.isBoundedInteger(0, 100)), - lbd: optNull(p.isPositiveInteger), - ebd: optNull(p.isPositiveInteger), - lpr: optNull(p.isPositiveInteger), - epr: optNull(p.isPositiveInteger), - lgd: optNull(p.isPositiveInteger), - egd: optNull(p.isPositiveInteger), - lgr: optNull(p.isPositiveInteger), - egr: optNull(p.isPositiveInteger), - lpg: optNull(p.isPositiveInteger), - epg: optNull(p.isPositiveInteger), - diedAt: optNull(p.isPositiveInteger), - }; - } + if (game === "iidx") { + return { + bp: optNull(p.isPositiveInteger), + gauge: optNull(p.isBoundedInteger(0, 100)), + gaugeHistory: optNull([p.isBoundedInteger(0, 100)]), + comboBreak: optNull(p.isPositiveInteger), + }; + } else if (game === "popn" || game === "sdvx" || game === "usc") { + return { + gauge: optNull(p.isBoundedInteger(0, 100)), + }; + } else if (game === "bms") { + return { + bp: optNull(p.isPositiveInteger), + gauge: optNull(p.isBoundedInteger(0, 100)), + lbd: optNull(p.isPositiveInteger), + ebd: optNull(p.isPositiveInteger), + lpr: optNull(p.isPositiveInteger), + epr: optNull(p.isPositiveInteger), + lgd: optNull(p.isPositiveInteger), + egd: optNull(p.isPositiveInteger), + lgr: optNull(p.isPositiveInteger), + egr: optNull(p.isPositiveInteger), + lpg: optNull(p.isPositiveInteger), + epg: optNull(p.isPositiveInteger), + diedAt: optNull(p.isPositiveInteger), + }; + } - return {}; + return {}; }; const PR_BatchManualScore = (game: Game): PrudenceSchema => ({ - score: "number", - lamp: p.isIn(lamps[game]), - matchType: p.isIn( - "songTitle", - "ddrSongHash", - "kamaitachiSongID", - "bmsChartHash", - "title", - "songHash", - "songID", - "hash" - ), - identifier: "string", - comment: optNull(p.isBoundedString(3, 240)), - playtype: optNull(p.isIn(validPlaytypes[game])), - difficulty: "*?string", // this is checked in converting instead - // september 9th 2001 - this saves people not - // reading the documentation. - timeAchieved: optNull( - (self) => - (typeof self === "number" && self > 1_000_000_000_000) || - "Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds?" - ), - hitData: optNull((self) => { - if (typeof self !== "object" || self === null) { - return "Not a valid object."; - } + score: "number", + lamp: p.isIn(lamps[game]), + matchType: p.isIn( + "songTitle", + "ddrSongHash", + "kamaitachiSongID", + "bmsChartHash", + "title", + "songHash", + "songID", + "hash" + ), + identifier: "string", + comment: optNull(p.isBoundedString(3, 240)), + playtype: optNull(p.isIn(validPlaytypes[game])), + difficulty: "*?string", // this is checked in converting instead + // september 9th 2001 - this saves people not + // reading the documentation. + timeAchieved: optNull( + (self) => + (typeof self === "number" && self > 1_000_000_000_000) || + "Expected a number greater than 1 Trillion - did you pass unix seconds instead of miliseconds?" + ), + hitData: optNull((self) => { + if (typeof self !== "object" || self === null) { + return "Not a valid object."; + } - for (const key in self) { - if (!validHitData[game].includes(key)) { - return `Invalid Key ${key}. Expected any of ${validHitData[game].join(", ")}`; - } + for (const key in self) { + if (!validHitData[game].includes(key)) { + return `Invalid Key ${key}. Expected any of ${validHitData[game].join(", ")}`; + } - // @ts-expect-error shush - const v = self[key]; - if ((!Number.isSafeInteger(v) || v < 0) && v !== null) { - return `Key ${key} had an invalid value of ${v} [${typeof v}]`; - } - } + // @ts-expect-error shush + const v = self[key]; + if ((!Number.isSafeInteger(v) || v < 0) && v !== null) { + return `Key ${key} had an invalid value of ${v} [${typeof v}]`; + } + } - return true; - }), - hitMeta: optNull(deepmerge(BaseValidHitMeta, PR_HitMeta(game)) as unknown as ValidSchemaValue), - // scoreMeta: @todo #74 - // more game specific props, maybe? + return true; + }), + hitMeta: optNull(deepmerge(BaseValidHitMeta, PR_HitMeta(game)) as unknown as ValidSchemaValue), + // scoreMeta: @todo #74 + // more game specific props, maybe? }); const PR_BatchManual = (game: Game): PrudenceSchema => ({ - head: { - service: p.isBoundedString(3, 15), - game: p.isIn(CONF_INFO.SUPPORTED_GAMES), - version: "*?string", - }, - body: [PR_BatchManualScore(game)], + head: { + service: p.isBoundedString(3, 15), + game: p.isIn(CONF_INFO.SUPPORTED_GAMES), + version: "*?string", + }, + body: [PR_BatchManualScore(game)], }); /** @@ -113,63 +113,63 @@ const PR_BatchManual = (game: Game): PrudenceSchema => ({ * @param body - The request body that made this file import request. */ export function ParseBatchManualFromObject( - object: unknown, - importType: ImportTypes, - logger: KtLogger + object: unknown, + importType: ImportTypes, + logger: KtLogger ): ParserFunctionReturns { - // now to perform some basic validation so we can return - // the iterable + // now to perform some basic validation so we can return + // the iterable - if (typeof object !== "object" || object === null) { - throw new ScoreImportFatalError( - 400, - `Invalid BATCH-MANUAL (Not an object, recieved ${ - object === null ? "null" : typeof object - }.)` - ); - } + if (typeof object !== "object" || object === null) { + throw new ScoreImportFatalError( + 400, + `Invalid BATCH-MANUAL (Not an object, recieved ${ + object === null ? "null" : typeof object + }.)` + ); + } - // attempt to retrieve game - // @ts-expect-error man. - const possiblyGame = object?.head?.game; + // attempt to retrieve game + // @ts-expect-error man. + const possiblyGame = object?.head?.game; - if (!possiblyGame) { - throw new ScoreImportFatalError( - 400, - `Could not retrieve head.game - is this valid BATCH-MANUAL?` - ); - } + if (!possiblyGame) { + throw new ScoreImportFatalError( + 400, + `Could not retrieve head.game - is this valid BATCH-MANUAL?` + ); + } - if (!CONF_INFO.SUPPORTED_GAMES.includes(possiblyGame)) { - throw new ScoreImportFatalError( - 400, - `Invalid game ${possiblyGame} - expected any of ${CONF_INFO.SUPPORTED_GAMES.join(", ")}` - ); - } + if (!CONF_INFO.SUPPORTED_GAMES.includes(possiblyGame)) { + throw new ScoreImportFatalError( + 400, + `Invalid game ${possiblyGame} - expected any of ${CONF_INFO.SUPPORTED_GAMES.join(", ")}` + ); + } - const game: Game = possiblyGame; + const game: Game = possiblyGame; - // now that we have the game, we can validate this against - // the prudence schema for batch-manual. - // This mostly works as a sanity check, and doesn't - // check things like whether a score is > 100% - // or something. - const err = p(object, PR_BatchManual(game)); + // now that we have the game, we can validate this against + // the prudence schema for batch-manual. + // This mostly works as a sanity check, and doesn't + // check things like whether a score is > 100% + // or something. + const err = p(object, PR_BatchManual(game)); - if (err) { - throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid BATCH-MANUAL")); - } + if (err) { + throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid BATCH-MANUAL")); + } - const batchManual = object as BatchManual; + const batchManual = object as BatchManual; - return { - game, - context: { - service: batchManual.head.service, - game, - version: batchManual.head.version ?? null, - }, - iterable: batchManual.body, - classHandler: null, - }; + return { + game, + context: { + service: batchManual.head.service, + game, + version: batchManual.head.version ?? null, + }, + iterable: batchManual.body, + classHandler: null, + }; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/types.ts b/server/src/lib/score-import/import-types/common/batch-manual/types.ts index 0e37cecb9..96ce1203b 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/types.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/types.ts @@ -1,38 +1,38 @@ import { Game, Playtypes, Lamps, IDStrings } from "tachi-common"; export type BatchManualScore = { - score: number; - lamp: Lamps[IDStrings]; - timeAchieved?: number | null; - comment?: string | null; - hitData?: Record | null; - hitMeta?: Record | null; - identifier: string; - matchType: - | "songTitle" - | "ddrSongHash" - | "kamaitachiSongID" - | "bmsChartHash" - | "title" // title is legacy - use songTitle - | "songHash" // songHash is legacy - use ddrSongHash - | "songID" // songID is legacy - use kamaitachiSongID - | "hash"; // hash is legacy - use bmsChartHash + score: number; + lamp: Lamps[IDStrings]; + timeAchieved?: number | null; + comment?: string | null; + hitData?: Record | null; + hitMeta?: Record | null; + identifier: string; + matchType: + | "songTitle" + | "ddrSongHash" + | "kamaitachiSongID" + | "bmsChartHash" + | "title" // title is legacy - use songTitle + | "songHash" // songHash is legacy - use ddrSongHash + | "songID" // songID is legacy - use kamaitachiSongID + | "hash"; // hash is legacy - use bmsChartHash - playtype?: Playtypes[Game] | null; - difficulty?: string | null; // lazy... + playtype?: Playtypes[Game] | null; + difficulty?: string | null; // lazy... }; export interface BatchManual { - head: { - service: string; - game: Game; - version?: string | null; - }; - body: BatchManualScore[]; + head: { + service: string; + game: Game; + version?: string | null; + }; + body: BatchManualScore[]; } export interface BatchManualContext { - game: Game; - service: string; - version: string | null; + game: Game; + service: string; + version: string | null; } diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.test.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.test.ts index 6c2fa7ea9..b1cedaf8c 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.test.ts @@ -6,32 +6,32 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); const valid511Score = { - bp: "12", - difficulty: "ANOTHER" as const, // lol - exscore: "192", - great: "42", - pgreat: "75", - lamp: "HARD CLEAR", - level: "10", + bp: "12", + difficulty: "ANOTHER" as const, // lol + exscore: "192", + great: "42", + pgreat: "75", + lamp: "HARD CLEAR", + level: "10", }; const converterContext = { - playtype: "SP" as const, - hasBeginnerAndLegg: false, - importVersion: "27", - service: "e-amusement", + playtype: "SP" as const, + hasBeginnerAndLegg: false, + importVersion: "27", + service: "e-amusement", }; const data = { - scores: [valid511Score], - timestamp: "Tue, 27 Apr 2021 21:35:35 GMT", - title: "5.1.1", + scores: [valid511Score], + timestamp: "Tue, 27 Apr 2021 21:35:35 GMT", + title: "5.1.1", }; t.todo("#ConverterFn", async (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts index 2f39704b5..e5047db44 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts @@ -2,13 +2,13 @@ import { Lamps, ChartDocument } from "tachi-common"; import { FindChartWithPTDFVersion } from "../../../../../utils/queries/charts"; import { FindSongOnTitle } from "../../../../../utils/queries/songs"; import { - KTDataNotFoundFailure, - InvalidScoreFailure, - SkipScoreFailure, + KTDataNotFoundFailure, + InvalidScoreFailure, + SkipScoreFailure, } from "../../../framework/common/converter-failures"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; import { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; @@ -16,163 +16,163 @@ import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../types"; const EAMUSEMENT_LAMP_RESOLVER: Map = new Map([ - ["NO PLAY", "NO PLAY"], - ["FAILED", "FAILED"], - ["FULLCOMBO CLEAR", "FULL COMBO"], - ["EX HARD CLEAR", "EX HARD CLEAR"], - ["HARD CLEAR", "HARD CLEAR"], - ["CLEAR", "CLEAR"], - ["EASY CLEAR", "EASY CLEAR"], - ["ASSIST CLEAR", "ASSIST CLEAR"], + ["NO PLAY", "NO PLAY"], + ["FAILED", "FAILED"], + ["FULLCOMBO CLEAR", "FULL COMBO"], + ["EX HARD CLEAR", "EX HARD CLEAR"], + ["HARD CLEAR", "HARD CLEAR"], + ["CLEAR", "CLEAR"], + ["EASY CLEAR", "EASY CLEAR"], + ["ASSIST CLEAR", "ASSIST CLEAR"], ]); const NINE_HOURS = 1000 * 60 * 60 * 9; const ConvertEamIIDXCSV: ConverterFunction = - async (data, context, importType, logger) => { - const eamScore = data.score; + async (data, context, importType, logger) => { + const eamScore = data.score; - if (eamScore.exscore === "0") { - // skip scores with an exscore of 0 - // This also skips things like score resets. - throw new SkipScoreFailure("Score has an exscore of 0."); - } + if (eamScore.exscore === "0") { + // skip scores with an exscore of 0 + // This also skips things like score resets. + throw new SkipScoreFailure("Score has an exscore of 0."); + } - if (!eamScore.level || eamScore.level === "0") { - // charts that dont exist in the format have a level of 0 - throw new SkipScoreFailure("Chart has a level of 0."); - } + if (!eamScore.level || eamScore.level === "0") { + // charts that dont exist in the format have a level of 0 + throw new SkipScoreFailure("Chart has a level of 0."); + } - let isLegacyLeggendaria = false; + let isLegacyLeggendaria = false; - // if pre-HV, leggendarias were stored in a wacky form. - if (!context.hasBeginnerAndLegg) { - // hack fix for legacy LEGGENDARIA titles - if (data.title.match(/(†|†LEGGENDARIA)$/u)) { - data.title = data.title.replace(/(†|†LEGGENDARIA)$/u, "").trimEnd(); - isLegacyLeggendaria = true; - } - } + // if pre-HV, leggendarias were stored in a wacky form. + if (!context.hasBeginnerAndLegg) { + // hack fix for legacy LEGGENDARIA titles + if (data.title.match(/(†|†LEGGENDARIA)$/u)) { + data.title = data.title.replace(/(†|†LEGGENDARIA)$/u, "").trimEnd(); + isLegacyLeggendaria = true; + } + } - // @optimisable - This is actually a multi-fetch. Since eam-csv scores - // are batched up into (song, chart1, chart2, chart3 ...) rows - // we actually already have fetched this song a second ago. - const tachiSong = await FindSongOnTitle("iidx", data.title); + // @optimisable - This is actually a multi-fetch. Since eam-csv scores + // are batched up into (song, chart1, chart2, chart3 ...) rows + // we actually already have fetched this song a second ago. + const tachiSong = await FindSongOnTitle("iidx", data.title); - if (!tachiSong) { - throw new KTDataNotFoundFailure( - `Could not find song for ${data.title}.`, - importType, - data, - context - ); - } + if (!tachiSong) { + throw new KTDataNotFoundFailure( + `Could not find song for ${data.title}.`, + importType, + data, + context + ); + } - const HUMANISED_CHART_TITLE = `${tachiSong.title} (${context.playtype} ${eamScore.difficulty} [v${context.importVersion}])`; + const HUMANISED_CHART_TITLE = `${tachiSong.title} (${context.playtype} ${eamScore.difficulty} [v${context.importVersion}])`; - if (isLegacyLeggendaria) { - eamScore.difficulty = "LEGGENDARIA"; - } + if (isLegacyLeggendaria) { + eamScore.difficulty = "LEGGENDARIA"; + } - const tachiChart = (await FindChartWithPTDFVersion( - "iidx", - tachiSong.id, - context.playtype, - eamScore.difficulty, - context.importVersion - )) as ChartDocument<"iidx:SP" | "iidx:DP">; + const tachiChart = (await FindChartWithPTDFVersion( + "iidx", + tachiSong.id, + context.playtype, + eamScore.difficulty, + context.importVersion + )) as ChartDocument<"iidx:SP" | "iidx:DP">; - if (!tachiChart) { - throw new KTDataNotFoundFailure( - `Could not find chart for ${HUMANISED_CHART_TITLE}`, - "file/eamusement-iidx-csv", - data, - context - ); - } + if (!tachiChart) { + throw new KTDataNotFoundFailure( + `Could not find chart for ${HUMANISED_CHART_TITLE}`, + "file/eamusement-iidx-csv", + data, + context + ); + } - const exscore = AssertStrAsPositiveInt( - eamScore.exscore, - `${HUMANISED_CHART_TITLE} - Invalid EX score of ${eamScore.exscore}` - ); + const exscore = AssertStrAsPositiveInt( + eamScore.exscore, + `${HUMANISED_CHART_TITLE} - Invalid EX score of ${eamScore.exscore}` + ); - const MAX_EX = tachiChart.data.notecount * 2; + const MAX_EX = tachiChart.data.notecount * 2; - if (exscore > MAX_EX) { - throw new InvalidScoreFailure( - `${HUMANISED_CHART_TITLE} - Invalid EX Score of ${eamScore.exscore} (Was greater than max chart ex of ${MAX_EX}).` - ); - } + if (exscore > MAX_EX) { + throw new InvalidScoreFailure( + `${HUMANISED_CHART_TITLE} - Invalid EX Score of ${eamScore.exscore} (Was greater than max chart ex of ${MAX_EX}).` + ); + } - const pgreat = AssertStrAsPositiveInt( - eamScore.pgreat, - `${HUMANISED_CHART_TITLE} - Invalid PGreats of ${eamScore.pgreat}` - ); + const pgreat = AssertStrAsPositiveInt( + eamScore.pgreat, + `${HUMANISED_CHART_TITLE} - Invalid PGreats of ${eamScore.pgreat}` + ); - const great = AssertStrAsPositiveInt( - eamScore.great, - `${HUMANISED_CHART_TITLE} - Invalid Greats of ${eamScore.pgreat}` - ); + const great = AssertStrAsPositiveInt( + eamScore.great, + `${HUMANISED_CHART_TITLE} - Invalid Greats of ${eamScore.pgreat}` + ); - if (pgreat * 2 + great !== exscore) { - throw new InvalidScoreFailure( - `${HUMANISED_CHART_TITLE} - PGreats * 2 + Greats did not equal EXScore (${pgreat} * 2 + ${great} != ${exscore}).` - ); - } + if (pgreat * 2 + great !== exscore) { + throw new InvalidScoreFailure( + `${HUMANISED_CHART_TITLE} - PGreats * 2 + Greats did not equal EXScore (${pgreat} * 2 + ${great} != ${exscore}).` + ); + } - const lamp = EAMUSEMENT_LAMP_RESOLVER.get(eamScore.lamp); + const lamp = EAMUSEMENT_LAMP_RESOLVER.get(eamScore.lamp); - if (!lamp) { - logger.info(`Invalid lamp of ${eamScore.lamp} provided.`); - throw new InvalidScoreFailure( - `${HUMANISED_CHART_TITLE} - Invalid Lamp of ${eamScore.lamp}.` - ); - } + if (!lamp) { + logger.info(`Invalid lamp of ${eamScore.lamp} provided.`); + throw new InvalidScoreFailure( + `${HUMANISED_CHART_TITLE} - Invalid Lamp of ${eamScore.lamp}.` + ); + } - const { percent, grade } = GenericGetGradeAndPercent("iidx", exscore, tachiChart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", exscore, tachiChart); - const timestamp = ParseDateFromString(data.timestamp); + const timestamp = ParseDateFromString(data.timestamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - service: context.service, - comment: null, - game: "iidx", - importType, - scoreData: { - score: exscore, - lamp, - hitData: { - pgreat, - great, - }, - hitMeta: {}, - percent, - grade, - }, - scoreMeta: {}, - // japan is gmt+9 - timeAchieved: timestamp ? timestamp - NINE_HOURS : null, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + service: context.service, + comment: null, + game: "iidx", + importType, + scoreData: { + score: exscore, + lamp, + hitData: { + pgreat, + great, + }, + hitMeta: {}, + percent, + grade, + }, + scoreMeta: {}, + // japan is gmt+9 + timeAchieved: timestamp ? timestamp - NINE_HOURS : null, + }; - const numBP = Number(eamScore.bp); + const numBP = Number(eamScore.bp); - if (!Number.isNaN(numBP)) { - if (!Number.isInteger(numBP) || numBP < 0 || numBP > 9999) { - throw new InvalidScoreFailure( - `${HUMANISED_CHART_TITLE} - Invalid BP of ${eamScore.bp}.` - ); - } - dryScore.scoreData.hitMeta.bp = numBP; - } else if (eamScore.bp === "---") { - logger.debug( - `Skipped assigning BP for score as it had expected null value of ${eamScore.bp}.` - ); - } else { - logger.info(`Skipped assigning BP for score. Had unexpected value of ${eamScore.bp}.`); - } + if (!Number.isNaN(numBP)) { + if (!Number.isInteger(numBP) || numBP < 0 || numBP > 9999) { + throw new InvalidScoreFailure( + `${HUMANISED_CHART_TITLE} - Invalid BP of ${eamScore.bp}.` + ); + } + dryScore.scoreData.hitMeta.bp = numBP; + } else if (eamScore.bp === "---") { + logger.debug( + `Skipped assigning BP for score as it had expected null value of ${eamScore.bp}.` + ); + } else { + logger.info(`Skipped assigning BP for score. Had unexpected value of ${eamScore.bp}.`); + } - // ts thinks tachiSong might be null. It's not, though! - return { chart: tachiChart, dryScore, song: tachiSong }; - }; + // ts thinks tachiSong might be null. It's not, though! + return { chart: tachiChart, dryScore, song: tachiSong }; + }; export default ConvertEamIIDXCSV; diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts index 241706b6d..2875d7dcc 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts @@ -3,8 +3,8 @@ import CreateLogCtx from "../../../../logger/logger"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import GenericParseEamIIDXCSV, { NaiveCSVParse, ResolveHeaders } from "./parser"; import { - TestingIIDXEamusementCSV26, - TestingIIDXEamusementCSV27, + TestingIIDXEamusementCSV26, + TestingIIDXEamusementCSV27, } from "../../../../../test-utils/test-data"; import { MockMulterFile } from "../../../../../test-utils/mock-multer"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; @@ -12,296 +12,296 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#ParseEamusementCSV", (t) => { - t.test("Valid Rootage-Type CSV", (t) => { - const { iterableData, hasBeginnerAndLegg, version } = NaiveCSVParse( - TestingIIDXEamusementCSV26, - logger - ); + t.test("Valid Rootage-Type CSV", (t) => { + const { iterableData, hasBeginnerAndLegg, version } = NaiveCSVParse( + TestingIIDXEamusementCSV26, + logger + ); - t.equal(iterableData.length, 456, "Should return exactly 456 datapoints."); - t.equal(hasBeginnerAndLegg, false, "Should not mark as a HV csv."); - t.equal(version, "26", "Should correctly assert that the version of this CSV is ROOTAGE."); + t.equal(iterableData.length, 456, "Should return exactly 456 datapoints."); + t.equal(hasBeginnerAndLegg, false, "Should not mark as a HV csv."); + t.equal(version, "26", "Should correctly assert that the version of this CSV is ROOTAGE."); - t.end(); - }); + t.end(); + }); - t.test("Valid HV CSV", (t) => { - const { iterableData, hasBeginnerAndLegg, version } = NaiveCSVParse( - TestingIIDXEamusementCSV27, - logger - ); + t.test("Valid HV CSV", (t) => { + const { iterableData, hasBeginnerAndLegg, version } = NaiveCSVParse( + TestingIIDXEamusementCSV27, + logger + ); - t.equal(iterableData.length, 6285, "Should return exactly 6285 datapoints."); - t.equal(hasBeginnerAndLegg, true, "Should mark as a HV csv."); - t.equal( - version, - "27", - "Should correctly assert that the version of this CSV is HEROIC VERSE." - ); + t.equal(iterableData.length, 6285, "Should return exactly 6285 datapoints."); + t.equal(hasBeginnerAndLegg, true, "Should mark as a HV csv."); + t.equal( + version, + "27", + "Should correctly assert that the version of this CSV is HEROIC VERSE." + ); - t.end(); - }); + t.end(); + }); - t.test("Malicious Headers", (t) => { - // these headers are valid, because we don't check the contents - // only that it has the right amt of headers - const headerStr = `${"a,".repeat(26)}a`; + t.test("Malicious Headers", (t) => { + // these headers are valid, because we don't check the contents + // only that it has the right amt of headers + const headerStr = `${"a,".repeat(26)}a`; - const TooShort = Buffer.from(`${headerStr}\n${"a,".repeat(3)}a`); + const TooShort = Buffer.from(`${headerStr}\n${"a,".repeat(3)}a`); - t.throws( - () => NaiveCSVParse(TooShort, logger), - new ScoreImportFatalError(400, "Row 1 has an invalid amount of cells (4, expected 27)") - ); + t.throws( + () => NaiveCSVParse(TooShort, logger), + new ScoreImportFatalError(400, "Row 1 has an invalid amount of cells (4, expected 27)") + ); - const TooLong = Buffer.from(`${headerStr}\n${"a,".repeat(50)}a`); + const TooLong = Buffer.from(`${headerStr}\n${"a,".repeat(50)}a`); - t.throws( - () => NaiveCSVParse(TooLong, logger), - new ScoreImportFatalError(400, "Row 1 has an invalid amount of cells (51, expected 27)") - ); + t.throws( + () => NaiveCSVParse(TooLong, logger), + new ScoreImportFatalError(400, "Row 1 has an invalid amount of cells (51, expected 27)") + ); - t.end(); - }); + t.end(); + }); - t.test("Misshaped Rows", (t) => { - const LongHeaders = Buffer.from(`${"a".repeat(1000)},a`); + t.test("Misshaped Rows", (t) => { + const LongHeaders = Buffer.from(`${"a".repeat(1000)},a`); - t.throws( - () => NaiveCSVParse(LongHeaders, logger), - new ScoreImportFatalError(400, "Headers were longer than 1000 characters long.") - ); + t.throws( + () => NaiveCSVParse(LongHeaders, logger), + new ScoreImportFatalError(400, "Headers were longer than 1000 characters long.") + ); - const TooManyHeaders = Buffer.from(`${"a,".repeat(50)}a`); + const TooManyHeaders = Buffer.from(`${"a,".repeat(50)}a`); - t.throws( - () => NaiveCSVParse(TooManyHeaders, logger), - new ScoreImportFatalError(400, "Too many CSV headers.") - ); + t.throws( + () => NaiveCSVParse(TooManyHeaders, logger), + new ScoreImportFatalError(400, "Too many CSV headers.") + ); - t.end(); - }); + t.end(); + }); - t.test("Version Inference", (t) => { - const headerStr = `${"a,".repeat(26)}a`; + t.test("Version Inference", (t) => { + const headerStr = `${"a,".repeat(26)}a`; - const row = `GARBAGE VERSION,foo bar,${"a,".repeat(24)}a`; + const row = `GARBAGE VERSION,foo bar,${"a,".repeat(24)}a`; - const InvalidVersions = Buffer.from(`${headerStr}\n${row}`); + const InvalidVersions = Buffer.from(`${headerStr}\n${row}`); - t.throws( - () => NaiveCSVParse(InvalidVersions, logger), - new ScoreImportFatalError( - 400, - "Invalid/Unsupported Eamusement Version Name GARBAGE VERSION." - ) - ); + t.throws( + () => NaiveCSVParse(InvalidVersions, logger), + new ScoreImportFatalError( + 400, + "Invalid/Unsupported Eamusement Version Name GARBAGE VERSION." + ) + ); - const row27th = `HEROIC VERSE,foo bar,${"a,".repeat(24)}a`; - const row17th = `SIRIUS,foo bar,${"a,".repeat(24)}a`; + const row27th = `HEROIC VERSE,foo bar,${"a,".repeat(24)}a`; + const row17th = `SIRIUS,foo bar,${"a,".repeat(24)}a`; - let { version } = NaiveCSVParse( - Buffer.from([headerStr, row27th, row17th].join("\n")), - logger - ); + let { version } = NaiveCSVParse( + Buffer.from([headerStr, row27th, row17th].join("\n")), + logger + ); - t.equal(version, "27", "Should pick the largest version from the list of scores."); + t.equal(version, "27", "Should pick the largest version from the list of scores."); - ({ version } = NaiveCSVParse( - Buffer.from([headerStr, row17th, row27th].join("\n")), - logger - )); + ({ version } = NaiveCSVParse( + Buffer.from([headerStr, row17th, row27th].join("\n")), + logger + )); - // this is technically allowing invalid eam-csv, but, who cares? - t.equal( - version, - "27", - "Should pick largest version regardless of order scores are recieved in." - ); + // this is technically allowing invalid eam-csv, but, who cares? + t.equal( + version, + "27", + "Should pick largest version regardless of order scores are recieved in." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ResolveHeader", (t) => { - t.test("Rootage-type Headers", (t) => { - const { hasBeginnerAndLegg } = ResolveHeaders( - [ - "バージョン", - "タイトル", - "ジャンル", - "アーティスト", - "プレー回数", - "NORMAL 難易度", - "NORMAL EXスコア", - "NORMAL PGreat", - "NORMAL Great", - "NORMAL ミスカウント", - "NORMAL クリアタイプ", - "NORMAL DJ LEVEL", - "HYPER 難易度", - "HYPER EXスコア", - "HYPER PGreat", - "HYPER Great", - "HYPER ミスカウント", - "HYPER クリアタイプ", - "HYPER DJ LEVEL", - "ANOTHER 難易度", - "ANOTHER EXスコア", - "ANOTHER PGreat", - "ANOTHER Great", - "ANOTHER ミスカウント", - "ANOTHER クリアタイプ", - "ANOTHER DJ LEVEL", - "最終プレー日時", - ], - logger - ); + t.test("Rootage-type Headers", (t) => { + const { hasBeginnerAndLegg } = ResolveHeaders( + [ + "バージョン", + "タイトル", + "ジャンル", + "アーティスト", + "プレー回数", + "NORMAL 難易度", + "NORMAL EXスコア", + "NORMAL PGreat", + "NORMAL Great", + "NORMAL ミスカウント", + "NORMAL クリアタイプ", + "NORMAL DJ LEVEL", + "HYPER 難易度", + "HYPER EXスコア", + "HYPER PGreat", + "HYPER Great", + "HYPER ミスカウント", + "HYPER クリアタイプ", + "HYPER DJ LEVEL", + "ANOTHER 難易度", + "ANOTHER EXスコア", + "ANOTHER PGreat", + "ANOTHER Great", + "ANOTHER ミスカウント", + "ANOTHER クリアタイプ", + "ANOTHER DJ LEVEL", + "最終プレー日時", + ], + logger + ); - t.equal(hasBeginnerAndLegg, false, "Should return false for hasBeginnerAndLegg"); + t.equal(hasBeginnerAndLegg, false, "Should return false for hasBeginnerAndLegg"); - t.end(); - }); + t.end(); + }); - t.test("HV-type Headers", (t) => { - const { hasBeginnerAndLegg } = ResolveHeaders( - [ - "バージョン", - "タイトル", - "ジャンル", - "アーティスト", - "プレー回数", - "BEGINNER 難易度", - "BEGINNER スコア", - "BEGINNER PGreat", - "BEGINNER Great", - "BEGINNER ミスカウント", - "BEGINNER クリアタイプ", - "BEGINNER DJ LEVEL", - "NORMAL 難易度", - "NORMAL スコア", - "NORMAL PGreat", - "NORMAL Great", - "NORMAL ミスカウント", - "NORMAL クリアタイプ", - "NORMAL DJ LEVEL", - "HYPER 難易度", - "HYPER スコア", - "HYPER PGreat", - "HYPER Great", - "HYPER ミスカウント", - "HYPER クリアタイプ", - "HYPER DJ LEVEL", - "ANOTHER 難易度", - "ANOTHER スコア", - "ANOTHER PGreat", - "ANOTHER Great", - "ANOTHER ミスカウント", - "ANOTHER クリアタイプ", - "ANOTHER DJ LEVEL", - "LEGGENDARIA 難易度", - "LEGGENDARIA スコア", - "LEGGENDARIA PGreat", - "LEGGENDARIA Great", - "LEGGENDARIA ミスカウント", - "LEGGENDARIA クリアタイプ", - "LEGGENDARIA DJ LEVEL", - "最終プレー日時", - ], - logger - ); + t.test("HV-type Headers", (t) => { + const { hasBeginnerAndLegg } = ResolveHeaders( + [ + "バージョン", + "タイトル", + "ジャンル", + "アーティスト", + "プレー回数", + "BEGINNER 難易度", + "BEGINNER スコア", + "BEGINNER PGreat", + "BEGINNER Great", + "BEGINNER ミスカウント", + "BEGINNER クリアタイプ", + "BEGINNER DJ LEVEL", + "NORMAL 難易度", + "NORMAL スコア", + "NORMAL PGreat", + "NORMAL Great", + "NORMAL ミスカウント", + "NORMAL クリアタイプ", + "NORMAL DJ LEVEL", + "HYPER 難易度", + "HYPER スコア", + "HYPER PGreat", + "HYPER Great", + "HYPER ミスカウント", + "HYPER クリアタイプ", + "HYPER DJ LEVEL", + "ANOTHER 難易度", + "ANOTHER スコア", + "ANOTHER PGreat", + "ANOTHER Great", + "ANOTHER ミスカウント", + "ANOTHER クリアタイプ", + "ANOTHER DJ LEVEL", + "LEGGENDARIA 難易度", + "LEGGENDARIA スコア", + "LEGGENDARIA PGreat", + "LEGGENDARIA Great", + "LEGGENDARIA ミスカウント", + "LEGGENDARIA クリアタイプ", + "LEGGENDARIA DJ LEVEL", + "最終プレー日時", + ], + logger + ); - t.equal(hasBeginnerAndLegg, true, "Should return true for hasBeginnerAndLegg"); + t.equal(hasBeginnerAndLegg, true, "Should return true for hasBeginnerAndLegg"); - t.end(); - }); + t.end(); + }); - t.test("No headers", (t) => { - t.throws( - () => ResolveHeaders([], logger), - new ScoreImportFatalError( - 400, - "Invalid CSV provided. CSV does not have the correct amount of headers." - ), - "Should throw an error on no headers." - ); + t.test("No headers", (t) => { + t.throws( + () => ResolveHeaders([], logger), + new ScoreImportFatalError( + 400, + "Invalid CSV provided. CSV does not have the correct amount of headers." + ), + "Should throw an error on no headers." + ); - t.end(); - }); + t.end(); + }); - t.test("Too Many Headers", (t) => { - t.throws( - () => ResolveHeaders(Array(1000), logger), - new ScoreImportFatalError( - 400, - "Invalid CSV provided. CSV does not have the correct amount of headers." - ), - "Should throw an error on no headers." - ); + t.test("Too Many Headers", (t) => { + t.throws( + () => ResolveHeaders(Array(1000), logger), + new ScoreImportFatalError( + 400, + "Invalid CSV provided. CSV does not have the correct amount of headers." + ), + "Should throw an error on no headers." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ParseEamusementCSV", (t) => { - t.test("Playtype Tests", (t) => { - const validSPFile = MockMulterFile(TestingIIDXEamusementCSV27, "iidx_27_sp.csv"); + t.test("Playtype Tests", (t) => { + const validSPFile = MockMulterFile(TestingIIDXEamusementCSV27, "iidx_27_sp.csv"); - t.throws( - () => GenericParseEamIIDXCSV(validSPFile, {}, "e-amusement", logger), - new ScoreImportFatalError(400, "Invalid playtype of Nothing given.") - ); + t.throws( + () => GenericParseEamIIDXCSV(validSPFile, {}, "e-amusement", logger), + new ScoreImportFatalError(400, "Invalid playtype of Nothing given.") + ); - let { context } = GenericParseEamIIDXCSV( - validSPFile, - { playtype: "SP" }, - "e-amusement", - logger - ); + let { context } = GenericParseEamIIDXCSV( + validSPFile, + { playtype: "SP" }, + "e-amusement", + logger + ); - t.equal(context.playtype, "SP", "Should correctly assert SP for a body of SP."); + t.equal(context.playtype, "SP", "Should correctly assert SP for a body of SP."); - const mockDPFile = MockMulterFile(TestingIIDXEamusementCSV27, "iidx_27_dp.csv"); + const mockDPFile = MockMulterFile(TestingIIDXEamusementCSV27, "iidx_27_dp.csv"); - ({ context } = GenericParseEamIIDXCSV( - mockDPFile, - { playtype: "DP" }, - "e-amusement", - logger - )); + ({ context } = GenericParseEamIIDXCSV( + mockDPFile, + { playtype: "DP" }, + "e-amusement", + logger + )); - t.equal(context.playtype, "DP", "Should correctly assert DP for a body of DP."); + t.equal(context.playtype, "DP", "Should correctly assert DP for a body of DP."); - t.throws( - () => GenericParseEamIIDXCSV(validSPFile, { playtype: "DP" }, "e-amusement", logger), - new ScoreImportFatalError( - 400, - "Safety Triggered: Filename contained 'SP', but was marked as a DP import. Are you *absolutely* sure this is right?" - ), - "Should trigger safety if playtype asserted goes against filename." - ); + t.throws( + () => GenericParseEamIIDXCSV(validSPFile, { playtype: "DP" }, "e-amusement", logger), + new ScoreImportFatalError( + 400, + "Safety Triggered: Filename contained 'SP', but was marked as a DP import. Are you *absolutely* sure this is right?" + ), + "Should trigger safety if playtype asserted goes against filename." + ); - ({ context } = GenericParseEamIIDXCSV( - mockDPFile, - { playtype: "DP", assertPlaytypeCorrect: true }, - "e-amusement", - logger - )); + ({ context } = GenericParseEamIIDXCSV( + mockDPFile, + { playtype: "DP", assertPlaytypeCorrect: true }, + "e-amusement", + logger + )); - t.equal( - context.playtype, - "DP", - "Should ignore weird filenames if assertPlaytypeCorrect is passed." - ); + t.equal( + context.playtype, + "DP", + "Should ignore weird filenames if assertPlaytypeCorrect is passed." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts index 40411c39a..f71168e3e 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts @@ -5,34 +5,34 @@ import { ParserFunctionReturns } from "../types"; import { EamusementScoreData, IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; enum EAM_VERSION_NAMES { - "1st&substream" = 1, - "2nd style", - "3rd style", - "4th style", - "5th style", - "6th style", - "7th style", - "8th style", - "9th style", - "10th style", - "IIDX RED", - "HAPPY SKY", - "DistorteD", - "GOLD", - "DJ TROOPERS", - "EMPRESS", - "SIRIUS", - "Resort Anthem", - "Lincle", - "tricoro", - "SPADA", - "PENDUAL", - "copula", - "SINOBUZ", - "CANNON BALLERS", - "Rootage", - "HEROIC VERSE", - "BISTROVER", + "1st&substream" = 1, + "2nd style", + "3rd style", + "4th style", + "5th style", + "6th style", + "7th style", + "8th style", + "9th style", + "10th style", + "IIDX RED", + "HAPPY SKY", + "DistorteD", + "GOLD", + "DJ TROOPERS", + "EMPRESS", + "SIRIUS", + "Resort Anthem", + "Lincle", + "tricoro", + "SPADA", + "PENDUAL", + "copula", + "SINOBUZ", + "CANNON BALLERS", + "Rootage", + "HEROIC VERSE", + "BISTROVER", } const PRE_HV_HEADER_COUNT = 27; @@ -72,142 +72,142 @@ const HV_HEADER_COUNT = 41; // ]; export function ResolveHeaders(headers: string[], logger: KtLogger) { - if (headers.length === PRE_HV_HEADER_COUNT) { - logger.verbose("PRE_HV csv recieved."); - return { - hasBeginnerAndLegg: false, - }; - } else if (headers.length === HV_HEADER_COUNT) { - logger.verbose("HV+ csv recieved."); - return { - hasBeginnerAndLegg: true, - }; - } + if (headers.length === PRE_HV_HEADER_COUNT) { + logger.verbose("PRE_HV csv recieved."); + return { + hasBeginnerAndLegg: false, + }; + } else if (headers.length === HV_HEADER_COUNT) { + logger.verbose("HV+ csv recieved."); + return { + hasBeginnerAndLegg: true, + }; + } - logger.info(`Invalid CSV header count of ${headers.length} received.`); - throw new ScoreImportFatalError( - 400, - "Invalid CSV provided. CSV does not have the correct amount of headers." - ); + logger.info(`Invalid CSV header count of ${headers.length} received.`); + throw new ScoreImportFatalError( + 400, + "Invalid CSV provided. CSV does not have the correct amount of headers." + ); } export function NaiveCSVParse(csvBuffer: Buffer, logger: KtLogger) { - const csvString = csvBuffer.toString("utf-8"); + const csvString = csvBuffer.toString("utf-8"); - const csvData = csvString.split("\n"); + const csvData = csvString.split("\n"); - const rawHeaders = []; - let headerLen = 0; - let curStr = ""; + const rawHeaders = []; + let headerLen = 0; + let curStr = ""; - // looks like we're doing it like this. - for (const char of csvData[0]) { - headerLen++; + // looks like we're doing it like this. + for (const char of csvData[0]) { + headerLen++; - // safety checks to avoid getting DOS'd - if (headerLen > 1000) { - throw new ScoreImportFatalError(400, "Headers were longer than 1000 characters long."); - } else if (rawHeaders.length >= 50) { - // this does not *really* do what it seems. - // because there's inevitably something left in curStr in this fn - // this means that the above check is actually > 50 headers. Not - // >= 50. - throw new ScoreImportFatalError(400, "Too many CSV headers."); - } + // safety checks to avoid getting DOS'd + if (headerLen > 1000) { + throw new ScoreImportFatalError(400, "Headers were longer than 1000 characters long."); + } else if (rawHeaders.length >= 50) { + // this does not *really* do what it seems. + // because there's inevitably something left in curStr in this fn + // this means that the above check is actually > 50 headers. Not + // >= 50. + throw new ScoreImportFatalError(400, "Too many CSV headers."); + } - if (char === ",") { - rawHeaders.push(curStr); - curStr = ""; - } else { - curStr += char; - } - } + if (char === ",") { + rawHeaders.push(curStr); + curStr = ""; + } else { + curStr += char; + } + } - rawHeaders.push(curStr); + rawHeaders.push(curStr); - const { hasBeginnerAndLegg } = ResolveHeaders(rawHeaders, logger); + const { hasBeginnerAndLegg } = ResolveHeaders(rawHeaders, logger); - const diffs = hasBeginnerAndLegg - ? ["beginner", "normal", "hyper", "another", "leggendaria"] - : ["normal", "hyper", "another"]; + const diffs = hasBeginnerAndLegg + ? ["beginner", "normal", "hyper", "another", "leggendaria"] + : ["normal", "hyper", "another"]; - const iterableData = []; + const iterableData = []; - let gameVersion = 0; + let gameVersion = 0; - for (let i = 1; i < csvData.length; i++) { - const data = csvData[i]; + for (let i = 1; i < csvData.length; i++) { + const data = csvData[i]; - // @security: This should probably be safetied from DOSing - const cells = data.split(","); + // @security: This should probably be safetied from DOSing + const cells = data.split(","); - // weirdly enough, an empty string split on "," is an array with - // one empty value. - // regardless, this line skips empty rows - if (cells.length === 1) { - logger.verbose(`Skipped empty row ${i}.`); - continue; - } + // weirdly enough, an empty string split on "," is an array with + // one empty value. + // regardless, this line skips empty rows + if (cells.length === 1) { + logger.verbose(`Skipped empty row ${i}.`); + continue; + } - if (cells.length !== rawHeaders.length) { - logger.info( - `eamusement-iidx csv has row (${i}) with invalid cell count of ${cells.length}, rejecting.`, - { - data, - } - ); - throw new ScoreImportFatalError( - 400, - `Row ${i} has an invalid amount of cells (${cells.length}, expected ${rawHeaders.length}).` - ); - } + if (cells.length !== rawHeaders.length) { + logger.info( + `eamusement-iidx csv has row (${i}) with invalid cell count of ${cells.length}, rejecting.`, + { + data, + } + ); + throw new ScoreImportFatalError( + 400, + `Row ${i} has an invalid amount of cells (${cells.length}, expected ${rawHeaders.length}).` + ); + } - const version = cells[0]; - const title = cells[1].trim(); // konmai quality - const timestamp = cells[rawHeaders.length - 1].trim(); + const version = cells[0]; + const title = cells[1].trim(); // konmai quality + const timestamp = cells[rawHeaders.length - 1].trim(); - // wtf typescript?? what's the point of enums? - const versionNum = EAM_VERSION_NAMES[version as keyof typeof EAM_VERSION_NAMES]; + // wtf typescript?? what's the point of enums? + const versionNum = EAM_VERSION_NAMES[version as keyof typeof EAM_VERSION_NAMES]; - if (!versionNum) { - logger.info(`Invalid/Unsupported EAM_VERSION_NAME ${version}.`); - throw new ScoreImportFatalError( - 400, - `Invalid/Unsupported Eamusement Version Name ${version}.` - ); - } + if (!versionNum) { + logger.info(`Invalid/Unsupported EAM_VERSION_NAME ${version}.`); + throw new ScoreImportFatalError( + 400, + `Invalid/Unsupported Eamusement Version Name ${version}.` + ); + } - if (versionNum > gameVersion) { - gameVersion = versionNum; - } + if (versionNum > gameVersion) { + gameVersion = versionNum; + } - const scores: EamusementScoreData[] = []; + const scores: EamusementScoreData[] = []; - for (let d = 0; d < diffs.length; d++) { - const diff = diffs[d]; - const di = 5 + d * 7; + for (let d = 0; d < diffs.length; d++) { + const diff = diffs[d]; + const di = 5 + d * 7; - scores.push({ - difficulty: diff.toUpperCase() as Difficulties["iidx:SP" | "iidx:DP"], - bp: cells[di + 4], - exscore: cells[di + 1], - pgreat: cells[di + 2], - great: cells[di + 3], - lamp: cells[di + 5], - level: cells[di], - }); - } + scores.push({ + difficulty: diff.toUpperCase() as Difficulties["iidx:SP" | "iidx:DP"], + bp: cells[di + 4], + exscore: cells[di + 1], + pgreat: cells[di + 2], + great: cells[di + 3], + lamp: cells[di + 5], + level: cells[di], + }); + } - iterableData.push( - ...scores.map((e) => ({ - score: e, - timestamp, - title, - })) - ); - } + iterableData.push( + ...scores.map((e) => ({ + score: e, + timestamp, + title, + })) + ); + } - return { iterableData, version: gameVersion.toString(), hasBeginnerAndLegg }; + return { iterableData, version: gameVersion.toString(), hasBeginnerAndLegg }; } /** @@ -216,64 +216,64 @@ export function NaiveCSVParse(csvBuffer: Buffer, logger: KtLogger) { * @param body - The request body that made this file import request. Used to infer playtype. */ function GenericParseEamIIDXCSV( - fileData: Express.Multer.File, - body: Record, - service: string, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + service: string, + logger: KtLogger ): ParserFunctionReturns { - let playtype: "SP" | "DP"; + let playtype: "SP" | "DP"; - if (body.playtype === "SP") { - playtype = "SP"; - } else if (body.playtype === "DP") { - playtype = "DP"; - } else { - logger.info(`Invalid playtype of ${body.playtype} passed to ParseEamusementCSV.`); - throw new ScoreImportFatalError( - 400, - `Invalid playtype of ${body.playtype ?? "Nothing"} given.` - ); - } + if (body.playtype === "SP") { + playtype = "SP"; + } else if (body.playtype === "DP") { + playtype = "DP"; + } else { + logger.info(`Invalid playtype of ${body.playtype} passed to ParseEamusementCSV.`); + throw new ScoreImportFatalError( + 400, + `Invalid playtype of ${body.playtype ?? "Nothing"} given.` + ); + } - const lowercaseFilename = fileData.originalname.toLowerCase(); + const lowercaseFilename = fileData.originalname.toLowerCase(); - // prettier pls - if ( - !body.assertPlaytypeCorrect && - ((lowercaseFilename.includes("sp") && playtype === "DP") || - (lowercaseFilename.includes("dp") && playtype === "SP")) - ) { - logger.info( - `File was uploaded with filename ${fileData.originalname}, but this was set as a ${playtype} import. Sanity check refusing.` - ); + // prettier pls + if ( + !body.assertPlaytypeCorrect && + ((lowercaseFilename.includes("sp") && playtype === "DP") || + (lowercaseFilename.includes("dp") && playtype === "SP")) + ) { + logger.info( + `File was uploaded with filename ${fileData.originalname}, but this was set as a ${playtype} import. Sanity check refusing.` + ); - throw new ScoreImportFatalError( - 400, - `Safety Triggered: Filename contained '${ - playtype === "SP" ? "DP" : "SP" - }', but was marked as a ${playtype} import. Are you *absolutely* sure this is right?` - ); - } + throw new ScoreImportFatalError( + 400, + `Safety Triggered: Filename contained '${ + playtype === "SP" ? "DP" : "SP" + }', but was marked as a ${playtype} import. Are you *absolutely* sure this is right?` + ); + } - const { hasBeginnerAndLegg, version, iterableData } = NaiveCSVParse(fileData.buffer, logger); + const { hasBeginnerAndLegg, version, iterableData } = NaiveCSVParse(fileData.buffer, logger); - logger.verbose("Successfully parsed CSV."); + logger.verbose("Successfully parsed CSV."); - const context: IIDXEamusementCSVContext = { - playtype, - importVersion: version, - hasBeginnerAndLegg, - service, - }; + const context: IIDXEamusementCSVContext = { + playtype, + importVersion: version, + hasBeginnerAndLegg, + service, + }; - logger.verbose(`Successfully Parsed with ${iterableData.length} results.`); + logger.verbose(`Successfully Parsed with ${iterableData.length} results.`); - return { - iterable: iterableData, - context, - game: "iidx", - classHandler: null, - }; + return { + iterable: iterableData, + context, + game: "iidx", + classHandler: null, + }; } export default GenericParseEamIIDXCSV; diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts index 7abd2011f..3215913ed 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts @@ -1,20 +1,20 @@ export interface EamusementScoreData { - difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; - lamp: string; - exscore: string; - pgreat: string; - great: string; - bp: string; - level: string; + difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + lamp: string; + exscore: string; + pgreat: string; + great: string; + bp: string; + level: string; } type BaseProps = { - title: string; - timestamp: string; + title: string; + timestamp: string; }; export type IIDXEamusementCSVData = { - score: EamusementScoreData; + score: EamusementScoreData; } & BaseProps; type Props = "exscore" | "lamp" | "pgreat" | "great" | "bp" | "level"; @@ -22,13 +22,13 @@ type Props = "exscore" | "lamp" | "pgreat" | "great" | "bp" | "level"; type RawPropKeys = `${"beginner" | "normal" | "hyper" | "another" | "leggendaria"}-${Props}`; export type RawIIDXEamusementCSVData = { - [K in RawPropKeys]: unknown; + [K in RawPropKeys]: unknown; } & - BaseProps & { [index: string]: unknown }; + BaseProps & { [index: string]: unknown }; export interface IIDXEamusementCSVContext { - playtype: "SP" | "DP"; - importVersion: string; - hasBeginnerAndLegg: boolean; - service: string; + playtype: "SP" | "DP"; + importVersion: string; + hasBeginnerAndLegg: boolean; + service: string; } diff --git a/server/src/lib/score-import/import-types/common/types.ts b/server/src/lib/score-import/import-types/common/types.ts index 91286ebcd..399877c5a 100644 --- a/server/src/lib/score-import/import-types/common/types.ts +++ b/server/src/lib/score-import/import-types/common/types.ts @@ -9,12 +9,12 @@ import { KaiContext } from "./api-kai/types"; import { BatchManualContext, BatchManualScore } from "./batch-manual/types"; import { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./eamusement-iidx-csv/types"; import { - ImportTypes, - Game, - AnyChartDocument, - AnySongDocument, - integer, - MongoDBDocument, + ImportTypes, + Game, + AnyChartDocument, + AnySongDocument, + integer, + MongoDBDocument, } from "tachi-common"; import { ConverterFailure } from "../../framework/common/converter-failures"; import { DryScore } from "../../framework/common/types"; @@ -23,89 +23,89 @@ import { USCClientScore } from "../../../../server/router/ir/usc/types"; import { IRUSCContext } from "../ir/usc/types"; import { ClassHandler } from "../../framework/user-game-stats/types"; export interface ImportTypeDataMap { - "file/eamusement-iidx-csv": IIDXEamusementCSVData; - "file/batch-manual": BatchManualScore; - "file/solid-state-squad": S3Score; - "file/mer-iidx": MerScore; - "file/pli-iidx-csv": IIDXEamusementCSVData; + "file/eamusement-iidx-csv": IIDXEamusementCSVData; + "file/batch-manual": BatchManualScore; + "file/solid-state-squad": S3Score; + "file/mer-iidx": MerScore; + "file/pli-iidx-csv": IIDXEamusementCSVData; - "ir/direct-manual": BatchManualScore; - "ir/barbatos": BarbatosScore; - "ir/fervidex": FervidexScore; - "ir/fervidex-static": FervidexStaticScore; - "ir/chunitachi": BatchManualScore; - "ir/beatoraja": BeatorajaScore; - "ir/usc": USCClientScore; + "ir/direct-manual": BatchManualScore; + "ir/barbatos": BarbatosScore; + "ir/fervidex": FervidexScore; + "ir/fervidex-static": FervidexStaticScore; + "ir/chunitachi": BatchManualScore; + "ir/beatoraja": BeatorajaScore; + "ir/usc": USCClientScore; - // These aren't placeholder values - the data is yielded in a way that - // the value of these is legitimately unknown at convert time. - "api/arc-iidx": unknown; - "api/arc-sdvx": unknown; - "api/arc-ddr": unknown; - "api/flo-iidx": unknown; - "api/flo-sdvx": unknown; - "api/eag-iidx": unknown; - "api/eag-sdvx": unknown; + // These aren't placeholder values - the data is yielded in a way that + // the value of these is legitimately unknown at convert time. + "api/arc-iidx": unknown; + "api/arc-sdvx": unknown; + "api/arc-ddr": unknown; + "api/flo-iidx": unknown; + "api/flo-sdvx": unknown; + "api/eag-iidx": unknown; + "api/eag-sdvx": unknown; } export interface ImportTypeContextMap { - "file/eamusement-iidx-csv": IIDXEamusementCSVContext; - "file/batch-manual": BatchManualContext; - "file/solid-state-squad": EmptyObject; - "file/mer-iidx": EmptyObject; - "file/pli-iidx-csv": IIDXEamusementCSVContext; + "file/eamusement-iidx-csv": IIDXEamusementCSVContext; + "file/batch-manual": BatchManualContext; + "file/solid-state-squad": EmptyObject; + "file/mer-iidx": EmptyObject; + "file/pli-iidx-csv": IIDXEamusementCSVContext; - "ir/direct-manual": BatchManualContext; - "ir/barbatos": EmptyObject; - "ir/fervidex": FervidexContext; - "ir/fervidex-static": FervidexStaticContext; - "ir/chunitachi": BatchManualContext; - "ir/beatoraja": BeatorajaContext; - "ir/usc": IRUSCContext; + "ir/direct-manual": BatchManualContext; + "ir/barbatos": EmptyObject; + "ir/fervidex": FervidexContext; + "ir/fervidex-static": FervidexStaticContext; + "ir/chunitachi": BatchManualContext; + "ir/beatoraja": BeatorajaContext; + "ir/usc": IRUSCContext; - "api/arc-iidx": EmptyObject; - "api/arc-sdvx": EmptyObject; - "api/arc-ddr": EmptyObject; - "api/flo-iidx": KaiContext; - "api/flo-sdvx": KaiContext; - "api/eag-iidx": KaiContext; - "api/eag-sdvx": KaiContext; + "api/arc-iidx": EmptyObject; + "api/arc-sdvx": EmptyObject; + "api/arc-ddr": EmptyObject; + "api/flo-iidx": KaiContext; + "api/flo-sdvx": KaiContext; + "api/eag-iidx": KaiContext; + "api/eag-sdvx": KaiContext; } export interface OrphanScoreDocument extends MongoDBDocument { - importType: T; - data: ImportTypeDataMap[T]; - context: ImportTypeContextMap[T]; - errMsg: string | null; - orphanID: string; - userID: integer; - timeInserted: number; + importType: T; + data: ImportTypeDataMap[T]; + context: ImportTypeContextMap[T]; + errMsg: string | null; + orphanID: string; + userID: integer; + timeInserted: number; } export interface ConverterFnSuccessReturn { - dryScore: DryScore; - chart: AnyChartDocument; - song: AnySongDocument; + dryScore: DryScore; + chart: AnyChartDocument; + song: AnySongDocument; } export type ConverterFnReturnOrFailure = ConverterFailure | ConverterFnSuccessReturn; export interface ConverterFunction { - ( - data: D, - processContext: C, - importType: ImportTypes, - logger: KtLogger - ): Promise; + ( + data: D, + processContext: C, + importType: ImportTypes, + logger: KtLogger + ): Promise; } export interface ImportInputParser { - (logger: KtLogger): ParserFunctionReturns | Promise>; + (logger: KtLogger): ParserFunctionReturns | Promise>; } export interface ParserFunctionReturns { - iterable: Iterable | AsyncIterable; - context: C; - game: Game; - classHandler: ClassHandler | null; + iterable: Iterable | AsyncIterable; + context: C; + game: Game; + classHandler: ClassHandler | null; } diff --git a/server/src/lib/score-import/import-types/converters.ts b/server/src/lib/score-import/import-types/converters.ts index e96f23c77..a3ebe469b 100644 --- a/server/src/lib/score-import/import-types/converters.ts +++ b/server/src/lib/score-import/import-types/converters.ts @@ -16,29 +16,29 @@ import { ConverterIRFervidex } from "./ir/fervidex/converter"; import { ConverterIRUSC } from "./ir/usc/converter"; export type ConverterMap = { - [I in ImportTypes]: ConverterFunction; + [I in ImportTypes]: ConverterFunction; }; export const Converters: ConverterMap = { - "file/solid-state-squad": ConvertFileS3, - "file/batch-manual": ConverterBatchManual, - "file/mer-iidx": ConvertFileMerIIDX, - "file/pli-iidx-csv": ConvertEamIIDXCSV, - "file/eamusement-iidx-csv": ConvertEamIIDXCSV, + "file/solid-state-squad": ConvertFileS3, + "file/batch-manual": ConverterBatchManual, + "file/mer-iidx": ConvertFileMerIIDX, + "file/pli-iidx-csv": ConvertEamIIDXCSV, + "file/eamusement-iidx-csv": ConvertEamIIDXCSV, - "api/eag-iidx": ConvertAPIKaiIIDX, - "api/eag-sdvx": ConvertAPIKaiSDVX, - "api/flo-iidx": ConvertAPIKaiIIDX, - "api/flo-sdvx": ConvertAPIKaiSDVX, - "api/arc-iidx": ConvertAPIArcIIDX, - "api/arc-ddr": ConvertAPIArcDDR, - "api/arc-sdvx": ConvertAPIArcSDVX, + "api/eag-iidx": ConvertAPIKaiIIDX, + "api/eag-sdvx": ConvertAPIKaiSDVX, + "api/flo-iidx": ConvertAPIKaiIIDX, + "api/flo-sdvx": ConvertAPIKaiSDVX, + "api/arc-iidx": ConvertAPIArcIIDX, + "api/arc-ddr": ConvertAPIArcDDR, + "api/arc-sdvx": ConvertAPIArcSDVX, - "ir/barbatos": ConverterIRBarbatos, - "ir/beatoraja": ConverterIRBeatoraja, - "ir/fervidex": ConverterIRFervidex, - "ir/fervidex-static": ConverterIRFervidexStatic, - "ir/direct-manual": ConverterBatchManual, - "ir/usc": ConverterIRUSC, - "ir/chunitachi": ConverterBatchManual, + "ir/barbatos": ConverterIRBarbatos, + "ir/beatoraja": ConverterIRBeatoraja, + "ir/fervidex": ConverterIRFervidex, + "ir/fervidex-static": ConverterIRFervidexStatic, + "ir/direct-manual": ConverterBatchManual, + "ir/usc": ConverterIRUSC, + "ir/chunitachi": ConverterBatchManual, }; diff --git a/server/src/lib/score-import/import-types/file/batch-manual/parser.ts b/server/src/lib/score-import/import-types/file/batch-manual/parser.ts index 30dca9037..3df8e017c 100644 --- a/server/src/lib/score-import/import-types/file/batch-manual/parser.ts +++ b/server/src/lib/score-import/import-types/file/batch-manual/parser.ts @@ -10,22 +10,22 @@ import { ParserFunctionReturns } from "../../common/types"; * @param body - The request body that made this file import request. */ function ParseBatchManual( - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ): ParserFunctionReturns { - let jsonData: unknown; + let jsonData: unknown; - try { - jsonData = JSON.parse(fileData.buffer.toString("utf-8")); - } catch (err) { - throw new ScoreImportFatalError( - 400, - `Invalid JSON. (${err?.message ?? "No Error Message Available."})` - ); - } + try { + jsonData = JSON.parse(fileData.buffer.toString("utf-8")); + } catch (err) { + throw new ScoreImportFatalError( + 400, + `Invalid JSON. (${err?.message ?? "No Error Message Available."})` + ); + } - return ParseBatchManualFromObject(jsonData, "file/batch-manual", logger); + return ParseBatchManualFromObject(jsonData, "file/batch-manual", logger); } export default ParseBatchManual; diff --git a/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts index 385242ee8..d30846d93 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts @@ -1,17 +1,17 @@ import { KtLogger } from "../../../../logger/logger"; import GenericParseEamIIDXCSV from "../../common/eamusement-iidx-csv/parser"; import { - IIDXEamusementCSVContext, - IIDXEamusementCSVData, + IIDXEamusementCSVContext, + IIDXEamusementCSVData, } from "../../common/eamusement-iidx-csv/types"; import { ParserFunctionReturns } from "../../common/types"; function ParseEamusementIIDXCSV( - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ): ParserFunctionReturns { - return GenericParseEamIIDXCSV(fileData, body, "e-amusement", logger); + return GenericParseEamIIDXCSV(fileData, body, "e-amusement", logger); } export default ParseEamusementIIDXCSV; diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts index 6307bfd69..bf613a2bd 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts @@ -3,10 +3,10 @@ import db from "../../../../../external/mongo/db"; import CreateLogCtx from "../../../../logger/logger"; import ResetDBState from "../../../../../test-utils/resets"; import { - GetKTDataJSON, - LoadKTBlackIIDXData, - Testing511Song, - Testing511SPA, + GetKTDataJSON, + LoadKTBlackIIDXData, + Testing511Song, + Testing511SPA, } from "../../../../../test-utils/test-data"; import { ConvertFileMerIIDX } from "./converter"; import deepmerge from "deepmerge"; @@ -16,124 +16,124 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#ConvertFileMerIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - function merc(g: Partial = {}) { - return ConvertFileMerIIDX(deepmerge(MerScore, g), {}, "file/mer-iidx", logger); - } + function merc(g: Partial = {}) { + return ConvertFileMerIIDX(deepmerge(MerScore, g), {}, "file/mer-iidx", logger); + } - const MerScore = GetKTDataJSON("./mer/merscore.json"); + const MerScore = GetKTDataJSON("./mer/merscore.json"); - t.test("Valid Conversion", async (t) => { - const res = await ConvertFileMerIIDX(MerScore, {}, "file/mer-iidx", logger); + t.test("Valid Conversion", async (t) => { + const res = await ConvertFileMerIIDX(MerScore, {}, "file/mer-iidx", logger); - t.hasStrict( - res, - { - song: Testing511Song, - chart: Testing511SPA, - dryScore: { - game: "iidx", - comment: null, - importType: "file/mer-iidx", - service: "MER", - scoreData: { - score: 1000, - // percent: 63.61, approximately, fpa impossible. - grade: "B", - lamp: "CLEAR", - hitData: {}, - hitMeta: { - bp: 21, - }, - }, - scoreMeta: {}, - timeAchieved: 1616537722000, - }, - }, - "Should return the converted dry score." - ); + t.hasStrict( + res, + { + song: Testing511Song, + chart: Testing511SPA, + dryScore: { + game: "iidx", + comment: null, + importType: "file/mer-iidx", + service: "MER", + scoreData: { + score: 1000, + // percent: 63.61, approximately, fpa impossible. + grade: "B", + lamp: "CLEAR", + hitData: {}, + hitMeta: { + bp: 21, + }, + }, + scoreMeta: {}, + timeAchieved: 1616537722000, + }, + }, + "Should return the converted dry score." + ); - t.end(); - }); + t.end(); + }); - t.test("Valid DP Conversion", async (t) => { - await LoadKTBlackIIDXData(); - const res = await merc({ play_type: "DOUBLE" }); + t.test("Valid DP Conversion", async (t) => { + await LoadKTBlackIIDXData(); + const res = await merc({ play_type: "DOUBLE" }); - t.hasStrict( - res, - { - song: { id: 1 }, - chart: { playtype: "DP", data: { inGameID: 1000 } }, - dryScore: { - game: "iidx", - comment: null, - importType: "file/mer-iidx", - service: "MER", - scoreData: { - score: 1000, - // percent: 63.61, approximately, fpa impossible. - grade: "B", - lamp: "CLEAR", - hitData: {}, - hitMeta: { - bp: 21, - }, - }, - scoreMeta: {}, - timeAchieved: 1616537722000, - }, - }, - "Should return the converted dry score." - ); + t.hasStrict( + res, + { + song: { id: 1 }, + chart: { playtype: "DP", data: { inGameID: 1000 } }, + dryScore: { + game: "iidx", + comment: null, + importType: "file/mer-iidx", + service: "MER", + scoreData: { + score: 1000, + // percent: 63.61, approximately, fpa impossible. + grade: "B", + lamp: "CLEAR", + hitData: {}, + hitMeta: { + bp: 21, + }, + }, + scoreMeta: {}, + timeAchieved: 1616537722000, + }, + }, + "Should return the converted dry score." + ); - t.end(); - }); + t.end(); + }); - t.test("Invalid Chart", (t) => { - t.rejects(() => merc({ diff_type: "LEGGENDARIA" }), { - message: /Could not find chart with musicID 1000 \(SP LEGGENDARIA/u, - } as any); + t.test("Invalid Chart", (t) => { + t.rejects(() => merc({ diff_type: "LEGGENDARIA" }), { + message: /Could not find chart with musicID 1000 \(SP LEGGENDARIA/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Invalid Song", (t) => { - t.rejects(() => merc({ music_id: 0 }), { - message: /Could not find chart with musicID 0/u, - } as any); + t.test("Invalid Song", (t) => { + t.rejects(() => merc({ music_id: 0 }), { + message: /Could not find chart with musicID 0/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should log severe on Song-Chart Desync", async (t) => { - await db.songs.iidx.remove({}); + t.test("Should log severe on Song-Chart Desync", async (t) => { + await db.songs.iidx.remove({}); - t.rejects(() => merc(), { - message: /Song-Chart Desync on songID 1/u, - } as any); + t.rejects(() => merc(), { + message: /Song-Chart Desync on songID 1/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Invalid Percent", (t) => { - t.rejects(() => merc({ score: 9999 }), { - message: /Invalid percent/u, - } as any); + t.test("Invalid Percent", (t) => { + t.rejects(() => merc({ score: 9999 }), { + message: /Invalid percent/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Invalid Date", (t) => { - t.rejects(() => merc({ update_time: "INVALID" }), { - message: /Invalid\/Unparsable score timestamp of INVALID/u, - } as any); + t.test("Invalid Date", (t) => { + t.rejects(() => merc({ update_time: "INVALID" }), { + message: /Invalid\/Unparsable score timestamp of INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts index d0f19f009..86c868939 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts @@ -2,12 +2,12 @@ import { FindIIDXChartOnInGameID } from "../../../../../utils/queries/charts"; import { FindSongOnID } from "../../../../../utils/queries/songs"; import { EmptyObject } from "../../../../../utils/types"; import { - InternalFailure, - KTDataNotFoundFailure, + InternalFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { MerScore } from "./types"; import { Lamps } from "tachi-common"; @@ -15,70 +15,70 @@ import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../../common/types"; function ConvertMERLamp(lamp: MerScore["clear_type"]): Lamps["iidx:DP" | "iidx:SP"] { - if (lamp === "FULLCOMBO CLEAR") { - return "FULL COMBO"; - } + if (lamp === "FULLCOMBO CLEAR") { + return "FULL COMBO"; + } - return lamp; + return lamp; } export const ConvertFileMerIIDX: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const playtype = data.play_type === "SINGLE" ? "SP" : "DP"; + const playtype = data.play_type === "SINGLE" ? "SP" : "DP"; - const chart = await FindIIDXChartOnInGameID(data.music_id, playtype, data.diff_type); + const chart = await FindIIDXChartOnInGameID(data.music_id, playtype, data.diff_type); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with musicID ${data.music_id} (${playtype} ${data.diff_type}.)`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with musicID ${data.music_id} (${playtype} ${data.diff_type}.)`, + importType, + data, + context + ); + } - const song = await FindSongOnID("iidx", chart.songID); + const song = await FindSongOnID("iidx", chart.songID); - if (!song) { - logger.severe(`Could not find song with songID ${chart.songID}, but chart exists for it?`); - throw new InternalFailure(`Song-Chart Desync on songID ${chart.songID}`); - } + if (!song) { + logger.severe(`Could not find song with songID ${chart.songID}, but chart exists for it?`); + throw new InternalFailure(`Song-Chart Desync on songID ${chart.songID}`); + } - const { percent, grade } = GenericGetGradeAndPercent("iidx", data.score, chart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", data.score, chart); - const lamp = ConvertMERLamp(data.clear_type); + const lamp = ConvertMERLamp(data.clear_type); - const timeAchieved = ParseDateFromString(data.update_time); + const timeAchieved = ParseDateFromString(data.update_time); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - game: "iidx", - comment: null, - importType: "file/mer-iidx", - service: "MER", - scoreData: { - score: data.score, - percent, - grade, - lamp, - hitData: {}, - hitMeta: { - bp: data.miss_count === -1 ? null : data.miss_count, - }, - }, - scoreMeta: {}, - // japan is GMT+9 - timeAchieved: timeAchieved ? timeAchieved - NINE_HOURS : null, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + game: "iidx", + comment: null, + importType: "file/mer-iidx", + service: "MER", + scoreData: { + score: data.score, + percent, + grade, + lamp, + hitData: {}, + hitMeta: { + bp: data.miss_count === -1 ? null : data.miss_count, + }, + }, + scoreMeta: {}, + // japan is GMT+9 + timeAchieved: timeAchieved ? timeAchieved - NINE_HOURS : null, + }; - return { - chart, - song, - dryScore, - }; + return { + chart, + song, + dryScore, + }; }; const NINE_HOURS = 1000 * 60 * 60 * 9; diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts index d0ae8c0eb..d95538369 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts @@ -10,136 +10,136 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#ParseMerIIDX", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - function mrfb(buffer: Buffer) { - return ParseMerIIDX(MockMulterFile(buffer, "buffer.json"), {}, logger); - } + function mrfb(buffer: Buffer) { + return ParseMerIIDX(MockMulterFile(buffer, "buffer.json"), {}, logger); + } - function mrfj(obj: any) { - return ParseMerIIDX( - MockMulterFile(Buffer.from(JSON.stringify(obj)), "buffer.json"), - {}, - logger - ); - } + function mrfj(obj: any) { + return ParseMerIIDX( + MockMulterFile(Buffer.from(JSON.stringify(obj)), "buffer.json"), + {}, + logger + ); + } - function mrff(filename: string) { - return ParseMerIIDX(MockMulterFile(GetKTDataBuffer(filename), filename), {}, logger); - } + function mrff(filename: string) { + return ParseMerIIDX(MockMulterFile(GetKTDataBuffer(filename), filename), {}, logger); + } - t.test("Basic Parsing", (t) => { - const res = mrff("./mer/base.json"); + t.test("Basic Parsing", (t) => { + const res = mrff("./mer/base.json"); - t.hasStrict( - res, - { - game: "iidx", - context: {}, - classHandler: null, - iterable: [ - { - music_id: 3007, - music_name: "Presto", - play_type: "SINGLE", - diff_type: "HYPER", - score: 566, - miss_count: 46, - clear_type: "NO PLAY", - update_time: "2019-06-01 19:56:59", - }, - { - music_id: 3013, - music_name: "THE SAFARI", - play_type: "SINGLE", - diff_type: "NORMAL", - score: 681, - miss_count: 102, - clear_type: "FAILED", - update_time: "2019-05-30 03:21:57", - }, - { - music_id: 3213, - music_name: "TAKE ON ME", - play_type: "SINGLE", - diff_type: "ANOTHER", - score: 922, - miss_count: 56, - clear_type: "CLEAR", - update_time: "2019-05-03 02:10:58", - }, - ], - }, - "Should correctly parse data." - ); + t.hasStrict( + res, + { + game: "iidx", + context: {}, + classHandler: null, + iterable: [ + { + music_id: 3007, + music_name: "Presto", + play_type: "SINGLE", + diff_type: "HYPER", + score: 566, + miss_count: 46, + clear_type: "NO PLAY", + update_time: "2019-06-01 19:56:59", + }, + { + music_id: 3013, + music_name: "THE SAFARI", + play_type: "SINGLE", + diff_type: "NORMAL", + score: 681, + miss_count: 102, + clear_type: "FAILED", + update_time: "2019-05-30 03:21:57", + }, + { + music_id: 3213, + music_name: "TAKE ON ME", + play_type: "SINGLE", + diff_type: "ANOTHER", + score: 922, + miss_count: 56, + clear_type: "CLEAR", + update_time: "2019-05-03 02:10:58", + }, + ], + }, + "Should correctly parse data." + ); - t.end(); - }); + t.end(); + }); - const baseScore = GetKTDataJSON("./mer/base.json"); + const baseScore = GetKTDataJSON("./mer/base.json"); - t.test("Should throw on invalid play_type", (t) => { - t.throws(() => mrfj(deepmerge(baseScore, { play_type: "INVALID" }))); - t.throws(() => mrfj(deepmerge(baseScore, { play_type: null }))); - t.throws(() => mrfj(deepmerge(baseScore, { play_type: undefined }))); - t.throws(() => mrfj(deepmerge(baseScore, { play_type: 1 }))); + t.test("Should throw on invalid play_type", (t) => { + t.throws(() => mrfj(deepmerge(baseScore, { play_type: "INVALID" }))); + t.throws(() => mrfj(deepmerge(baseScore, { play_type: null }))); + t.throws(() => mrfj(deepmerge(baseScore, { play_type: undefined }))); + t.throws(() => mrfj(deepmerge(baseScore, { play_type: 1 }))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid score", (t) => { - t.throws(() => mrfj(deepmerge(baseScore, { score: -1 }))); - t.throws(() => mrfj(deepmerge(baseScore, { score: 1.5 }))); - t.throws(() => mrfj(deepmerge(baseScore, { score: "1" }))); - t.throws(() => mrfj(deepmerge(baseScore, { score: null }))); - t.throws(() => mrfj(deepmerge(baseScore, { score: undefined }))); + t.test("Should throw on invalid score", (t) => { + t.throws(() => mrfj(deepmerge(baseScore, { score: -1 }))); + t.throws(() => mrfj(deepmerge(baseScore, { score: 1.5 }))); + t.throws(() => mrfj(deepmerge(baseScore, { score: "1" }))); + t.throws(() => mrfj(deepmerge(baseScore, { score: null }))); + t.throws(() => mrfj(deepmerge(baseScore, { score: undefined }))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid diff_type", (t) => { - t.throws(() => mrfj(deepmerge(baseScore, { diff_type: "INVALID" }))); - t.throws(() => mrfj(deepmerge(baseScore, { diff_type: null }))); - t.throws(() => mrfj(deepmerge(baseScore, { diff_type: undefined }))); + t.test("Should throw on invalid diff_type", (t) => { + t.throws(() => mrfj(deepmerge(baseScore, { diff_type: "INVALID" }))); + t.throws(() => mrfj(deepmerge(baseScore, { diff_type: null }))); + t.throws(() => mrfj(deepmerge(baseScore, { diff_type: undefined }))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid clear_type", (t) => { - t.throws(() => mrfj(deepmerge(baseScore, { clear_type: "INVALID" }))); - t.throws(() => mrfj(deepmerge(baseScore, { clear_type: null }))); - t.throws(() => mrfj(deepmerge(baseScore, { clear_type: undefined }))); + t.test("Should throw on invalid clear_type", (t) => { + t.throws(() => mrfj(deepmerge(baseScore, { clear_type: "INVALID" }))); + t.throws(() => mrfj(deepmerge(baseScore, { clear_type: null }))); + t.throws(() => mrfj(deepmerge(baseScore, { clear_type: undefined }))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid miss_count", (t) => { - t.throws(() => mrfj(deepmerge(baseScore, { miss_count: "INVALID" }))); - t.throws(() => mrfj(deepmerge(baseScore, { miss_count: null }))); - t.throws(() => mrfj(deepmerge(baseScore, { miss_count: undefined }))); + t.test("Should throw on invalid miss_count", (t) => { + t.throws(() => mrfj(deepmerge(baseScore, { miss_count: "INVALID" }))); + t.throws(() => mrfj(deepmerge(baseScore, { miss_count: null }))); + t.throws(() => mrfj(deepmerge(baseScore, { miss_count: undefined }))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid JSON", (t) => { - t.throws(() => mrfb(Buffer.from("{]"))); + t.test("Should throw on invalid JSON", (t) => { + t.throws(() => mrfb(Buffer.from("{]"))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on non-top-level-array", (t) => { - t.throws(() => mrfb(Buffer.from("{}"))); + t.test("Should throw on non-top-level-array", (t) => { + t.throws(() => mrfb(Buffer.from("{}"))); - t.end(); - }); + t.end(); + }); - t.test("Should throw on invalid Mer Scores", (t) => { - t.throws(() => mrfb(Buffer.from(JSON.stringify([{ foo: "bar" }])))); + t.test("Should throw on invalid Mer Scores", (t) => { + t.throws(() => mrfb(Buffer.from(JSON.stringify([{ foo: "bar" }])))); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts index 63d456ed7..5d0a66134 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts @@ -8,55 +8,55 @@ import { ConvertFileMerIIDX } from "./converter"; import { MerScore } from "./types"; const PR_MerIIDX = { - music_id: p.isPositiveInteger, - play_type: p.isIn("SINGLE", "DOUBLE"), - diff_type: p.isIn("NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA", "BEGINNER"), - score: p.isPositiveInteger, - miss_count: p.or(p.isPositiveInteger, p.is(-1)), - clear_type: p.isIn( - "NO PLAY", - "FAILED", - "ASSIST CLEAR", - "EASY CLEAR", - "CLEAR", - "HARD CLEAR", - "EX HARD CLEAR", - "FULLCOMBO CLEAR" - ), - update_time: "string", + music_id: p.isPositiveInteger, + play_type: p.isIn("SINGLE", "DOUBLE"), + diff_type: p.isIn("NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA", "BEGINNER"), + score: p.isPositiveInteger, + miss_count: p.or(p.isPositiveInteger, p.is(-1)), + clear_type: p.isIn( + "NO PLAY", + "FAILED", + "ASSIST CLEAR", + "EASY CLEAR", + "CLEAR", + "HARD CLEAR", + "EX HARD CLEAR", + "FULLCOMBO CLEAR" + ), + update_time: "string", }; export function ParseMerIIDX( - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ): ParserFunctionReturns { - let jsonData; + let jsonData; - try { - jsonData = JSON.parse(fileData.buffer.toString("utf-8")); - } catch (err) { - logger.info(err.message); - throw new ScoreImportFatalError(400, "Invalid JSON."); - } + try { + jsonData = JSON.parse(fileData.buffer.toString("utf-8")); + } catch (err) { + logger.info(err.message); + throw new ScoreImportFatalError(400, "Invalid JSON."); + } - if (!Array.isArray(jsonData)) { - throw new ScoreImportFatalError(400, `Invalid MER-IIDX JSON: Expected Top-Level Array.`); - } + if (!Array.isArray(jsonData)) { + throw new ScoreImportFatalError(400, `Invalid MER-IIDX JSON: Expected Top-Level Array.`); + } - // this is because prudence doesn't support top level arrays at the moment. - for (let i = 0; i < jsonData.length; i++) { - const err = p(jsonData[i], PR_MerIIDX, {}, { allowExcessKeys: true }); + // this is because prudence doesn't support top level arrays at the moment. + for (let i = 0; i < jsonData.length; i++) { + const err = p(jsonData[i], PR_MerIIDX, {}, { allowExcessKeys: true }); - if (err) { - throw new ScoreImportFatalError(400, `${FormatPrError(err)} [Index ${i}]`); - } - } + if (err) { + throw new ScoreImportFatalError(400, `${FormatPrError(err)} [Index ${i}]`); + } + } - return { - classHandler: null, - context: {}, - iterable: jsonData as MerScore[], - game: "iidx", - }; + return { + classHandler: null, + context: {}, + iterable: jsonData as MerScore[], + game: "iidx", + }; } diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/types.ts b/server/src/lib/score-import/import-types/file/mer-iidx/types.ts index 914a79621..91a959e39 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/types.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/types.ts @@ -1,19 +1,19 @@ import { integer } from "tachi-common"; export interface MerScore { - music_id: integer; - play_type: "SINGLE" | "DOUBLE"; - diff_type: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; - score: integer; - miss_count: integer; - clear_type: - | "NO PLAY" - | "FAILED" - | "ASSIST CLEAR" - | "EASY CLEAR" - | "CLEAR" - | "HARD CLEAR" - | "EX HARD CLEAR" - | "FULLCOMBO CLEAR"; - update_time: string; + music_id: integer; + play_type: "SINGLE" | "DOUBLE"; + diff_type: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + score: integer; + miss_count: integer; + clear_type: + | "NO PLAY" + | "FAILED" + | "ASSIST CLEAR" + | "EASY CLEAR" + | "CLEAR" + | "HARD CLEAR" + | "EX HARD CLEAR" + | "FULLCOMBO CLEAR"; + update_time: string; } diff --git a/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts index 39fa6a401..dd34b5929 100644 --- a/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts @@ -1,17 +1,17 @@ import { KtLogger } from "../../../../logger/logger"; import GenericParseEamIIDXCSV from "../../common/eamusement-iidx-csv/parser"; import { - IIDXEamusementCSVContext, - IIDXEamusementCSVData, + IIDXEamusementCSVContext, + IIDXEamusementCSVData, } from "../../common/eamusement-iidx-csv/types"; import { ParserFunctionReturns } from "../../common/types"; function ParsePLIIIDXCSV( - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ): ParserFunctionReturns { - return GenericParseEamIIDXCSV(fileData, body, "PLI", logger); + return GenericParseEamIIDXCSV(fileData, body, "PLI", logger); } export default ParsePLIIIDXCSV; diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts index 2698cb9d0..fe42228b0 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts @@ -2,10 +2,10 @@ import t from "tap"; import CreateLogCtx from "../../../../logger/logger"; import ResetDBState from "../../../../../test-utils/resets"; import { - GetKTDataJSON, - LoadKTBlackIIDXData, - Testing511Song, - Testing511SPA, + GetKTDataJSON, + LoadKTBlackIIDXData, + Testing511Song, + Testing511SPA, } from "../../../../../test-utils/test-data"; import { ConvertFileS3, ParseDifficulty, ResolveS3Lamp } from "./converter"; import { S3Score } from "./types"; @@ -15,210 +15,210 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); function cfile(data: S3Score) { - return ConvertFileS3(data, {}, "file/solid-state-squad", logger); + return ConvertFileS3(data, {}, "file/solid-state-squad", logger); } t.test("#ConvertFileS3", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => { - delete BaseS3Score._id; // just incase - }); + t.beforeEach(ResetDBState); + t.beforeEach(() => { + delete BaseS3Score._id; // just incase + }); - function mfile(merge: Partial) { - return cfile(deepmerge(BaseS3Score, merge)); - } + function mfile(merge: Partial) { + return cfile(deepmerge(BaseS3Score, merge)); + } - const dryScore = { - game: "iidx", - comment: null, - importType: "file/solid-state-squad", - service: "Solid State Squad", - scoreData: { - // percent: 6.36, -- fpa - grade: "F", - score: 100, - lamp: "FULL COMBO", - hitData: { - pgreat: 25, - great: 50, - good: 0, - bad: 0, - poor: 4, - }, - hitMeta: {}, - }, - scoreMeta: {}, - // can't be tested because the timestamp format doesnt specify a timezone. - // timeAchieved: 1287460462000, - }; + const dryScore = { + game: "iidx", + comment: null, + importType: "file/solid-state-squad", + service: "Solid State Squad", + scoreData: { + // percent: 6.36, -- fpa + grade: "F", + score: 100, + lamp: "FULL COMBO", + hitData: { + pgreat: 25, + great: 50, + good: 0, + bad: 0, + poor: 4, + }, + hitMeta: {}, + }, + scoreMeta: {}, + // can't be tested because the timestamp format doesnt specify a timezone. + // timeAchieved: 1287460462000, + }; - const BaseS3Score = GetKTDataJSON("./s3/s3score.json"); + const BaseS3Score = GetKTDataJSON("./s3/s3score.json"); - t.test("Should import a valid S3 score", async (t) => { - const res = await cfile(BaseS3Score); + t.test("Should import a valid S3 score", async (t) => { + const res = await cfile(BaseS3Score); - t.hasStrict( - res, - { - chart: Testing511SPA, - song: Testing511Song, - dryScore, - }, - "Should correctly return the song, chart and DryScore." - ); + t.hasStrict( + res, + { + chart: Testing511SPA, + song: Testing511Song, + dryScore, + }, + "Should correctly return the song, chart and DryScore." + ); - t.end(); - }); + t.end(); + }); - t.test("Should support comments in S3 scores", async (t) => { - const res = await mfile({ comment: "FOO BAR" }); + t.test("Should support comments in S3 scores", async (t) => { + const res = await mfile({ comment: "FOO BAR" }); - t.hasStrict( - res, - { - chart: Testing511SPA, - song: Testing511Song, - dryScore: deepmerge(dryScore, { comment: "FOO BAR" }), - } as any, - "Should correctly return the song, chart and DryScore." - ); + t.hasStrict( + res, + { + chart: Testing511SPA, + song: Testing511Song, + dryScore: deepmerge(dryScore, { comment: "FOO BAR" }), + } as any, + "Should correctly return the song, chart and DryScore." + ); - t.end(); - }); + t.end(); + }); - t.test("Should find song case-insensitively", async (t) => { - await LoadKTBlackIIDXData(); + t.test("Should find song case-insensitively", async (t) => { + await LoadKTBlackIIDXData(); - const res = await mfile({ songname: "aBSolUte", diff: 7 }); + const res = await mfile({ songname: "aBSolUte", diff: 7 }); - t.hasStrict( - res, - { - chart: { songID: 97, difficulty: "HYPER", playtype: "SP" }, - song: { title: "ABSOLUTE" }, - // dryScore, dont care - }, - "Should correctly return the song, chart and DryScore." - ); + t.hasStrict( + res, + { + chart: { songID: 97, difficulty: "HYPER", playtype: "SP" }, + song: { title: "ABSOLUTE" }, + // dryScore, dont care + }, + "Should correctly return the song, chart and DryScore." + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid styles in S3 scores", (t) => { - t.rejects(mfile({ styles: "3rd,4th,INVALID" }), { - message: /Song has invalid style INVALID/u, - } as any); + t.test("Should reject invalid styles in S3 scores", (t) => { + t.rejects(mfile({ styles: "3rd,4th,INVALID" }), { + message: /Song has invalid style INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw ktdatanf if no song", (t) => { - t.rejects(mfile({ songname: "INVALID SONG TITLE" }), { - message: /Could not find song with title INVALID SONG TITLE/u, - } as any); + t.test("Should throw ktdatanf if no song", (t) => { + t.rejects(mfile({ songname: "INVALID SONG TITLE" }), { + message: /Could not find song with title INVALID SONG TITLE/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw ktdatanf if no song", (t) => { - t.rejects(mfile({ diff: "B" }), { - message: /Could not find chart 5\.1\.1\. \(SP LEGGENDARIA/u, - } as any); + t.test("Should throw ktdatanf if no song", (t) => { + t.rejects(mfile({ diff: "B" }), { + message: /Could not find chart 5\.1\.1\. \(SP LEGGENDARIA/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw a skipscore if the song is 5key", (t) => { - t.rejects(mfile({ diff: 5 }), { - message: /5KEY scores are not supported/u, - } as any); + t.test("Should throw a skipscore if the song is 5key", (t) => { + t.rejects(mfile({ diff: 5 }), { + message: /5KEY scores are not supported/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw an invalidscore if the difficulty is invalid", (t) => { - t.rejects(mfile({ diff: "INVALID" as any }), { - message: /Invalid difficulty INVALID/u, - } as any); + t.test("Should throw an invalidscore if the difficulty is invalid", (t) => { + t.rejects(mfile({ diff: "INVALID" as any }), { + message: /Invalid difficulty INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw an invalidscore if the hardeasy is invalid", (t) => { - t.rejects(mfile({ mods: { hardeasy: "INVALID" } as any, cleartype: "cleared" }), { - message: /Invalid cleartype of 'cleared' with hardeasy of INVALID/u, - } as any); + t.test("Should throw an invalidscore if the hardeasy is invalid", (t) => { + t.rejects(mfile({ mods: { hardeasy: "INVALID" } as any, cleartype: "cleared" }), { + message: /Invalid cleartype of 'cleared' with hardeasy of INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw an invalidscore if the cleartype is invalid", (t) => { - t.rejects(mfile({ cleartype: "INVALID" as any }), { - message: /Invalid cleartype of INVALID/u, - } as any); + t.test("Should throw an invalidscore if the cleartype is invalid", (t) => { + t.rejects(mfile({ cleartype: "INVALID" as any }), { + message: /Invalid cleartype of INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw an invalidscore if the exscore is greater than MAX", (t) => { - t.rejects(mfile({ exscore: 10000 }), { - message: /Invalid percent of 636/u, - } as any); + t.test("Should throw an invalidscore if the exscore is greater than MAX", (t) => { + t.rejects(mfile({ exscore: 10000 }), { + message: /Invalid percent of 636/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw an invalidscore if the date is invalid.", (t) => { - t.rejects(mfile({ date: "INVALID" }), { - message: /Invalid\/Unparsable score timestamp of INVALID/u, - } as any); + t.test("Should throw an invalidscore if the date is invalid.", (t) => { + t.rejects(mfile({ date: "INVALID" }), { + message: /Invalid\/Unparsable score timestamp of INVALID/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#ParseDifficulty", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.strictSame(ParseDifficulty("L7"), { playtype: "SP", difficulty: "NORMAL" }); - t.strictSame(ParseDifficulty(7), { playtype: "SP", difficulty: "HYPER" }); - t.strictSame(ParseDifficulty("A"), { playtype: "SP", difficulty: "ANOTHER" }); - t.strictSame(ParseDifficulty("B"), { playtype: "SP", difficulty: "LEGGENDARIA" }); - t.strictSame(ParseDifficulty("L14"), { playtype: "DP", difficulty: "NORMAL" }); - t.strictSame(ParseDifficulty(14), { playtype: "DP", difficulty: "HYPER" }); - t.strictSame(ParseDifficulty("A14"), { playtype: "DP", difficulty: "ANOTHER" }); - t.strictSame(ParseDifficulty("B14"), { playtype: "DP", difficulty: "LEGGENDARIA" }); - t.throws(() => ParseDifficulty(5)); + t.strictSame(ParseDifficulty("L7"), { playtype: "SP", difficulty: "NORMAL" }); + t.strictSame(ParseDifficulty(7), { playtype: "SP", difficulty: "HYPER" }); + t.strictSame(ParseDifficulty("A"), { playtype: "SP", difficulty: "ANOTHER" }); + t.strictSame(ParseDifficulty("B"), { playtype: "SP", difficulty: "LEGGENDARIA" }); + t.strictSame(ParseDifficulty("L14"), { playtype: "DP", difficulty: "NORMAL" }); + t.strictSame(ParseDifficulty(14), { playtype: "DP", difficulty: "HYPER" }); + t.strictSame(ParseDifficulty("A14"), { playtype: "DP", difficulty: "ANOTHER" }); + t.strictSame(ParseDifficulty("B14"), { playtype: "DP", difficulty: "LEGGENDARIA" }); + t.throws(() => ParseDifficulty(5)); - t.end(); + t.end(); }); t.test("#ResolveS3Lamp", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.equal(ResolveS3Lamp({ cleartype: "played" } as S3Score, logger), "FAILED"); - t.equal(ResolveS3Lamp({ cleartype: "cleared", mods: {} } as S3Score, logger), "CLEAR"); - t.equal( - ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "E" } } as S3Score, logger), - "EASY CLEAR" - ); - t.equal( - ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "H" } } as S3Score, logger), - "HARD CLEAR" - ); - t.equal(ResolveS3Lamp({ cleartype: "combo" } as S3Score, logger), "FULL COMBO"); - t.equal(ResolveS3Lamp({ cleartype: "comboed" } as S3Score, logger), "FULL COMBO"); - t.equal(ResolveS3Lamp({ cleartype: "perfect" } as S3Score, logger), "FULL COMBO"); - t.equal(ResolveS3Lamp({ cleartype: "perfected" } as S3Score, logger), "FULL COMBO"); + t.equal(ResolveS3Lamp({ cleartype: "played" } as S3Score, logger), "FAILED"); + t.equal(ResolveS3Lamp({ cleartype: "cleared", mods: {} } as S3Score, logger), "CLEAR"); + t.equal( + ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "E" } } as S3Score, logger), + "EASY CLEAR" + ); + t.equal( + ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "H" } } as S3Score, logger), + "HARD CLEAR" + ); + t.equal(ResolveS3Lamp({ cleartype: "combo" } as S3Score, logger), "FULL COMBO"); + t.equal(ResolveS3Lamp({ cleartype: "comboed" } as S3Score, logger), "FULL COMBO"); + t.equal(ResolveS3Lamp({ cleartype: "perfect" } as S3Score, logger), "FULL COMBO"); + t.equal(ResolveS3Lamp({ cleartype: "perfected" } as S3Score, logger), "FULL COMBO"); - t.throws(() => ResolveS3Lamp({ cleartype: "invalid" } as any, logger)); - t.throws(() => - ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "invalid" } } as any, logger) - ); + t.throws(() => ResolveS3Lamp({ cleartype: "invalid" } as any, logger)); + t.throws(() => + ResolveS3Lamp({ cleartype: "cleared", mods: { hardeasy: "invalid" } } as any, logger) + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts index f601334b3..772dff01d 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts @@ -1,174 +1,174 @@ import { EmptyObject } from "../../../../../utils/types"; import { KtLogger } from "../../../../logger/logger"; import { - InvalidScoreFailure, - KTDataNotFoundFailure, - SkipScoreFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, + SkipScoreFailure, } from "../../../framework/common/converter-failures"; import { S3Score } from "./types"; import { Playtypes, Difficulties, Grades, Lamps } from "tachi-common"; import { FindSongOnTitleInsensitive } from "../../../../../utils/queries/songs"; import { FindChartWithPTDFVersion } from "../../../../../utils/queries/charts"; import { - GenericGetGradeAndPercent, - ParseDateFromString, + GenericGetGradeAndPercent, + ParseDateFromString, } from "../../../framework/common/score-utils"; import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../../common/types"; export function ParseDifficulty(diff: S3Score["diff"]): { - playtype: Playtypes["iidx"]; - difficulty: Difficulties["iidx:SP" | "iidx:DP"]; + playtype: Playtypes["iidx"]; + difficulty: Difficulties["iidx:SP" | "iidx:DP"]; } { - switch (diff) { - case "L7": - return { playtype: "SP", difficulty: "NORMAL" }; - case 7: - return { playtype: "SP", difficulty: "HYPER" }; - case "A": - return { playtype: "SP", difficulty: "ANOTHER" }; - case "B": - return { playtype: "SP", difficulty: "LEGGENDARIA" }; - case 5: - throw new SkipScoreFailure(`5KEY scores are not supported.`); - case "L14": - return { playtype: "DP", difficulty: "NORMAL" }; - case 14: - return { playtype: "DP", difficulty: "HYPER" }; - case "A14": - return { playtype: "DP", difficulty: "ANOTHER" }; - case "B14": - return { playtype: "DP", difficulty: "LEGGENDARIA" }; - default: - throw new InvalidScoreFailure(`Invalid difficulty ${diff}.`); - } + switch (diff) { + case "L7": + return { playtype: "SP", difficulty: "NORMAL" }; + case 7: + return { playtype: "SP", difficulty: "HYPER" }; + case "A": + return { playtype: "SP", difficulty: "ANOTHER" }; + case "B": + return { playtype: "SP", difficulty: "LEGGENDARIA" }; + case 5: + throw new SkipScoreFailure(`5KEY scores are not supported.`); + case "L14": + return { playtype: "DP", difficulty: "NORMAL" }; + case 14: + return { playtype: "DP", difficulty: "HYPER" }; + case "A14": + return { playtype: "DP", difficulty: "ANOTHER" }; + case "B14": + return { playtype: "DP", difficulty: "LEGGENDARIA" }; + default: + throw new InvalidScoreFailure(`Invalid difficulty ${diff}.`); + } } export function ResolveS3Lamp(data: S3Score, logger: KtLogger): Lamps["iidx:SP" | "iidx:DP"] { - if (data.cleartype === "played") { - return "FAILED"; - } else if (data.cleartype === "cleared") { - if (!data.mods.hardeasy) { - return "CLEAR"; - } else if (data.mods.hardeasy === "H") { - return "HARD CLEAR"; - } else if (data.mods.hardeasy === "E") { - return "EASY CLEAR"; - } else { - logger.warn(`Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?`); - throw new InvalidScoreFailure( - `Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?` - ); - } - } else if ( - data.cleartype === "combo" || - data.cleartype === "comboed" || - data.cleartype === "perfect" || - data.cleartype === "perfected" - ) { - return "FULL COMBO"; - } + if (data.cleartype === "played") { + return "FAILED"; + } else if (data.cleartype === "cleared") { + if (!data.mods.hardeasy) { + return "CLEAR"; + } else if (data.mods.hardeasy === "H") { + return "HARD CLEAR"; + } else if (data.mods.hardeasy === "E") { + return "EASY CLEAR"; + } else { + logger.warn(`Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?`); + throw new InvalidScoreFailure( + `Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?` + ); + } + } else if ( + data.cleartype === "combo" || + data.cleartype === "comboed" || + data.cleartype === "perfect" || + data.cleartype === "perfected" + ) { + return "FULL COMBO"; + } - throw new InvalidScoreFailure(`Invalid cleartype of ${data.cleartype}.`); + throw new InvalidScoreFailure(`Invalid cleartype of ${data.cleartype}.`); } const S3_VERSION_CONV: Record = { - "3rd": "3-cs", - "4th": "4-cs", - "5th": "5-cs", - "6th": "6-cs", - "7th": "7-cs", - "8th": "8-cs", - "9th": "9-cs", - "10th": "10-cs", - red: "11-cs", - hs: "12-cs", - dd: "13-cs", - gold: "14-cs", - djt: "15-cs", - emp: "16-cs", - pb: "16-cs", - us: "bmus", + "3rd": "3-cs", + "4th": "4-cs", + "5th": "5-cs", + "6th": "6-cs", + "7th": "7-cs", + "8th": "8-cs", + "9th": "9-cs", + "10th": "10-cs", + red: "11-cs", + hs: "12-cs", + dd: "13-cs", + gold: "14-cs", + djt: "15-cs", + emp: "16-cs", + pb: "16-cs", + us: "bmus", }; function ConvertVersion(joinedStyles: string) { - const styles = joinedStyles.split(","); + const styles = joinedStyles.split(","); - const style = styles[styles.length - 1]; + const style = styles[styles.length - 1]; - const convertedStyle = S3_VERSION_CONV[style]; + const convertedStyle = S3_VERSION_CONV[style]; - if (!convertedStyle) { - throw new InvalidScoreFailure(`Song has invalid style ${style}.`); - } + if (!convertedStyle) { + throw new InvalidScoreFailure(`Song has invalid style ${style}.`); + } - return convertedStyle; + return convertedStyle; } export const ConvertFileS3: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const song = await FindSongOnTitleInsensitive("iidx", data.songname); + const song = await FindSongOnTitleInsensitive("iidx", data.songname); - if (!song) { - throw new KTDataNotFoundFailure( - `Could not find song with title ${data.songname}`, - importType, - data, - context - ); - } + if (!song) { + throw new KTDataNotFoundFailure( + `Could not find song with title ${data.songname}`, + importType, + data, + context + ); + } - const { playtype, difficulty } = ParseDifficulty(data.diff); - const version = ConvertVersion(data.styles); + const { playtype, difficulty } = ParseDifficulty(data.diff); + const version = ConvertVersion(data.styles); - const chart = await FindChartWithPTDFVersion("iidx", song.id, playtype, difficulty, version); + const chart = await FindChartWithPTDFVersion("iidx", song.id, playtype, difficulty, version); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart ${data.songname} (${playtype} ${difficulty} version (${version}))`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart ${data.songname} (${playtype} ${difficulty} version (${version}))`, + importType, + data, + context + ); + } - const { percent, grade } = GenericGetGradeAndPercent("iidx", data.exscore, chart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", data.exscore, chart); - const lamp = ResolveS3Lamp(data, logger); + const lamp = ResolveS3Lamp(data, logger); - const timeAchieved = ParseDateFromString(data.date); + const timeAchieved = ParseDateFromString(data.date); - let hitData = {}; - if (data.scorebreakdown) { - hitData = { - pgreat: data.scorebreakdown.justgreats, - great: data.scorebreakdown.greats, - good: data.scorebreakdown.good, - bad: data.scorebreakdown.bad, - poor: data.scorebreakdown.poor, - }; - } + let hitData = {}; + if (data.scorebreakdown) { + hitData = { + pgreat: data.scorebreakdown.justgreats, + great: data.scorebreakdown.greats, + good: data.scorebreakdown.good, + bad: data.scorebreakdown.bad, + poor: data.scorebreakdown.poor, + }; + } - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - game: "iidx", - comment: data.comment ?? null, - importType: "file/solid-state-squad", - service: "Solid State Squad", - scoreData: { - percent, - grade: grade as Grades["iidx:SP" | "iidx:DP"], - score: data.exscore, - lamp, - hitData, - hitMeta: {}, - }, - scoreMeta: {}, - timeAchieved, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + game: "iidx", + comment: data.comment ?? null, + importType: "file/solid-state-squad", + service: "Solid State Squad", + scoreData: { + percent, + grade: grade as Grades["iidx:SP" | "iidx:DP"], + score: data.exscore, + lamp, + hitData, + hitMeta: {}, + }, + scoreMeta: {}, + timeAchieved, + }; - return { chart, song, dryScore }; + return { chart, song, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts index 104d229a7..a2ea54ee4 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts @@ -9,219 +9,219 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#ParseSolidStateXML", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should parse simple, valid S3 XML", (t) => { - const res = ParseSolidStateXML( - MockMulterFile(GetKTDataBuffer("./s3/valid.xml"), "valid.xml"), - {}, - logger - ); + t.test("Should parse simple, valid S3 XML", (t) => { + const res = ParseSolidStateXML( + MockMulterFile(GetKTDataBuffer("./s3/valid.xml"), "valid.xml"), + {}, + logger + ); - t.hasStrict( - res.iterable, - [ - { - id: 187, - diff: 7, - songname: "GAMBOL", - styles: "3rd", - exscore: 100, - scorebreakdown: { - justgreats: 25, - greats: 50, - good: 0, - bad: 0, - poor: 4, - }, - mods: { - hardeasy: "H", - }, - cleartype: "cleared", - date: "2010-10-19 04:54:22", - }, - { - id: 187, - diff: "L7", - songname: "GAMBOL", - styles: "3rd", - exscore: 100, - scorebreakdown: { - justgreats: 25, - greats: 50, - good: 0, - bad: 0, - poor: 4, - }, - mods: {}, - cleartype: "perfect", - date: "2010-10-19 04:54:22", - }, - ], - "Should return the right scores in the iterable." - ); - t.equal(res.game, "iidx", "Should return IIDX as the game."); - t.equal(res.classHandler, null, "Should return no class handler."); - t.strictSame(res.context, {}, "Should return no context."); + t.hasStrict( + res.iterable, + [ + { + id: 187, + diff: 7, + songname: "GAMBOL", + styles: "3rd", + exscore: 100, + scorebreakdown: { + justgreats: 25, + greats: 50, + good: 0, + bad: 0, + poor: 4, + }, + mods: { + hardeasy: "H", + }, + cleartype: "cleared", + date: "2010-10-19 04:54:22", + }, + { + id: 187, + diff: "L7", + songname: "GAMBOL", + styles: "3rd", + exscore: 100, + scorebreakdown: { + justgreats: 25, + greats: 50, + good: 0, + bad: 0, + poor: 4, + }, + mods: {}, + cleartype: "perfect", + date: "2010-10-19 04:54:22", + }, + ], + "Should return the right scores in the iterable." + ); + t.equal(res.game, "iidx", "Should return IIDX as the game."); + t.equal(res.classHandler, null, "Should return no class handler."); + t.strictSame(res.context, {}, "Should return no context."); - t.end(); - }); + t.end(); + }); - t.test("Should parse S3 XML with a single score", (t) => { - const res = ParseSolidStateXML( - MockMulterFile(GetKTDataBuffer("./s3/one-score.xml"), "one-score.xml"), - {}, - logger - ); + t.test("Should parse S3 XML with a single score", (t) => { + const res = ParseSolidStateXML( + MockMulterFile(GetKTDataBuffer("./s3/one-score.xml"), "one-score.xml"), + {}, + logger + ); - t.hasStrict( - res.iterable, - [ - { - id: 187, - diff: 7, - songname: "GAMBOL", - styles: "3rd", - exscore: 100, - scorebreakdown: { - justgreats: 50, - greats: 50, - good: 0, - bad: 0, - poor: 4, - }, - mods: {}, - cleartype: "perfect", - date: "2010-10-19 04:54:22", - }, - ], - "Should return the right score in the iterable." - ); - t.equal(res.game, "iidx", "Should return IIDX as the game."); - t.equal(res.classHandler, null, "Should return no class handler."); - t.strictSame(res.context, {}, "Should return no context."); + t.hasStrict( + res.iterable, + [ + { + id: 187, + diff: 7, + songname: "GAMBOL", + styles: "3rd", + exscore: 100, + scorebreakdown: { + justgreats: 50, + greats: 50, + good: 0, + bad: 0, + poor: 4, + }, + mods: {}, + cleartype: "perfect", + date: "2010-10-19 04:54:22", + }, + ], + "Should return the right score in the iterable." + ); + t.equal(res.game, "iidx", "Should return IIDX as the game."); + t.equal(res.classHandler, null, "Should return no class handler."); + t.strictSame(res.context, {}, "Should return no context."); - t.end(); - }); + t.end(); + }); - t.test("Should reject S3 XML with no scores", (t) => { - t.throws( - () => - ParseSolidStateXML( - MockMulterFile(GetKTDataBuffer("./s3/no-score-data.xml"), "no-score-data.xml"), + t.test("Should reject S3 XML with no scores", (t) => { + t.throws( + () => + ParseSolidStateXML( + MockMulterFile(GetKTDataBuffer("./s3/no-score-data.xml"), "no-score-data.xml"), - {}, - logger - ), - { message: /Invalid S3 XML/u } as any - ); + {}, + logger + ), + { message: /Invalid S3 XML/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid lamps", (t) => { - t.throws( - () => - ParseSolidStateXML( - MockMulterFile(GetKTDataBuffer("./s3/invalid-lamp.xml"), "invalid-lamp.xml"), - {}, - logger - ), - { message: /Invalid S3 XML.*cleartype.*BAD LAMP/u } as any - ); + t.test("Should reject invalid lamps", (t) => { + t.throws( + () => + ParseSolidStateXML( + MockMulterFile(GetKTDataBuffer("./s3/invalid-lamp.xml"), "invalid-lamp.xml"), + {}, + logger + ), + { message: /Invalid S3 XML.*cleartype.*BAD LAMP/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject malicious mods", (t) => { - t.throws( - () => - ParseSolidStateXML( - MockMulterFile( - GetKTDataBuffer("./s3/malicious-mods.xml"), - "malicious-mods.xml" - ), - {}, - logger - ), - { message: /Invalid S3 XML.*object.*1/u } as any - ); + t.test("Should reject malicious mods", (t) => { + t.throws( + () => + ParseSolidStateXML( + MockMulterFile( + GetKTDataBuffer("./s3/malicious-mods.xml"), + "malicious-mods.xml" + ), + {}, + logger + ), + { message: /Invalid S3 XML.*object.*1/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject malicious scorebreakdown", (t) => { - t.throws( - () => - ParseSolidStateXML( - MockMulterFile( - GetKTDataBuffer("./s3/malicious-scorebreakdown.xml"), - "malicious-scorebreakdown.xml" - ), - {}, - logger - ), - { message: /Invalid S3 XML.*object.*1/u } as any - ); + t.test("Should reject malicious scorebreakdown", (t) => { + t.throws( + () => + ParseSolidStateXML( + MockMulterFile( + GetKTDataBuffer("./s3/malicious-scorebreakdown.xml"), + "malicious-scorebreakdown.xml" + ), + {}, + logger + ), + { message: /Invalid S3 XML.*object.*1/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid exscore", (t) => { - t.throws( - () => - ParseSolidStateXML( - MockMulterFile( - GetKTDataBuffer("./s3/invalid-exscore.xml"), - "invalid-exscore.xml" - ), - {}, - logger - ), - { message: /Invalid S3 XML.*exscore.*positive integer.*-1/u } as any - ); + t.test("Should reject invalid exscore", (t) => { + t.throws( + () => + ParseSolidStateXML( + MockMulterFile( + GetKTDataBuffer("./s3/invalid-exscore.xml"), + "invalid-exscore.xml" + ), + {}, + logger + ), + { message: /Invalid S3 XML.*exscore.*positive integer.*-1/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject billion laughs", (t) => { - t.setTimeout(5000); + t.test("Should reject billion laughs", (t) => { + t.setTimeout(5000); - t.throws( - () => - ParseSolidStateXML( - MockMulterFile( - GetKTDataBuffer("./s3/danger/billion-laughs.xml"), - "billion-laughs.xml" - ), - {}, - logger - ), - { message: /Invalid S3 XML/u } as any - ); + t.throws( + () => + ParseSolidStateXML( + MockMulterFile( + GetKTDataBuffer("./s3/danger/billion-laughs.xml"), + "billion-laughs.xml" + ), + {}, + logger + ), + { message: /Invalid S3 XML/u } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should not expand specifically crafted billion laughs", (t) => { - t.setTimeout(5000); + t.test("Should not expand specifically crafted billion laughs", (t) => { + t.setTimeout(5000); - const res = ParseSolidStateXML( - MockMulterFile( - GetKTDataBuffer("./s3/danger/specific-blaugh.xml"), - "specific-blaugh.xml" - ), - {}, - logger - ); + const res = ParseSolidStateXML( + MockMulterFile( + GetKTDataBuffer("./s3/danger/specific-blaugh.xml"), + "specific-blaugh.xml" + ), + {}, + logger + ); - // @ts-expect-error shush - t.equal(res.iterable[0].songname, "&lol9;", "Should not expand billion laughs."); + // @ts-expect-error shush + t.equal(res.iterable[0].songname, "&lol9;", "Should not expand billion laughs."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts index bf8de700a..0ea24c0cf 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts @@ -8,83 +8,83 @@ import { ParserFunctionReturns } from "../../common/types"; import { KtLogger } from "../../../../logger/logger"; const PR_SolidState: PrudenceSchema = { - s3data: { - userdata: { - id: p.isPositiveInteger, - login: "string", - djname: "string", - }, - scoredata: { - song: [ - { - id: p.isPositiveInteger, - diff: p.isIn("L7", 7, "A", "B", 5, "L14", 14, "A14", "B14"), - songname: "string", - styles: "string", + s3data: { + userdata: { + id: p.isPositiveInteger, + login: "string", + djname: "string", + }, + scoredata: { + song: [ + { + id: p.isPositiveInteger, + diff: p.isIn("L7", 7, "A", "B", 5, "L14", 14, "A14", "B14"), + songname: "string", + styles: "string", - exscore: p.isPositiveInteger, - scorebreakdown: p.optional({ - justgreats: p.isPositiveInteger, - greats: p.isPositiveInteger, - good: p.isPositiveInteger, - bad: p.isPositiveInteger, - poor: p.isPositiveInteger, - }), - mods: { - hardeasy: p.optional(p.isIn("H", "E")), - }, - cleartype: p.isIn( - "played", - "cleared", - "combo", - "comboed", - "perfect", - "perfected" - ), - date: "string", + exscore: p.isPositiveInteger, + scorebreakdown: p.optional({ + justgreats: p.isPositiveInteger, + greats: p.isPositiveInteger, + good: p.isPositiveInteger, + bad: p.isPositiveInteger, + poor: p.isPositiveInteger, + }), + mods: { + hardeasy: p.optional(p.isIn("H", "E")), + }, + cleartype: p.isIn( + "played", + "cleared", + "combo", + "comboed", + "perfect", + "perfected" + ), + date: "string", - // loads of stuff is ignored from this and just defaults to any because we don't use it. - }, - ], - }, - }, + // loads of stuff is ignored from this and just defaults to any because we don't use it. + }, + ], + }, + }, }; export function ParseSolidStateXML( - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ): ParserFunctionReturns { - let parsedXML; + let parsedXML; - try { - parsedXML = FastParseXML(fileData.buffer.toString("utf-8")); - } catch (err) { - logger.info(err); + try { + parsedXML = FastParseXML(fileData.buffer.toString("utf-8")); + } catch (err) { + logger.info(err); - throw new ScoreImportFatalError(400, "Could not parse XML."); - } + throw new ScoreImportFatalError(400, "Could not parse XML."); + } - if (!parsedXML?.s3data?.scoredata?.song) { - throw new ScoreImportFatalError(400, `Invalid S3 XML.`); - } + if (!parsedXML?.s3data?.scoredata?.song) { + throw new ScoreImportFatalError(400, `Invalid S3 XML.`); + } - // the XML parser can't understand this is meant to be an array - // if someone only has one score. - if (!Array.isArray(parsedXML.s3data.scoredata.song)) { - parsedXML.s3data.scoredata.song = [parsedXML.s3data.scoredata.song]; - } + // the XML parser can't understand this is meant to be an array + // if someone only has one score. + if (!Array.isArray(parsedXML.s3data.scoredata.song)) { + parsedXML.s3data.scoredata.song = [parsedXML.s3data.scoredata.song]; + } - const err = p(parsedXML, PR_SolidState, {}, { allowExcessKeys: true }); + const err = p(parsedXML, PR_SolidState, {}, { allowExcessKeys: true }); - if (err) { - throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid S3 XML.")); - } + if (err) { + throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid S3 XML.")); + } - return { - classHandler: null, - context: {}, - iterable: parsedXML.s3data.scoredata.song as S3Score[], - game: "iidx", - }; + return { + classHandler: null, + context: {}, + iterable: parsedXML.s3data.scoredata.song as S3Score[], + game: "iidx", + }; } diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts index 2879ca18d..c51575a36 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts @@ -1,22 +1,22 @@ import { integer } from "tachi-common"; export interface S3Score { - id: integer; - diff: 5 | "L7" | 7 | "A" | "B" | "L14" | 14 | "A14" | "B14"; - songname: string; - exscore: integer; - styles: string; - scorebreakdown?: { - justgreats: integer; - greats: integer; - good: integer; - bad: integer; - poor: integer; - }; - mods: { - hardeasy?: "H" | "E"; - }; - cleartype: "cleared" | "played" | "combo" | "comboed" | "perfect" | "perfected"; - date: string; - comment?: string; + id: integer; + diff: 5 | "L7" | 7 | "A" | "B" | "L14" | 14 | "A14" | "B14"; + songname: string; + exscore: integer; + styles: string; + scorebreakdown?: { + justgreats: integer; + greats: integer; + good: integer; + bad: integer; + poor: integer; + }; + mods: { + hardeasy?: "H" | "E"; + }; + cleartype: "cleared" | "played" | "combo" | "comboed" | "perfect" | "perfected"; + date: string; + comment?: string; } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts b/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts index 52ecf165e..1b8f67be1 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts @@ -10,106 +10,106 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#ConverterIRBarbatos", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const albidaSong = { - title: "ALBIDA Powerless Mix", - artist: "無力P", - id: 1, - firstVersion: "booth", - "alt-titles": [], - "search-titles": ["albida_muryoku", "アルビダパワーレスミックス"], - data: { - uscEquiv: null, - }, - }; + const albidaSong = { + title: "ALBIDA Powerless Mix", + artist: "無力P", + id: 1, + firstVersion: "booth", + "alt-titles": [], + "search-titles": ["albida_muryoku", "アルビダパワーレスミックス"], + data: { + uscEquiv: null, + }, + }; - const albidaChart = { - rgcID: null, - chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b", - difficulty: "ADV", - songID: 1, - playtype: "Single", - levelNum: 10, - level: "10", - flags: { - "IN BASE GAME": true, - OMNIMIX: false, - "N-1": false, - }, - data: { - inGameID: 1, - }, - isPrimary: true, - versions: ["booth", "inf", "gw", "heaven", "vivid"], - }; + const albidaChart = { + rgcID: null, + chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b", + difficulty: "ADV", + songID: 1, + playtype: "Single", + levelNum: 10, + level: "10", + flags: { + "IN BASE GAME": true, + OMNIMIX: false, + "N-1": false, + }, + data: { + inGameID: 1, + }, + isPrimary: true, + versions: ["booth", "inf", "gw", "heaven", "vivid"], + }; - t.test("Should convert a BarbatosScore into a Dry Score", async (t) => { - const res = await ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger); + t.test("Should convert a BarbatosScore into a Dry Score", async (t) => { + const res = await ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger); - t.hasStrict(res, { - song: albidaSong, - chart: albidaChart, - dryScore: { - game: "sdvx", - service: "Barbatos", - comment: null, - importType: "ir/barbatos", - // timeAchieved: , its Date.now() give or take lol - scoreData: { - score: 9000000, - percent: 90, - grade: "A+", - lamp: "CLEAR", - hitData: { - critical: 100, - near: 50, - miss: 5, - }, - hitMeta: { - fast: 40, - slow: 10, - gauge: 90, - maxCombo: 100, - }, - }, - scoreMeta: { - inSkillAnalyser: false, - }, - }, - } as any); + t.hasStrict(res, { + song: albidaSong, + chart: albidaChart, + dryScore: { + game: "sdvx", + service: "Barbatos", + comment: null, + importType: "ir/barbatos", + // timeAchieved: , its Date.now() give or take lol + scoreData: { + score: 9000000, + percent: 90, + grade: "A+", + lamp: "CLEAR", + hitData: { + critical: 100, + near: 50, + miss: 5, + }, + hitMeta: { + fast: 40, + slow: 10, + gauge: 90, + maxCombo: 100, + }, + }, + scoreMeta: { + inSkillAnalyser: false, + }, + }, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw KTDataNotFound if chart not found.", (t) => { - t.rejects( - () => - ConverterIRBarbatos( - deepmerge(barbScore, { song_id: 1000 } as any), - {}, - "ir/barbatos", - logger - ), - { - message: /Could not find chart with songID 1000/u, - } as any - ); + t.test("Should throw KTDataNotFound if chart not found.", (t) => { + t.rejects( + () => + ConverterIRBarbatos( + deepmerge(barbScore, { song_id: 1000 } as any), + {}, + "ir/barbatos", + logger + ), + { + message: /Could not find chart with songID 1000/u, + } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw InternalFailure if song-chart desync.", async (t) => { - await db.songs.sdvx.remove({ id: 1 }); // force a song-chart desync + t.test("Should throw InternalFailure if song-chart desync.", async (t) => { + await db.songs.sdvx.remove({ id: 1 }); // force a song-chart desync - t.rejects(() => ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger), { - message: /Song 1 \(sdvx\) has no parent song/u, - } as any); + t.rejects(() => ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger), { + message: /Song 1 \(sdvx\) has no parent song/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts index aa1abaf48..4266d0423 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts @@ -3,8 +3,8 @@ import { FindSongOnID } from "../../../../../utils/queries/songs"; import { EmptyObject } from "../../../../../utils/types"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { - InternalFailure, - KTDataNotFoundFailure, + InternalFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { BarbatosScore } from "./types"; import { Lamps } from "tachi-common"; @@ -12,81 +12,81 @@ import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../../common/types"; const LAMP_LOOKUP = { - 1: "FAILED", - 2: "CLEAR", - 3: "EXCESSIVE CLEAR", - 4: "ULTIMATE CHAIN", - 5: "PERFECT ULTIMATE CHAIN", + 1: "FAILED", + 2: "CLEAR", + 3: "EXCESSIVE CLEAR", + 4: "ULTIMATE CHAIN", + 5: "PERFECT ULTIMATE CHAIN", }; const DIFFICULTY_LOOKUP = { - 0: "NOV", - 1: "ADV", - 2: "EXH", - 3: "ANY_INF", // special case for inf/grv/hvn/vvd - which are all the same diff internally. (kinda). - 4: "MXM", + 0: "NOV", + 1: "ADV", + 2: "EXH", + 3: "ANY_INF", // special case for inf/grv/hvn/vvd - which are all the same diff internally. (kinda). + 4: "MXM", }; export const ConverterIRBarbatos: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const difficulty = DIFFICULTY_LOOKUP[data.difficulty] as - | "NOV" - | "ADV" - | "EXH" - | "ANY_INF" - | "MXM"; + const difficulty = DIFFICULTY_LOOKUP[data.difficulty] as + | "NOV" + | "ADV" + | "EXH" + | "ANY_INF" + | "MXM"; - const chart = await FindSDVXChartOnInGameID(data.song_id, difficulty); + const chart = await FindSDVXChartOnInGameID(data.song_id, difficulty); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with songID ${data.song_id} (${difficulty})`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${data.song_id} (${difficulty})`, + importType, + data, + context + ); + } - const song = await FindSongOnID("sdvx", chart.songID); + const song = await FindSongOnID("sdvx", chart.songID); - if (!song) { - logger.severe(`Song ${chart.songID} (sdvx) has no parent song?`); - throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`); - } + if (!song) { + logger.severe(`Song ${chart.songID} (sdvx) has no parent song?`); + throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`); + } - const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart); + const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart); - const dryScore: DryScore<"sdvx:Single"> = { - game: "sdvx", - service: "Barbatos", - comment: null, - importType: "ir/barbatos", - timeAchieved: Date.now(), - scoreData: { - score: data.score, - percent, - grade, - lamp: LAMP_LOOKUP[data.clear_type] as Lamps["sdvx:Single"], - hitData: { - critical: data.critical, - near: data.near_total, - miss: data.error, - }, - hitMeta: { - fast: data.near_fast, - slow: data.near_slow, - gauge: data.percent, - maxCombo: data.max_chain, - }, - }, - scoreMeta: { - inSkillAnalyser: data.is_skill_analyser, - }, - }; + const dryScore: DryScore<"sdvx:Single"> = { + game: "sdvx", + service: "Barbatos", + comment: null, + importType: "ir/barbatos", + timeAchieved: Date.now(), + scoreData: { + score: data.score, + percent, + grade, + lamp: LAMP_LOOKUP[data.clear_type] as Lamps["sdvx:Single"], + hitData: { + critical: data.critical, + near: data.near_total, + miss: data.error, + }, + hitMeta: { + fast: data.near_fast, + slow: data.near_slow, + gauge: data.percent, + maxCombo: data.max_chain, + }, + }, + scoreMeta: { + inSkillAnalyser: data.is_skill_analyser, + }, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts b/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts index 8ed0a5dbf..54cbe2287 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts @@ -8,29 +8,29 @@ import { ParseBarbatosSingle } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseBarbatosSingle", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return the score as a payload", (t) => { - const res = ParseBarbatosSingle(barbScore as unknown as Record, logger); + t.test("Should return the score as a payload", (t) => { + const res = ParseBarbatosSingle(barbScore as unknown as Record, logger); - t.hasStrict(res, { - game: "sdvx", - context: {}, - iterable: [barbScore], - }); + t.hasStrict(res, { + game: "sdvx", + context: {}, + iterable: [barbScore], + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid scores", (t) => { - t.throws(() => ParseBarbatosSingle({}, logger), { - message: "Invalid Barbatos Request", - } as any); + t.test("Should reject invalid scores", (t) => { + t.throws(() => ParseBarbatosSingle({}, logger), { + message: "Invalid Barbatos Request", + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts index f4b0bd26f..f1342a23a 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts @@ -8,38 +8,38 @@ import { ConverterIRBarbatos } from "./converter"; import { ParserFunctionReturns } from "../../common/types"; const PR_Barbatos = { - difficulty: p.isIn(1, 2, 3, 4), - level: p.isBoundedInteger(1, 20), - song_id: p.isPositiveInteger, - max_chain: p.isPositiveInteger, - critical: p.isPositiveInteger, - near_total: p.isPositiveInteger, - near_fast: p.isPositiveInteger, - near_slow: p.isPositiveInteger, - score: p.isBoundedInteger(0, 10_000_000), - error: p.isPositiveInteger, - percent: p.isBetween(0, 100), - did_fail: "boolean", - clear_type: p.isIn(1, 2, 3, 4, 5), - gauge_type: p.isIn(0, 1, 2, 3), - is_skill_analyser: "boolean", + difficulty: p.isIn(1, 2, 3, 4), + level: p.isBoundedInteger(1, 20), + song_id: p.isPositiveInteger, + max_chain: p.isPositiveInteger, + critical: p.isPositiveInteger, + near_total: p.isPositiveInteger, + near_fast: p.isPositiveInteger, + near_slow: p.isPositiveInteger, + score: p.isBoundedInteger(0, 10_000_000), + error: p.isPositiveInteger, + percent: p.isBetween(0, 100), + did_fail: "boolean", + clear_type: p.isIn(1, 2, 3, 4, 5), + gauge_type: p.isIn(0, 1, 2, 3), + is_skill_analyser: "boolean", }; export function ParseBarbatosSingle( - body: Record, - logger: KtLogger + body: Record, + logger: KtLogger ): ParserFunctionReturns { - const err = p(body, PR_Barbatos); + const err = p(body, PR_Barbatos); - if (err) { - throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Barbatos Request")); - } + if (err) { + throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Barbatos Request")); + } - // asserted using prudence. - return { - context: {}, - game: "sdvx", - iterable: [body] as unknown as BarbatosScore[], - classHandler: null, - }; + // asserted using prudence. + return { + context: {}, + game: "sdvx", + iterable: [body] as unknown as BarbatosScore[], + classHandler: null, + }; } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/types.ts b/server/src/lib/score-import/import-types/ir/barbatos/types.ts index 7953888a4..d57506670 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/types.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/types.ts @@ -1,19 +1,19 @@ import { integer } from "tachi-common"; export interface BarbatosScore { - difficulty: 0 | 1 | 2 | 3 | 4; - level: integer; - song_id: integer; - max_chain: integer; - critical: integer; - near_total: integer; - near_fast: integer; - near_slow: integer; - score: integer; - error: integer; - percent: number; - did_fail: boolean; - clear_type: 1 | 2 | 3 | 4 | 5; - gauge_type: 1 | 2; - is_skill_analyser: boolean; + difficulty: 0 | 1 | 2 | 3 | 4; + level: integer; + song_id: integer; + max_chain: integer; + critical: integer; + near_total: integer; + near_fast: integer; + near_slow: integer; + score: integer; + error: integer; + percent: number; + did_fail: boolean; + clear_type: 1 | 2 | 3 | 4 | 5; + gauge_type: 1 | 2; + is_skill_analyser: boolean; } diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts index d7a68076a..1cd7cb289 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts @@ -1,8 +1,8 @@ import { FindChartOnSHA256 } from "../../../../../utils/queries/charts"; import { FindSongOnID } from "../../../../../utils/queries/songs"; import { - InternalFailure, - KTDataNotFoundFailure, + InternalFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { DryScore } from "../../../framework/common/types"; @@ -11,127 +11,127 @@ import { BeatorajaContext, BeatorajaScore } from "./types"; import { ChartDocument } from "tachi-common"; const LAMP_LOOKUP = { - NoPlay: "NO PLAY", - Failed: "FAILED", - LightAssistEasy: "ASSIST CLEAR", - Easy: "EASY CLEAR", - Normal: "CLEAR", - Hard: "HARD CLEAR", - ExHard: "EX HARD CLEAR", - FullCombo: "FULL COMBO", - Perfect: "FULL COMBO", + NoPlay: "NO PLAY", + Failed: "FAILED", + LightAssistEasy: "ASSIST CLEAR", + Easy: "EASY CLEAR", + Normal: "CLEAR", + Hard: "HARD CLEAR", + ExHard: "EX HARD CLEAR", + FullCombo: "FULL COMBO", + Perfect: "FULL COMBO", } as const; const RANDOM_LOOKUP = { - 0: "NONRAN", - 1: "MIRROR", - 2: "RANDOM", - 3: "R-RANDOM", - 4: "S-RANDOM", + 0: "NONRAN", + 1: "MIRROR", + 2: "RANDOM", + 3: "R-RANDOM", + 4: "S-RANDOM", } as const; export const ConverterIRBeatoraja: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument< - "bms:7K" | "bms:14K" - > | null; + const chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument< + "bms:7K" | "bms:14K" + > | null; - if (!chart) { - // @todo #141 Import charts into the Kamaitachi Database if the chart - // doesn't exist. - throw new KTDataNotFoundFailure( - `Could not find chart ${context.chart.title}.`, - importType, - data, - context - ); - } + if (!chart) { + // @todo #141 Import charts into the Kamaitachi Database if the chart + // doesn't exist. + throw new KTDataNotFoundFailure( + `Could not find chart ${context.chart.title}.`, + importType, + data, + context + ); + } - const song = await FindSongOnID("bms", chart.songID); + const song = await FindSongOnID("bms", chart.songID); - if (!song) { - logger.severe(`Song-Chart Desync with BMS ${chart.chartID}.`); - throw new InternalFailure(`Song-Chart Desync with BMS ${chart.chartID}.`); - } + if (!song) { + logger.severe(`Song-Chart Desync with BMS ${chart.chartID}.`); + throw new InternalFailure(`Song-Chart Desync with BMS ${chart.chartID}.`); + } - // if CN mode - if (data.lntype === 1) { - // Divide the EX score by the CN note count, then multiply it by the LN - // notecount. This converts CN EXScores to LN EXScores by routing - // through percent. - data.exscore = Math.floor((data.exscore / context.chart.notes) * chart.data.notecount); - } + // if CN mode + if (data.lntype === 1) { + // Divide the EX score by the CN note count, then multiply it by the LN + // notecount. This converts CN EXScores to LN EXScores by routing + // through percent. + data.exscore = Math.floor((data.exscore / context.chart.notes) * chart.data.notecount); + } - const { grade, percent } = GenericGetGradeAndPercent("bms", data.exscore, chart); + const { grade, percent } = GenericGetGradeAndPercent("bms", data.exscore, chart); - const hitMeta: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitMeta"] = { - bp: data.minbp === -1 ? null : data.minbp, - gauge: data.gauge === -1 ? null : data.gauge, - }; + const hitMeta: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitMeta"] = { + bp: data.minbp === -1 ? null : data.minbp, + gauge: data.gauge === -1 ? null : data.gauge, + }; - let hitData: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitData"] = {}; + let hitData: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitData"] = {}; - // if NOT CN mode we can assign judgements properly - if (data.lntype === 0) { - for (const k of [ - "ebd", - "lbd", - "egd", - "lgd", - "egr", - "lgr", - "epg", - "lpg", - "epr", - "lpr", - ] as const) { - hitMeta[k] = data[k]; - } + // if NOT CN mode we can assign judgements properly + if (data.lntype === 0) { + for (const k of [ + "ebd", + "lbd", + "egd", + "lgd", + "egr", + "lgr", + "epg", + "lpg", + "epr", + "lpr", + ] as const) { + hitMeta[k] = data[k]; + } - hitMeta.epr! += data.ems; - hitMeta.lpr! += data.lms; + hitMeta.epr! += data.ems; + hitMeta.lpr! += data.lms; - hitData = { - pgreat: data.epg + data.lpg, - great: data.egr + data.lgr, - good: data.egd + data.lgd, - bad: data.ebd + data.lbd, - poor: data.epr + data.lpr + data.ems + data.lms, - }; - } + hitData = { + pgreat: data.epg + data.lpg, + great: data.egr + data.lgr, + good: data.egd + data.lgd, + bad: data.ebd + data.lbd, + poor: data.epr + data.lpr + data.ems + data.lms, + }; + } - let random = null; + let random = null; - if (chart.playtype === "7K") { - random = RANDOM_LOOKUP[data.option]; - } + if (chart.playtype === "7K") { + random = RANDOM_LOOKUP[data.option]; + } - const lamp = LAMP_LOOKUP[data.clear]; + const lamp = LAMP_LOOKUP[data.clear]; - const dryScore: DryScore<"bms:7K" | "bms:14K"> = { - comment: null, - game: "bms", - importType, - scoreData: { - grade, - percent, - score: data.exscore, - lamp, - hitMeta, - hitData, - }, - scoreMeta: { - client: context.client, - inputDevice: data.deviceType, - random, - }, - timeAchieved: Date.now(), - service: "Beatoraja IR", - }; + const dryScore: DryScore<"bms:7K" | "bms:14K"> = { + comment: null, + game: "bms", + importType, + scoreData: { + grade, + percent, + score: data.exscore, + lamp, + hitMeta, + hitData, + }, + scoreMeta: { + client: context.client, + inputDevice: data.deviceType, + random, + }, + timeAchieved: Date.now(), + service: "Beatoraja IR", + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts index 009e50f33..b753feba6 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts @@ -7,107 +7,107 @@ import { ConverterIRBeatoraja } from "./converter"; import { ParserFunctionReturns } from "../../common/types"; const PR_BeatorajaScore = { - sha256: "string", - exscore: p.isPositiveInteger, - passnotes: p.isPositiveInteger, - gauge: p.or(p.isBetween(0, 100), p.is(-1)), - deviceType: p.isIn("BM_CONTROLLER", "KEYBOARD"), - minbp: p.or(p.isPositiveInteger, p.is(-1)), - option: p.isBoundedInteger(0, 4), + sha256: "string", + exscore: p.isPositiveInteger, + passnotes: p.isPositiveInteger, + gauge: p.or(p.isBetween(0, 100), p.is(-1)), + deviceType: p.isIn("BM_CONTROLLER", "KEYBOARD"), + minbp: p.or(p.isPositiveInteger, p.is(-1)), + option: p.isBoundedInteger(0, 4), - lntype: p.isIn(0, 1), - clear: p.isIn( - "NoPlay", - "Failed", - "LightAssistEasy", - "Easy", - "Normal", - "Hard", - "ExHard", - "FullCombo", - "Perfect" - ), - assist: p.is(0), - maxcombo: p.isPositiveInteger, + lntype: p.isIn(0, 1), + clear: p.isIn( + "NoPlay", + "Failed", + "LightAssistEasy", + "Easy", + "Normal", + "Hard", + "ExHard", + "FullCombo", + "Perfect" + ), + assist: p.is(0), + maxcombo: p.isPositiveInteger, - epg: p.isPositiveInteger, - egr: p.isPositiveInteger, - egd: p.isPositiveInteger, - ebd: p.isPositiveInteger, - epr: p.isPositiveInteger, - lpg: p.isPositiveInteger, - lgr: p.isPositiveInteger, - lgd: p.isPositiveInteger, - lbd: p.isPositiveInteger, - lpr: p.isPositiveInteger, - ems: p.isPositiveInteger, - lms: p.isPositiveInteger, + epg: p.isPositiveInteger, + egr: p.isPositiveInteger, + egd: p.isPositiveInteger, + ebd: p.isPositiveInteger, + epr: p.isPositiveInteger, + lpg: p.isPositiveInteger, + lgr: p.isPositiveInteger, + lgd: p.isPositiveInteger, + lbd: p.isPositiveInteger, + lpr: p.isPositiveInteger, + ems: p.isPositiveInteger, + lms: p.isPositiveInteger, }; const PR_BeatorajaChart = { - md5: "string", - sha256: "string", - title: "string", - subtitle: "string", - genre: "string", - artist: "string", - subartist: "string", - total: p.isPositive, + md5: "string", + sha256: "string", + title: "string", + subtitle: "string", + genre: "string", + artist: "string", + subartist: "string", + total: p.isPositive, - // currently only accepted playtypes. - mode: p.isIn("BEAT_7K", "BEAT_14K"), - judge: p.isPositive, - notes: p.isPositiveInteger, - hasUndefinedLN: p.is(false), - hasRandom: p.is(false), + // currently only accepted playtypes. + mode: p.isIn("BEAT_7K", "BEAT_14K"), + judge: p.isPositive, + notes: p.isPositiveInteger, + hasUndefinedLN: p.is(false), + hasRandom: p.is(false), }; export function ParseBeatorajaSingle( - body: Record, - logger: KtLogger + body: Record, + logger: KtLogger ): ParserFunctionReturns { - const err = p( - body.score, - PR_BeatorajaScore, - {}, - { allowExcessKeys: true, throwOnNonObject: false } - ); + const err = p( + body.score, + PR_BeatorajaScore, + {}, + { allowExcessKeys: true, throwOnNonObject: false } + ); - if (err) { - throw new ScoreImportFatalError( - 400, - FormatPrError(err, "Invalid Beatoraja Import - Score") - ); - } + if (err) { + throw new ScoreImportFatalError( + 400, + FormatPrError(err, "Invalid Beatoraja Import - Score") + ); + } - const chartErr = p( - body.chart, - PR_BeatorajaChart, - {}, - { allowExcessKeys: true, throwOnNonObject: false } - ); + const chartErr = p( + body.chart, + PR_BeatorajaChart, + {}, + { allowExcessKeys: true, throwOnNonObject: false } + ); - if (chartErr) { - throw new ScoreImportFatalError( - 400, - FormatPrError(chartErr, "Invalid Beatoraja Import - Chart") - ); - } + if (chartErr) { + throw new ScoreImportFatalError( + 400, + FormatPrError(chartErr, "Invalid Beatoraja Import - Chart") + ); + } - const client = body.client; + const client = body.client; - if (client !== "beatoraja" && client !== "lr2oraja") { - throw new ScoreImportFatalError(400, `Unknown client ${client}`); - } + if (client !== "beatoraja" && client !== "lr2oraja") { + throw new ScoreImportFatalError(400, `Unknown client ${client}`); + } - // asserted using prudence. - return { - context: { - client, - chart: body.chart as BeatorajaChart, - }, - game: "bms", - iterable: [body.score] as unknown as BeatorajaScore[], - classHandler: null, - }; + // asserted using prudence. + return { + context: { + client, + chart: body.chart as BeatorajaChart, + }, + game: "bms", + iterable: [body.score] as unknown as BeatorajaScore[], + classHandler: null, + }; } diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts index ad054b39d..7d5b57722 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts @@ -1,58 +1,58 @@ import { integer } from "tachi-common"; export interface BeatorajaContext { - client: "beatoraja" | "lr2oraja"; - chart: BeatorajaChart; + client: "beatoraja" | "lr2oraja"; + chart: BeatorajaChart; } export interface BeatorajaChart { - md5: string; - sha256: string; - title: string; - subtitle: string; - genre: string; - artist: string; - subartist: string; - total: integer; + md5: string; + sha256: string; + title: string; + subtitle: string; + genre: string; + artist: string; + subartist: string; + total: integer; - mode: "BEAT_7K" | "BEAT_14K"; - judge: number; - notes: integer; + mode: "BEAT_7K" | "BEAT_14K"; + judge: number; + notes: integer; } export interface BeatorajaScore { - sha256: string; - exscore: integer; - passnotes: integer; - gauge: number; - deviceType: "BM_CONTROLLER" | "KEYBOARD"; - minbp: integer; - option: 0 | 1 | 2 | 3 | 4; + sha256: string; + exscore: integer; + passnotes: integer; + gauge: number; + deviceType: "BM_CONTROLLER" | "KEYBOARD"; + minbp: integer; + option: 0 | 1 | 2 | 3 | 4; - lntype: 0 | 1; - clear: - | "NoPlay" - | "Failed" - | "LightAssistEasy" - | "Easy" - | "Normal" - | "Hard" - | "ExHard" - | "FullCombo" - | "Perfect"; - assist: 0; - maxcombo: integer; + lntype: 0 | 1; + clear: + | "NoPlay" + | "Failed" + | "LightAssistEasy" + | "Easy" + | "Normal" + | "Hard" + | "ExHard" + | "FullCombo" + | "Perfect"; + assist: 0; + maxcombo: integer; - epg: integer; - egr: integer; - egd: integer; - ebd: integer; - epr: integer; - lpg: integer; - lgr: integer; - lgd: integer; - lbd: integer; - lpr: integer; - ems: integer; - lms: integer; + epg: integer; + egr: integer; + egd: integer; + ebd: integer; + epr: integer; + lpg: integer; + lgr: integer; + lgd: integer; + lbd: integer; + lpr: integer; + ems: integer; + lms: integer; } diff --git a/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts b/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts index 5d26711b9..e90455de3 100644 --- a/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts +++ b/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts @@ -9,10 +9,10 @@ import { ParserFunctionReturns } from "../../common/types"; * @param body - The request body that made this file import request. */ function ParseDirectManual( - body: Record, - logger: KtLogger + body: Record, + logger: KtLogger ): ParserFunctionReturns { - return ParseBatchManualFromObject(body, "ir/direct-manual", logger); + return ParseBatchManualFromObject(body, "ir/direct-manual", logger); } export default ParseDirectManual; diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts index 19c143264..8b9deabbc 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts @@ -7,60 +7,60 @@ import { CreateFerStaticClassHandler } from "./class-handler"; const logger = CreateLogCtx(__filename); t.test("#FerStaticClassHandler", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should curry a function", (t) => { - const res = CreateFerStaticClassHandler({ sp_dan: 1 }); + t.test("Should curry a function", (t) => { + const res = CreateFerStaticClassHandler({ sp_dan: 1 }); - t.equal(typeof res, "function"); + t.equal(typeof res, "function"); - t.end(); - }); + t.end(); + }); - t.test("Should work with no dans", (t) => { - const res = CreateFerStaticClassHandler({})("iidx", "SP", 1, {}, logger); + t.test("Should work with no dans", (t) => { + const res = CreateFerStaticClassHandler({})("iidx", "SP", 1, {}, logger); - t.equal(res, undefined, "Should return nothing."); + t.equal(res, undefined, "Should return nothing."); - t.end(); - }); + t.end(); + }); - t.test("Should update the same dan as the playtype.", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); - const res = fn("iidx", "SP", 1, {}, logger); + t.test("Should update the same dan as the playtype.", (t) => { + const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); + const res = fn("iidx", "SP", 1, {}, logger); - t.strictSame(res, { dan: 5 }, "Should return SP dan's value."); + t.strictSame(res, { dan: 5 }, "Should return SP dan's value."); - const res2 = fn("iidx", "DP", 1, {}, logger); + const res2 = fn("iidx", "DP", 1, {}, logger); - t.strictSame(res2, { dan: 7 }, "Should return DP dan's value."); + t.strictSame(res2, { dan: 7 }, "Should return DP dan's value."); - t.end(); - }); + t.end(); + }); - t.test("Should skip if dan is invalid.", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: -1, dp_dan: 100 }); - const res = fn("iidx", "SP", 1, {}, logger); + t.test("Should skip if dan is invalid.", (t) => { + const fn = CreateFerStaticClassHandler({ sp_dan: -1, dp_dan: 100 }); + const res = fn("iidx", "SP", 1, {}, logger); - t.equal(res, undefined, "Should skip SP dan's value."); + t.equal(res, undefined, "Should skip SP dan's value."); - const res2 = fn("iidx", "DP", 1, {}, logger); + const res2 = fn("iidx", "DP", 1, {}, logger); - t.equal(res2, undefined, "Should skip DP dan's value."); + t.equal(res2, undefined, "Should skip DP dan's value."); - t.end(); - }); + t.end(); + }); - t.test("Should skip if playtype is invalid", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); - const res = fn("iidx", "INVALID" as any, 1, {}, logger); + t.test("Should skip if playtype is invalid", (t) => { + const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); + const res = fn("iidx", "INVALID" as any, 1, {}, logger); - t.equal(res, undefined, "Should skip over as a failsafe."); + t.equal(res, undefined, "Should skip over as a failsafe."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts index 89e512f81..4eab4ed45 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts @@ -2,34 +2,34 @@ import { integer } from "tachi-common"; import { ClassHandler } from "../../../framework/user-game-stats/types"; export function CreateFerStaticClassHandler(body: Record): ClassHandler { - return (game, playtype, userID, ratings, logger) => { - let index; + return (game, playtype, userID, ratings, logger) => { + let index; - if (playtype === "SP") { - index = body.sp_dan; - } else if (playtype === "DP") { - index = body.dp_dan; - } else { - logger.error( - `Invalid playtype ${playtype} passed to FerStaticClassHandler. Attempting to continue.` - ); - return; - } + if (playtype === "SP") { + index = body.sp_dan; + } else if (playtype === "DP") { + index = body.dp_dan; + } else { + logger.error( + `Invalid playtype ${playtype} passed to FerStaticClassHandler. Attempting to continue.` + ); + return; + } - if (!Number.isInteger(index)) { - logger.info(`Recieved invalid fer-static class of ${index}.`); - return; - } + if (!Number.isInteger(index)) { + logger.info(`Recieved invalid fer-static class of ${index}.`); + return; + } - const intIndex = index as integer; + const intIndex = index as integer; - if (intIndex < 0 || intIndex > 18) { - logger.warn(`Invalid fer-static class of ${index}. Skipping.`); - return; - } + if (intIndex < 0 || intIndex > 18) { + logger.warn(`Invalid fer-static class of ${index}. Skipping.`); + return; + } - return { - dan: intIndex, - }; - }; + return { + dan: intIndex, + }; + }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.test.ts index 609714989..7d5ed02ee 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.test.ts @@ -2,5 +2,5 @@ import t from "tap"; import { ConverterIRFervidexStatic } from "./converter"; t.test("#ConverterIRFervidexStatic", (t) => { - t.end(); + t.end(); }); diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts index 4d2779952..1bf4090d3 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts @@ -1,8 +1,8 @@ import { FindSongOnID } from "../../../../../utils/queries/songs"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { - InternalFailure, - KTDataNotFoundFailure, + InternalFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { Lamps } from "tachi-common"; import { FindChartOnInGameIDVersion } from "../../../../../utils/queries/charts"; @@ -12,55 +12,55 @@ import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../../common/types"; export const ConverterIRFervidexStatic: ConverterFunction< - FervidexStaticScore, - FervidexStaticContext + FervidexStaticScore, + FervidexStaticContext > = async (data, context, importType, logger) => { - const { difficulty, playtype } = SplitFervidexChartRef(data.chart); + const { difficulty, playtype } = SplitFervidexChartRef(data.chart); - const chart = await FindChartOnInGameIDVersion( - "iidx", - data.song_id, - playtype, - difficulty, - context.version - ); + const chart = await FindChartOnInGameIDVersion( + "iidx", + data.song_id, + playtype, + difficulty, + context.version + ); - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with songID ${data.song_id} (${playtype} ${difficulty} Version ${context.version})`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${data.song_id} (${playtype} ${difficulty} Version ${context.version})`, + importType, + data, + context + ); + } - const song = await FindSongOnID("iidx", chart.songID); + const song = await FindSongOnID("iidx", chart.songID); - if (!song) { - logger.severe(`Song ${chart.songID} (iidx) has no parent song?`); - throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`); - } + if (!song) { + logger.severe(`Song ${chart.songID} (iidx) has no parent song?`); + throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`); + } - const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - game: "iidx", - service: "Fervidex Static", - comment: null, - importType: "ir/fervidex-static", - timeAchieved: null, - scoreData: { - score: data.ex_score, - percent, - grade, - lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], - hitData: {}, - hitMeta: { - bp: data.miss_count === -1 ? null : data.miss_count, - }, - }, - scoreMeta: {}, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + game: "iidx", + service: "Fervidex Static", + comment: null, + importType: "ir/fervidex-static", + timeAchieved: null, + scoreData: { + score: data.ex_score, + percent, + grade, + lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], + hitData: {}, + hitMeta: { + bp: data.miss_count === -1 ? null : data.miss_count, + }, + }, + scoreMeta: {}, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts index 5e9c6095a..3ad8bfab7 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts @@ -8,177 +8,177 @@ import { ParseFervidexStatic } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseFervidexStatic", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const ferStatic = GetKTDataJSON("./fervidex-static/base.json"); + const ferStatic = GetKTDataJSON("./fervidex-static/base.json"); - t.test("Should parse static data from body", (t) => { - const res = ParseFervidexStatic(ferStatic, { model: "LDJ:J:B:A:2020092900" }, logger); + t.test("Should parse static data from body", (t) => { + const res = ParseFervidexStatic(ferStatic, { model: "LDJ:J:B:A:2020092900" }, logger); - t.strictSame(res.iterable, [ - { - song_id: 1000, - chart: "spa", - clear_type: 4, - ex_score: 1180, - miss_count: 40, - }, - { - song_id: 1000, - chart: "spn", - clear_type: 7, - ex_score: 158, - miss_count: 0, - }, - { - song_id: 1001, - chart: "dph", - clear_type: 3, - ex_score: 15, - miss_count: 1, - }, - ]); + t.strictSame(res.iterable, [ + { + song_id: 1000, + chart: "spa", + clear_type: 4, + ex_score: 1180, + miss_count: 40, + }, + { + song_id: 1000, + chart: "spn", + clear_type: 7, + ex_score: 158, + miss_count: 0, + }, + { + song_id: 1001, + chart: "dph", + clear_type: 3, + ex_score: 15, + miss_count: 1, + }, + ]); - t.hasStrict(res, { - context: { version: "27" }, - game: "iidx", - }); + t.hasStrict(res, { + context: { version: "27" }, + game: "iidx", + }); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if no body.scores is present", (t) => { - t.throws( - () => ParseFervidexStatic({}, { model: "LDJ:J:B:A:2020092900" }, logger), - "Invalid body.scores" - ); + t.test("Should throw an error if no body.scores is present", (t) => { + t.throws( + () => ParseFervidexStatic({}, { model: "LDJ:J:B:A:2020092900" }, logger), + "Invalid body.scores" + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if songID or its value is nonsense", (t) => { - t.throws( - () => - ParseFervidexStatic( - { scores: { nonsenseKey: {} } }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid songID nonsenseKey" - ); + t.test("Should throw an error if songID or its value is nonsense", (t) => { + t.throws( + () => + ParseFervidexStatic( + { scores: { nonsenseKey: {} } }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid songID nonsenseKey" + ); - t.throws( - () => - ParseFervidexStatic( - { scores: { 1000: null } }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000" - ); + t.throws( + () => + ParseFervidexStatic( + { scores: { 1000: null } }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000" + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if songID has no score", (t) => { - t.throws( - () => - ParseFervidexStatic( - { scores: { 1000: { spn: null } } }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000" - ); + t.test("Should throw an error if songID has no score", (t) => { + t.throws( + () => + ParseFervidexStatic( + { scores: { 1000: { spn: null } } }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000" + ); - t.throws( - () => - ParseFervidexStatic( - { scores: { 1000: { spn: undefined } } }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000" - ); + t.throws( + () => + ParseFervidexStatic( + { scores: { 1000: { spn: undefined } } }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000" + ); - t.throws( - () => - ParseFervidexStatic( - { scores: { 1000: { spn: "foo" } } }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000" - ); + t.throws( + () => + ParseFervidexStatic( + { scores: { 1000: { spn: "foo" } } }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000" + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if the score->chart document is invalid", (t) => { - t.throws( - () => - ParseFervidexStatic( - { - scores: { - 1000: { spn: { ex_score: -1, miss_count: null, clear_type: 0 } }, - }, - }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000 at chart spn" - ); + t.test("Should throw an error if the score->chart document is invalid", (t) => { + t.throws( + () => + ParseFervidexStatic( + { + scores: { + 1000: { spn: { ex_score: -1, miss_count: null, clear_type: 0 } }, + }, + }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000 at chart spn" + ); - t.throws( - () => - ParseFervidexStatic( - { - scores: { - 1000: { spn: { ex_score: 1000, miss_count: "foo", clear_type: 0 } }, - }, - }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000 at chart spn" - ); + t.throws( + () => + ParseFervidexStatic( + { + scores: { + 1000: { spn: { ex_score: 1000, miss_count: "foo", clear_type: 0 } }, + }, + }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000 at chart spn" + ); - t.throws( - () => - ParseFervidexStatic( - { - scores: { - 1000: { spn: { ex_score: 1000, miss_count: null, clear_type: -1 } }, - }, - }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid score with songID 1000 at chart spn" - ); + t.throws( + () => + ParseFervidexStatic( + { + scores: { + 1000: { spn: { ex_score: 1000, miss_count: null, clear_type: -1 } }, + }, + }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid score with songID 1000 at chart spn" + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw an error if the chart name is invalid", (t) => { - t.throws( - () => - ParseFervidexStatic( - { - scores: { - 1000: { spx: { ex_score: 1000, miss_count: null, clear_type: 0 } }, - }, - }, - { model: "LDJ:J:B:A:2020092900" }, - logger - ), - "Invalid chart spx" - ); + t.test("Should throw an error if the chart name is invalid", (t) => { + t.throws( + () => + ParseFervidexStatic( + { + scores: { + 1000: { spx: { ex_score: 1000, miss_count: null, clear_type: 0 } }, + }, + }, + { model: "LDJ:J:B:A:2020092900" }, + logger + ), + "Invalid chart spx" + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts index 6460e07c0..8dc374ebe 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts @@ -10,75 +10,75 @@ import { CreateFerStaticClassHandler } from "./class-handler"; import { ParserFunctionReturns } from "../../common/types"; const PR_FervidexStatic: PrudenceSchema = { - ex_score: p.isPositiveInteger, - miss_count: p.nullable(p.or(p.isPositiveInteger, p.is(-1))), - clear_type: p.isBoundedInteger(0, 7), + ex_score: p.isPositiveInteger, + miss_count: p.nullable(p.or(p.isPositiveInteger, p.is(-1))), + clear_type: p.isBoundedInteger(0, 7), }; export function ParseFervidexStatic( - body: Record, - headers: FerHeaders, - logger: KtLogger + body: Record, + headers: FerHeaders, + logger: KtLogger ): ParserFunctionReturns { - const version = SoftwareIDToVersion(headers.model); + const version = SoftwareIDToVersion(headers.model); - const staticScores = body?.scores; + const staticScores = body?.scores; - if (!staticScores || typeof staticScores !== "object") { - throw new ScoreImportFatalError(400, `Invalid body.scores.`); - } + if (!staticScores || typeof staticScores !== "object") { + throw new ScoreImportFatalError(400, `Invalid body.scores.`); + } - const scores: FervidexStaticScore[] = []; + const scores: FervidexStaticScore[] = []; - for (const songID in staticScores) { - // @ts-expect-error pls. - const subScores = staticScores[songID]; + for (const songID in staticScores) { + // @ts-expect-error pls. + const subScores = staticScores[songID]; - const song_id = AssertStrAsPositiveInt(songID, `Invalid songID ${songID}.`); + const song_id = AssertStrAsPositiveInt(songID, `Invalid songID ${songID}.`); - if (!subScores || typeof subScores !== "object") { - throw new ScoreImportFatalError(400, `Invalid score with songID ${songID}.`); - } + if (!subScores || typeof subScores !== "object") { + throw new ScoreImportFatalError(400, `Invalid score with songID ${songID}.`); + } - for (const chart in subScores) { - const score = subScores[chart]; + for (const chart in subScores) { + const score = subScores[chart]; - if (!score || typeof subScores !== "object") { - throw new ScoreImportFatalError( - 400, - `Invalid score with songID ${songID} at chart ${chart}.` - ); - } + if (!score || typeof subScores !== "object") { + throw new ScoreImportFatalError( + 400, + `Invalid score with songID ${songID} at chart ${chart}.` + ); + } - if (!["spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"].includes(chart)) { - throw new ScoreImportFatalError(400, `Invalid chart ${chart}.`); - } + if (!["spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"].includes(chart)) { + throw new ScoreImportFatalError(400, `Invalid chart ${chart}.`); + } - const err = p(score, PR_FervidexStatic); + const err = p(score, PR_FervidexStatic); - if (err) { - throw new ScoreImportFatalError( - 400, - FormatPrError(err, `Invalid Score with songID ${songID} at chart ${chart}`) - ); - } + if (err) { + throw new ScoreImportFatalError( + 400, + FormatPrError(err, `Invalid Score with songID ${songID} at chart ${chart}`) + ); + } - scores.push({ - song_id, - // is asserted with the above check - chart: chart as FervidexStaticScore["chart"], - clear_type: score.clear_type, - ex_score: score.ex_score, - miss_count: score.miss_count, - }); - } - } + scores.push({ + song_id, + // is asserted with the above check + chart: chart as FervidexStaticScore["chart"], + clear_type: score.clear_type, + ex_score: score.ex_score, + miss_count: score.miss_count, + }); + } + } - // asserted using prudence. - return { - context: { version }, - game: "iidx", - iterable: scores, - classHandler: CreateFerStaticClassHandler(body), - }; + // asserted using prudence. + return { + context: { version }, + game: "iidx", + iterable: scores, + classHandler: CreateFerStaticClassHandler(body), + }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts index 05dd351dd..5bebae409 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts @@ -1,13 +1,13 @@ import { integer } from "tachi-common"; export interface FervidexStaticScore { - clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - ex_score: integer; - miss_count: integer | null; // bp, not cbrk - chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; - song_id: integer; + clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + ex_score: integer; + miss_count: integer | null; // bp, not cbrk + chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; + song_id: integer; } export interface FervidexStaticContext { - version: string; + version: string; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts b/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts index 252a15cf6..1ae67f526 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts @@ -5,12 +5,12 @@ import ResetDBState from "../../../../../test-utils/resets"; import { GetKTDataJSON, Testing511Song, Testing511SPA } from "../../../../../test-utils/test-data"; import { InternalFailure } from "../../../framework/common/converter-failures"; import { - ConverterIRFervidex, - SplitFervidexChartRef, - TachifyAssist, - TachifyGauge, - TachifyRandom, - TachifyRange, + ConverterIRFervidex, + SplitFervidexChartRef, + TachifyAssist, + TachifyGauge, + TachifyRandom, + TachifyRange, } from "./converter"; import { FervidexScore } from "./types"; import deepmerge from "deepmerge"; @@ -19,210 +19,210 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const logger = CreateLogCtx(__filename); t.test("#SplitFervidexChartRef", (t) => { - t.strictSame(SplitFervidexChartRef("spb"), { playtype: "SP", difficulty: "BEGINNER" }); - t.strictSame(SplitFervidexChartRef("spn"), { playtype: "SP", difficulty: "NORMAL" }); - t.strictSame(SplitFervidexChartRef("sph"), { playtype: "SP", difficulty: "HYPER" }); - t.strictSame(SplitFervidexChartRef("spa"), { playtype: "SP", difficulty: "ANOTHER" }); - t.strictSame(SplitFervidexChartRef("spl"), { playtype: "SP", difficulty: "LEGGENDARIA" }); - t.strictSame(SplitFervidexChartRef("dpn"), { playtype: "DP", difficulty: "NORMAL" }); - t.strictSame(SplitFervidexChartRef("dph"), { playtype: "DP", difficulty: "HYPER" }); - t.strictSame(SplitFervidexChartRef("dpa"), { playtype: "DP", difficulty: "ANOTHER" }); - t.strictSame(SplitFervidexChartRef("dpl"), { playtype: "DP", difficulty: "LEGGENDARIA" }); + t.strictSame(SplitFervidexChartRef("spb"), { playtype: "SP", difficulty: "BEGINNER" }); + t.strictSame(SplitFervidexChartRef("spn"), { playtype: "SP", difficulty: "NORMAL" }); + t.strictSame(SplitFervidexChartRef("sph"), { playtype: "SP", difficulty: "HYPER" }); + t.strictSame(SplitFervidexChartRef("spa"), { playtype: "SP", difficulty: "ANOTHER" }); + t.strictSame(SplitFervidexChartRef("spl"), { playtype: "SP", difficulty: "LEGGENDARIA" }); + t.strictSame(SplitFervidexChartRef("dpn"), { playtype: "DP", difficulty: "NORMAL" }); + t.strictSame(SplitFervidexChartRef("dph"), { playtype: "DP", difficulty: "HYPER" }); + t.strictSame(SplitFervidexChartRef("dpa"), { playtype: "DP", difficulty: "ANOTHER" }); + t.strictSame(SplitFervidexChartRef("dpl"), { playtype: "DP", difficulty: "LEGGENDARIA" }); - t.throws( - () => SplitFervidexChartRef("INVALID" as "spn"), - new InternalFailure(`Invalid fervidex difficulty of INVALID`) as any - ); + t.throws( + () => SplitFervidexChartRef("INVALID" as "spn"), + new InternalFailure(`Invalid fervidex difficulty of INVALID`) as any + ); - t.end(); + t.end(); }); t.test("#TachifyAssist", (t) => { - t.equal(TachifyAssist("ASCR_LEGACY"), "FULL ASSIST"); - t.equal(TachifyAssist("AUTO_SCRATCH"), "AUTO SCRATCH"); - t.equal(TachifyAssist("FULL_ASSIST"), "FULL ASSIST"); - t.equal(TachifyAssist("LEGACY_NOTE"), "LEGACY NOTE"); - t.equal(TachifyAssist(null), "NO ASSIST"); - t.equal(TachifyAssist(undefined), "NO ASSIST"); + t.equal(TachifyAssist("ASCR_LEGACY"), "FULL ASSIST"); + t.equal(TachifyAssist("AUTO_SCRATCH"), "AUTO SCRATCH"); + t.equal(TachifyAssist("FULL_ASSIST"), "FULL ASSIST"); + t.equal(TachifyAssist("LEGACY_NOTE"), "LEGACY NOTE"); + t.equal(TachifyAssist(null), "NO ASSIST"); + t.equal(TachifyAssist(undefined), "NO ASSIST"); - t.end(); + t.end(); }); t.test("#TachifyGauge", (t) => { - t.equal(TachifyGauge("ASSISTED_EASY"), "ASSISTED EASY"); - t.equal(TachifyGauge("EASY"), "EASY"); - t.equal(TachifyGauge("HARD"), "HARD"); - t.equal(TachifyGauge("EX_HARD"), "EX HARD"); - t.equal(TachifyGauge(null), "NORMAL"); - t.equal(TachifyGauge(undefined), "NORMAL"); + t.equal(TachifyGauge("ASSISTED_EASY"), "ASSISTED EASY"); + t.equal(TachifyGauge("EASY"), "EASY"); + t.equal(TachifyGauge("HARD"), "HARD"); + t.equal(TachifyGauge("EX_HARD"), "EX HARD"); + t.equal(TachifyGauge(null), "NORMAL"); + t.equal(TachifyGauge(undefined), "NORMAL"); - t.end(); + t.end(); }); t.test("#TachifyRange", (t) => { - t.equal(TachifyRange("HIDDEN_PLUS"), "HIDDEN+"); - t.equal(TachifyRange("LIFT"), "LIFT"); - t.equal(TachifyRange("LIFT_SUD_PLUS"), "LIFT SUD+"); - t.equal(TachifyRange("SUDDEN_PLUS"), "SUDDEN+"); - t.equal(TachifyRange("SUD_PLUS_HID_PLUS"), "SUD+ HID+"); - t.equal(TachifyRange(null), "NONE"); - t.equal(TachifyRange(undefined), "NONE"); + t.equal(TachifyRange("HIDDEN_PLUS"), "HIDDEN+"); + t.equal(TachifyRange("LIFT"), "LIFT"); + t.equal(TachifyRange("LIFT_SUD_PLUS"), "LIFT SUD+"); + t.equal(TachifyRange("SUDDEN_PLUS"), "SUDDEN+"); + t.equal(TachifyRange("SUD_PLUS_HID_PLUS"), "SUD+ HID+"); + t.equal(TachifyRange(null), "NONE"); + t.equal(TachifyRange(undefined), "NONE"); - t.end(); + t.end(); }); t.test("#TachifyRandom", (t) => { - t.equal(TachifyRandom("MIRROR"), "MIRROR"); - t.equal(TachifyRandom("R_RANDOM"), "R-RANDOM"); - t.equal(TachifyRandom("S_RANDOM"), "S-RANDOM"); - t.equal(TachifyRandom("RANDOM"), "RANDOM"); - t.equal(TachifyRandom(null), "NONRAN"); - t.equal(TachifyRandom(undefined), "NONRAN"); + t.equal(TachifyRandom("MIRROR"), "MIRROR"); + t.equal(TachifyRandom("R_RANDOM"), "R-RANDOM"); + t.equal(TachifyRandom("S_RANDOM"), "S-RANDOM"); + t.equal(TachifyRandom("RANDOM"), "RANDOM"); + t.equal(TachifyRandom(null), "NONRAN"); + t.equal(TachifyRandom(undefined), "NONRAN"); - t.end(); + t.end(); }); const baseFervidexScore: FervidexScore = GetKTDataJSON("./fervidex/base.json"); const baseDryScore = { - game: "iidx", - service: "Fervidex", - comment: null, - importType: "ir/fervidex", - scoreData: { - score: 68, - percent: 4.325699745547074, - grade: "F", - lamp: "FAILED", - hitData: { - pgreat: 34, - great: 0, - good: 0, - bad: 0, - poor: 6, - }, - hitMeta: { - fast: 0, - slow: 0, - maxCombo: 34, - gaugeHistory: [100, 50], - gauge: 50, - bp: 6, - comboBreak: 6, - gsm: undefined, - }, - }, - scoreMeta: { - assist: "NO ASSIST", - gauge: "HARD", - random: "RANDOM", - range: "SUDDEN+", - }, + game: "iidx", + service: "Fervidex", + comment: null, + importType: "ir/fervidex", + scoreData: { + score: 68, + percent: 4.325699745547074, + grade: "F", + lamp: "FAILED", + hitData: { + pgreat: 34, + great: 0, + good: 0, + bad: 0, + poor: 6, + }, + hitMeta: { + fast: 0, + slow: 0, + maxCombo: 34, + gaugeHistory: [100, 50], + gauge: 50, + bp: 6, + comboBreak: 6, + gsm: undefined, + }, + }, + scoreMeta: { + assist: "NO ASSIST", + gauge: "HARD", + random: "RANDOM", + range: "SUDDEN+", + }, }; t.test("#ConverterIRFervidex", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should convert a valid fervidex score into a dry score.", async (t) => { - const res = await ConverterIRFervidex( - baseFervidexScore, - { version: "27" }, - "ir/fervidex", - logger - ); + t.test("Should convert a valid fervidex score into a dry score.", async (t) => { + const res = await ConverterIRFervidex( + baseFervidexScore, + { version: "27" }, + "ir/fervidex", + logger + ); - t.hasStrict( - res, - { - song: Testing511Song, - chart: Testing511SPA, - dryScore: baseDryScore, - }, // broken - "Should return a dry score." - ); + t.hasStrict( + res, + { + song: Testing511Song, + chart: Testing511SPA, + dryScore: baseDryScore, + }, // broken + "Should return a dry score." + ); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores on unknown charts", (t) => { - t.rejects( - ConverterIRFervidex( - deepmerge(baseFervidexScore, { chart: "spl" }), - { version: "27" }, - "ir/fervidex", - logger - ), - { message: /could not find chart/giu } as any - ); + t.test("Should reject scores on unknown charts", (t) => { + t.rejects( + ConverterIRFervidex( + deepmerge(baseFervidexScore, { chart: "spl" }), + { version: "27" }, + "ir/fervidex", + logger + ), + { message: /could not find chart/giu } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw internal failure on chart-song desync", async (t) => { - await db.songs.iidx.remove({}); // this forces desync + t.test("Should throw internal failure on chart-song desync", async (t) => { + await db.songs.iidx.remove({}); // this forces desync - t.rejects( - ConverterIRFervidex(baseFervidexScore, { version: "27" }, "ir/fervidex", logger), - { message: /Song 1 \(iidx\) has no parent song/giu } as any - ); + t.rejects( + ConverterIRFervidex(baseFervidexScore, { version: "27" }, "ir/fervidex", logger), + { message: /Song 1 \(iidx\) has no parent song/giu } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw invalid score on percent > 100.", (t) => { - t.rejects( - ConverterIRFervidex( - // @ts-expect-error eternally broken deepmerge - deepmerge(baseFervidexScore, { ex_score: 9999 }), - { version: "27" }, - "ir/fervidex", - logger - ), - { message: /Invalid percent/giu } as any - ); + t.test("Should throw invalid score on percent > 100.", (t) => { + t.rejects( + ConverterIRFervidex( + // @ts-expect-error eternally broken deepmerge + deepmerge(baseFervidexScore, { ex_score: 9999 }), + { version: "27" }, + "ir/fervidex", + logger + ), + { message: /Invalid percent/giu } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw invalid score on gauge > 100.", (t) => { - t.rejects( - ConverterIRFervidex( - // @ts-expect-error eternally broken deepmerge - deepmerge(baseFervidexScore, { gauge: [150] }), - { version: "27" }, - "ir/fervidex", - logger - ), - { message: /Invalid value of gauge 150/giu } as any - ); + t.test("Should throw invalid score on gauge > 100.", (t) => { + t.rejects( + ConverterIRFervidex( + // @ts-expect-error eternally broken deepmerge + deepmerge(baseFervidexScore, { gauge: [150] }), + { version: "27" }, + "ir/fervidex", + logger + ), + { message: /Invalid value of gauge 150/giu } as any + ); - t.end(); - }); + t.end(); + }); - t.test("Should convert undeflow gauge to null.", async (t) => { - const res = await ConverterIRFervidex( - deepmerge(baseFervidexScore, { gauge: [10, 5, 249, 248] }), - { version: "27" }, - "ir/fervidex", - logger - ); + t.test("Should convert undeflow gauge to null.", async (t) => { + const res = await ConverterIRFervidex( + deepmerge(baseFervidexScore, { gauge: [10, 5, 249, 248] }), + { version: "27" }, + "ir/fervidex", + logger + ); - t.hasStrict( - res, - { - song: Testing511Song, - chart: Testing511SPA, - dryScore: deepmerge(baseDryScore, { - scoreData: { hitMeta: { gauge: null, gaugeHistory: [10, 5, null, null] } }, - }), - } as any, // broken - "Should return a dry score." - ); + t.hasStrict( + res, + { + song: Testing511Song, + chart: Testing511SPA, + dryScore: deepmerge(baseDryScore, { + scoreData: { hitMeta: { gauge: null, gaugeHistory: [10, 5, null, null] } }, + }), + } as any, // broken + "Should return a dry score." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts index e465d2b6c..c410505df 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts @@ -1,220 +1,220 @@ import { FindSongOnID } from "../../../../../utils/queries/songs"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { FervidexContext, FervidexScore } from "./types"; import { Lamps, Difficulties, Playtypes } from "tachi-common"; import { - FindIIDXChartOnInGameIDVersion, - FindIIDXChartWith2DXtraHash, + FindIIDXChartOnInGameIDVersion, + FindIIDXChartWith2DXtraHash, } from "../../../../../utils/queries/charts"; import { ConverterFunction } from "../../common/types"; import { DryScore } from "../../../framework/common/types"; export const FERVIDEX_LAMP_LOOKUP = { - 0: "NO PLAY", - 1: "FAILED", - 2: "ASSIST CLEAR", - 3: "EASY CLEAR", - 4: "CLEAR", - 5: "HARD CLEAR", - 6: "EX HARD CLEAR", - 7: "FULL COMBO", + 0: "NO PLAY", + 1: "FAILED", + 2: "ASSIST CLEAR", + 3: "EASY CLEAR", + 4: "CLEAR", + 5: "HARD CLEAR", + 6: "EX HARD CLEAR", + 7: "FULL COMBO", }; export function TachifyAssist( - assist: FervidexScore["option"]["assist"] + assist: FervidexScore["option"]["assist"] ): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["assist"] { - switch (assist) { - case "ASCR_LEGACY": - return "FULL ASSIST"; - case "AUTO_SCRATCH": - return "AUTO SCRATCH"; - case "FULL_ASSIST": - return "FULL ASSIST"; - case "LEGACY_NOTE": - return "LEGACY NOTE"; - case null: - case undefined: - return "NO ASSIST"; - } + switch (assist) { + case "ASCR_LEGACY": + return "FULL ASSIST"; + case "AUTO_SCRATCH": + return "AUTO SCRATCH"; + case "FULL_ASSIST": + return "FULL ASSIST"; + case "LEGACY_NOTE": + return "LEGACY NOTE"; + case null: + case undefined: + return "NO ASSIST"; + } } export function TachifyGauge( - gauge: FervidexScore["option"]["gauge"] + gauge: FervidexScore["option"]["gauge"] ): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["gauge"] { - switch (gauge) { - case "ASSISTED_EASY": - return "ASSISTED EASY"; - case "EASY": - return "EASY"; - case "EX_HARD": - return "EX HARD"; - case "HARD": - return "HARD"; - case null: - case undefined: - return "NORMAL"; - } + switch (gauge) { + case "ASSISTED_EASY": + return "ASSISTED EASY"; + case "EASY": + return "EASY"; + case "EX_HARD": + return "EX HARD"; + case "HARD": + return "HARD"; + case null: + case undefined: + return "NORMAL"; + } } export function TachifyRange( - gauge: FervidexScore["option"]["range"] + gauge: FervidexScore["option"]["range"] ): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["range"] { - switch (gauge) { - case "HIDDEN_PLUS": - return "HIDDEN+"; - case "LIFT": - return "LIFT"; - case "LIFT_SUD_PLUS": - return "LIFT SUD+"; - case "SUDDEN_PLUS": - return "SUDDEN+"; - case "SUD_PLUS_HID_PLUS": - return "SUD+ HID+"; - case null: - case undefined: - return "NONE"; - } + switch (gauge) { + case "HIDDEN_PLUS": + return "HIDDEN+"; + case "LIFT": + return "LIFT"; + case "LIFT_SUD_PLUS": + return "LIFT SUD+"; + case "SUDDEN_PLUS": + return "SUDDEN+"; + case "SUD_PLUS_HID_PLUS": + return "SUD+ HID+"; + case null: + case undefined: + return "NONE"; + } } export function TachifyRandom( - gauge: FervidexScore["option"]["style"] + gauge: FervidexScore["option"]["style"] ): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["random"] { - switch (gauge) { - case "RANDOM": - return "RANDOM"; - case "S_RANDOM": - return "S-RANDOM"; - case "R_RANDOM": - return "R-RANDOM"; - case "MIRROR": - return "MIRROR"; - case null: - case undefined: - return "NONRAN"; - } + switch (gauge) { + case "RANDOM": + return "RANDOM"; + case "S_RANDOM": + return "S-RANDOM"; + case "R_RANDOM": + return "R-RANDOM"; + case "MIRROR": + return "MIRROR"; + case null: + case undefined: + return "NONRAN"; + } } export function SplitFervidexChartRef(ferDif: FervidexScore["chart"]) { - let playtype: Playtypes["iidx"]; - if (ferDif.startsWith("sp")) { - playtype = "SP"; - } else { - playtype = "DP"; - } + let playtype: Playtypes["iidx"]; + if (ferDif.startsWith("sp")) { + playtype = "SP"; + } else { + playtype = "DP"; + } - let difficulty: Difficulties["iidx:SP" | "iidx:DP"]; + let difficulty: Difficulties["iidx:SP" | "iidx:DP"]; - switch (ferDif[ferDif.length - 1]) { - case "b": - difficulty = "BEGINNER"; - break; - case "n": - difficulty = "NORMAL"; - break; - case "h": - difficulty = "HYPER"; - break; - case "a": - difficulty = "ANOTHER"; - break; - case "l": - difficulty = "LEGGENDARIA"; - break; - default: - throw new InternalFailure(`Invalid fervidex difficulty of ${ferDif}`); - } + switch (ferDif[ferDif.length - 1]) { + case "b": + difficulty = "BEGINNER"; + break; + case "n": + difficulty = "NORMAL"; + break; + case "h": + difficulty = "HYPER"; + break; + case "a": + difficulty = "ANOTHER"; + break; + case "l": + difficulty = "LEGGENDARIA"; + break; + default: + throw new InternalFailure(`Invalid fervidex difficulty of ${ferDif}`); + } - return { playtype, difficulty }; + return { playtype, difficulty }; } export const ConverterIRFervidex: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - const { difficulty, playtype } = SplitFervidexChartRef(data.chart); + const { difficulty, playtype } = SplitFervidexChartRef(data.chart); - let chart; - if (data.custom) { - chart = await FindIIDXChartWith2DXtraHash(data.chart_sha256); - } else { - chart = await FindIIDXChartOnInGameIDVersion( - data.entry_id, - playtype, - difficulty, - context.version - ); - } + let chart; + if (data.custom) { + chart = await FindIIDXChartWith2DXtraHash(data.chart_sha256); + } else { + chart = await FindIIDXChartOnInGameIDVersion( + data.entry_id, + playtype, + difficulty, + context.version + ); + } - if (!chart) { - throw new KTDataNotFoundFailure( - `Could not find chart with songID ${data.entry_id} (${playtype} ${difficulty} [${context.version}])`, - importType, - data, - context - ); - } + if (!chart) { + throw new KTDataNotFoundFailure( + `Could not find chart with songID ${data.entry_id} (${playtype} ${difficulty} [${context.version}])`, + importType, + data, + context + ); + } - const song = await FindSongOnID("iidx", chart.songID); + const song = await FindSongOnID("iidx", chart.songID); - if (!song) { - logger.severe(`Song ${chart.songID} (iidx) has no parent song?`); - throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`); - } + if (!song) { + logger.severe(`Song ${chart.songID} (iidx) has no parent song?`); + throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`); + } - const gaugeHistory = data.gauge.map((e) => (e > 200 ? null : e)); + const gaugeHistory = data.gauge.map((e) => (e > 200 ? null : e)); - const gauge = gaugeHistory[gaugeHistory.length - 1]; + const gauge = gaugeHistory[gaugeHistory.length - 1]; - if (gauge && gauge > 100) { - throw new InvalidScoreFailure(`Invalid value of gauge ${gauge}.`); - } + if (gauge && gauge > 100) { + throw new InvalidScoreFailure(`Invalid value of gauge ${gauge}.`); + } - const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart); + const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { - game: "iidx", - service: "Fervidex", - comment: null, - importType: "ir/fervidex", - timeAchieved: Date.now(), - scoreData: { - score: data.ex_score, - percent, - grade, - lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], - hitData: { - pgreat: data.pgreat, - great: data.great, - good: data.good, - bad: data.bad, - poor: data.poor, - }, - hitMeta: { - fast: data.fast, - slow: data.slow, - maxCombo: data.max_combo, - gaugeHistory, - scoreHistory: data.ghost, - gauge, - bp: data.bad + data.poor, - comboBreak: data.combo_break, - gsm: data["2dx-gsm"], - }, - }, - scoreMeta: { - assist: TachifyAssist(data.option.assist), - gauge: TachifyGauge(data.option.gauge), - random: TachifyRandom(data.option.style), - range: TachifyRange(data.option.range), - }, - }; + const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + game: "iidx", + service: "Fervidex", + comment: null, + importType: "ir/fervidex", + timeAchieved: Date.now(), + scoreData: { + score: data.ex_score, + percent, + grade, + lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], + hitData: { + pgreat: data.pgreat, + great: data.great, + good: data.good, + bad: data.bad, + poor: data.poor, + }, + hitMeta: { + fast: data.fast, + slow: data.slow, + maxCombo: data.max_combo, + gaugeHistory, + scoreHistory: data.ghost, + gauge, + bp: data.bad + data.poor, + comboBreak: data.combo_break, + gsm: data["2dx-gsm"], + }, + }, + scoreMeta: { + assist: TachifyAssist(data.option.assist), + gauge: TachifyGauge(data.option.gauge), + random: TachifyRandom(data.option.style), + range: TachifyRange(data.option.range), + }, + }; - return { song, chart, dryScore }; + return { song, chart, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts index c046a734b..2d2920c8a 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts @@ -6,60 +6,60 @@ import { FervidexContext, FervidexScore } from "./types"; import { ConverterIRFervidex } from "./converter"; import { ParseEA3SoftID } from "../../../../../utils/ea3id"; import { - EXT_HEROIC_VERSE, - MODEL_IIDX, - MODEL_INFINITAS_2, - REV_2DXTRA, - REV_OMNIMIX, + EXT_HEROIC_VERSE, + MODEL_IIDX, + MODEL_INFINITAS_2, + REV_2DXTRA, + REV_OMNIMIX, } from "../../../../constants/ea3id"; import { ParserFunctionReturns } from "../../common/types"; const PR_Fervidex: PrudenceSchema = { - chart: p.isIn("spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"), - entry_id: p.isPositiveInteger, - chart_sha256: "string", - custom: "boolean", + chart: p.isIn("spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"), + entry_id: p.isPositiveInteger, + chart_sha256: "string", + custom: "boolean", - pgreat: p.isPositiveInteger, - great: p.isPositiveInteger, - good: p.isPositiveInteger, - bad: p.isPositiveInteger, - poor: p.isPositiveInteger, + pgreat: p.isPositiveInteger, + great: p.isPositiveInteger, + good: p.isPositiveInteger, + bad: p.isPositiveInteger, + poor: p.isPositiveInteger, - slow: p.isPositiveInteger, - fast: p.isPositiveInteger, - max_combo: p.isPositiveInteger, - combo_break: p.isPositiveInteger, + slow: p.isPositiveInteger, + fast: p.isPositiveInteger, + max_combo: p.isPositiveInteger, + combo_break: p.isPositiveInteger, - ex_score: p.isPositiveInteger, - clear_type: p.isBoundedInteger(0, 7), + ex_score: p.isPositiveInteger, + clear_type: p.isBoundedInteger(0, 7), - gauge: [p.isBoundedInteger(0, 255)], - ghost: [p.isBoundedInteger(0, 100)], + gauge: [p.isBoundedInteger(0, 255)], + ghost: [p.isBoundedInteger(0, 100)], - dead: p.optional({ - measure: optNull(p.isPositiveInteger), - note: optNull(p.isPositiveInteger), - } as unknown as ValidSchemaValue), + dead: p.optional({ + measure: optNull(p.isPositiveInteger), + note: optNull(p.isPositiveInteger), + } as unknown as ValidSchemaValue), - option: { - gauge: optNull(p.isIn("ASSISTED_EASY", "EASY", "HARD", "EX_HARD")), - range: optNull( - p.isIn("SUDDEN_PLUS", "HIDDEN_PLUS", "SUD_PLUS_HID_PLUS", "LIFT", "LIFT_SUD_PLUS") - ), - style: optNull(p.isIn("RANDOM", "R_RANDOM", "S_RANDOM", "MIRROR")), - assist: optNull(p.isIn("AUTO_SCRATCH", "LEGACY_NOTE", "ASCR_LEGACY", "FULL_ASSIST")), - }, + option: { + gauge: optNull(p.isIn("ASSISTED_EASY", "EASY", "HARD", "EX_HARD")), + range: optNull( + p.isIn("SUDDEN_PLUS", "HIDDEN_PLUS", "SUD_PLUS_HID_PLUS", "LIFT", "LIFT_SUD_PLUS") + ), + style: optNull(p.isIn("RANDOM", "R_RANDOM", "S_RANDOM", "MIRROR")), + assist: optNull(p.isIn("AUTO_SCRATCH", "LEGACY_NOTE", "ASCR_LEGACY", "FULL_ASSIST")), + }, - // we dont use it and we dont care. - pacemaker: p.any, + // we dont use it and we dont care. + pacemaker: p.any, - "2dx-gsm": p.optional({ - EASY: [p.isBoundedInteger(0, 255)], - NORMAL: [p.isBoundedInteger(0, 255)], - HARD: [p.isBoundedInteger(0, 255)], - EX_HARD: [p.isBoundedInteger(0, 255)], - }), + "2dx-gsm": p.optional({ + EASY: [p.isBoundedInteger(0, 255)], + NORMAL: [p.isBoundedInteger(0, 255)], + HARD: [p.isBoundedInteger(0, 255)], + EX_HARD: [p.isBoundedInteger(0, 255)], + }), }; /** @@ -67,54 +67,54 @@ const PR_Fervidex: PrudenceSchema = { * I don't really understand the software model format, so this is lazy. */ export function SoftwareIDToVersion(model: string) { - try { - const data = ParseEA3SoftID(model); + try { + const data = ParseEA3SoftID(model); - if (data.model === MODEL_INFINITAS_2) { - return "inf"; - } else if (data.model === MODEL_IIDX) { - // only HV. everything else is disabled deliberately. - if (data.ext === EXT_HEROIC_VERSE) { - // @hack This is a hack workaround. For custom charts such as kichiku and kiraku - // they use a sha256 lookup which skips version lookups so we do not need a version - // for 2dxtra specifically. - if (data.rev === REV_OMNIMIX || data.rev === REV_2DXTRA) { - return "27-omni"; - } + if (data.model === MODEL_INFINITAS_2) { + return "inf"; + } else if (data.model === MODEL_IIDX) { + // only HV. everything else is disabled deliberately. + if (data.ext === EXT_HEROIC_VERSE) { + // @hack This is a hack workaround. For custom charts such as kichiku and kiraku + // they use a sha256 lookup which skips version lookups so we do not need a version + // for 2dxtra specifically. + if (data.rev === REV_OMNIMIX || data.rev === REV_2DXTRA) { + return "27-omni"; + } - return "27"; - } - } + return "27"; + } + } - throw new ScoreImportFatalError(400, `Unsupported Software Model ${model}.`); - } catch (err) { - throw new ScoreImportFatalError(400, `Unsupported Software Model ${model}`); - } + throw new ScoreImportFatalError(400, `Unsupported Software Model ${model}.`); + } catch (err) { + throw new ScoreImportFatalError(400, `Unsupported Software Model ${model}`); + } } export interface FerHeaders { - model: string; + model: string; } export function ParseFervidexSingle( - body: Record, - headers: FerHeaders, - logger: KtLogger + body: Record, + headers: FerHeaders, + logger: KtLogger ): ParserFunctionReturns { - const version = SoftwareIDToVersion(headers.model); + const version = SoftwareIDToVersion(headers.model); - // more mods may be added in the future, so lets ignore excess keys. - const err = p(body, PR_Fervidex, undefined, { allowExcessKeys: true }); + // more mods may be added in the future, so lets ignore excess keys. + const err = p(body, PR_Fervidex, undefined, { allowExcessKeys: true }); - if (err) { - throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Fervidex Request?")); - } + if (err) { + throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Fervidex Request?")); + } - // asserted using prudence. - return { - context: { version }, - game: "iidx", - iterable: [body] as unknown as FervidexScore[], - classHandler: null, - }; + // asserted using prudence. + return { + context: { version }, + game: "iidx", + iterable: [body] as unknown as FervidexScore[], + classHandler: null, + }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex/types.ts b/server/src/lib/score-import/import-types/ir/fervidex/types.ts index 33df39923..6149b309f 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/types.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/types.ts @@ -1,62 +1,62 @@ import { integer } from "tachi-common"; export interface FervidexScore { - chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; + chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; - entry_id: integer; - chart_sha256: string; - // whether or whether not this chart is from 2dxtra - custom: boolean; + entry_id: integer; + chart_sha256: string; + // whether or whether not this chart is from 2dxtra + custom: boolean; - pgreat: integer; - great: integer; - good: integer; - bad: integer; - poor: integer; + pgreat: integer; + great: integer; + good: integer; + bad: integer; + poor: integer; - slow: integer; - fast: integer; - max_combo: integer; - combo_break: integer; + slow: integer; + fast: integer; + max_combo: integer; + combo_break: integer; - ex_score: integer; - clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + ex_score: integer; + clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - gauge: [integer]; - ghost: [integer]; + gauge: [integer]; + ghost: [integer]; - dead?: { - measure?: integer | null; - note?: integer | null; - }; + dead?: { + measure?: integer | null; + note?: integer | null; + }; - option: { - gauge?: "ASSISTED_EASY" | "EASY" | "HARD" | "EX_HARD" | null; - range?: - | "SUDDEN_PLUS" - | "HIDDEN_PLUS" - | "SUD_PLUS_HID_PLUS" - | "LIFT" - | "LIFT_SUD_PLUS" - | null; - style?: "RANDOM" | "R_RANDOM" | "S_RANDOM" | "MIRROR" | null; - assist?: "AUTO_SCRATCH" | "LEGACY_NOTE" | "ASCR_LEGACY" | "FULL_ASSIST" | null; - }; + option: { + gauge?: "ASSISTED_EASY" | "EASY" | "HARD" | "EX_HARD" | null; + range?: + | "SUDDEN_PLUS" + | "HIDDEN_PLUS" + | "SUD_PLUS_HID_PLUS" + | "LIFT" + | "LIFT_SUD_PLUS" + | null; + style?: "RANDOM" | "R_RANDOM" | "S_RANDOM" | "MIRROR" | null; + assist?: "AUTO_SCRATCH" | "LEGACY_NOTE" | "ASCR_LEGACY" | "FULL_ASSIST" | null; + }; - pacemaker: { - name?: string | null; - score?: integer | null; - type?: string | null; // too lazy to type this and we dont use it. - }; + pacemaker: { + name?: string | null; + score?: integer | null; + type?: string | null; // too lazy to type this and we dont use it. + }; - "2dx-gsm"?: { - EASY: (number | null)[]; - NORMAL: (number | null)[]; - HARD: (number | null)[]; - EX_HARD: (number | null)[]; - } | null; + "2dx-gsm"?: { + EASY: (number | null)[]; + NORMAL: (number | null)[]; + HARD: (number | null)[]; + EX_HARD: (number | null)[]; + } | null; } export interface FervidexContext { - version: string; + version: string; } diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts index 9cc55c8f5..1234f7f2a 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts @@ -11,116 +11,116 @@ import { USCClientScore } from "../../../../../server/router/ir/usc/types"; const logger = CreateLogCtx(__filename); t.test("#DeriveLamp", (t) => { - t.equal(DeriveLamp(uscScore, logger), "EXCESSIVE CLEAR"); + t.equal(DeriveLamp(uscScore, logger), "EXCESSIVE CLEAR"); - t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 50 }), logger), "FAILED"); + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 50 }), logger), "FAILED"); - t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 70 }), logger), "CLEAR"); + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 70 }), logger), "CLEAR"); - t.equal( - DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0.1 }), logger), - "EXCESSIVE CLEAR" - ); + t.equal( + DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0.1 }), logger), + "EXCESSIVE CLEAR" + ); - t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0 }), logger), "FAILED"); + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0 }), logger), "FAILED"); - t.equal( - DeriveLamp( - d(uscScore, { score: 10_000_000, options: { gaugeType: 1 }, gauge: 0.1 }), - logger - ), - "PERFECT ULTIMATE CHAIN" - ); + t.equal( + DeriveLamp( + d(uscScore, { score: 10_000_000, options: { gaugeType: 1 }, gauge: 0.1 }), + logger + ), + "PERFECT ULTIMATE CHAIN" + ); - // error: 0 should not take priority over score 10million - t.equal( - DeriveLamp( - d(uscScore, { - score: 10_000_000, - options: { gaugeType: 1 }, - gauge: 0.1, - error: 0, - }), - logger - ), - "PERFECT ULTIMATE CHAIN" - ); + // error: 0 should not take priority over score 10million + t.equal( + DeriveLamp( + d(uscScore, { + score: 10_000_000, + options: { gaugeType: 1 }, + gauge: 0.1, + error: 0, + }), + logger + ), + "PERFECT ULTIMATE CHAIN" + ); - t.equal( - DeriveLamp( - d(uscScore, { - score: 9_000_000, - options: { gaugeType: 0 }, - gauge: 15, - error: 0, - }), - logger - ), - "ULTIMATE CHAIN" - ); + t.equal( + DeriveLamp( + d(uscScore, { + score: 9_000_000, + options: { gaugeType: 0 }, + gauge: 15, + error: 0, + }), + logger + ), + "ULTIMATE CHAIN" + ); - t.throws(() => DeriveLamp(d(uscScore, { options: { gaugeType: 2 } }), logger)); + t.throws(() => DeriveLamp(d(uscScore, { options: { gaugeType: 2 } }), logger)); - t.end(); + t.end(); }); t.test("#DeriveNoteMod", (t) => { - t.equal(DeriveNoteMod(uscScore), "MIRROR"); + t.equal(DeriveNoteMod(uscScore), "MIRROR"); - t.equal(DeriveNoteMod(d(uscScore, { options: { random: true } })), "MIR-RAN"); + t.equal(DeriveNoteMod(d(uscScore, { options: { random: true } })), "MIR-RAN"); - t.equal(DeriveNoteMod(d(uscScore, { options: { random: false, mirror: false } })), "NORMAL"); - t.equal(DeriveNoteMod(d(uscScore, { options: { random: true, mirror: false } })), "RANDOM"); + t.equal(DeriveNoteMod(d(uscScore, { options: { random: false, mirror: false } })), "NORMAL"); + t.equal(DeriveNoteMod(d(uscScore, { options: { random: true, mirror: false } })), "RANDOM"); - t.end(); + t.end(); }); const dm = (p: Partial | any) => - ConverterIRUSC(d(uscScore, p), { chart: uscChart }, "ir/usc", logger); + ConverterIRUSC(d(uscScore, p), { chart: uscChart }, "ir/usc", logger); t.test("#ConverterIRUSC", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should convert a score", async (t) => { - const res = await dm({}); + t.test("Should convert a score", async (t) => { + const res = await dm({}); - t.hasStrict(res, { - song: { - id: 1, - }, - chart: { - chartID: uscChart.chartID, - }, - dryScore: {}, - }); + t.hasStrict(res, { + song: { + id: 1, + }, + chart: { + chartID: uscChart.chartID, + }, + dryScore: {}, + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores with invalid hit windows", (t) => { - t.rejects(dm({ windows: { perfect: 0 } })); - t.rejects(dm({ windows: { good: 0 } })); - t.rejects(dm({ windows: { hold: 0 } })); - t.rejects(dm({ windows: { miss: 0 } })); - t.rejects(dm({ windows: { slam: 0 } })); + t.test("Should reject scores with invalid hit windows", (t) => { + t.rejects(dm({ windows: { perfect: 0 } })); + t.rejects(dm({ windows: { good: 0 } })); + t.rejects(dm({ windows: { hold: 0 } })); + t.rejects(dm({ windows: { miss: 0 } })); + t.rejects(dm({ windows: { slam: 0 } })); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores with invalid autoflags", (t) => { - t.rejects(dm({ options: { autoFlags: 1 } })); + t.test("Should reject scores with invalid autoflags", (t) => { + t.rejects(dm({ options: { autoFlags: 1 } })); - t.end(); - }); + t.end(); + }); - t.test("Should throw an InternalFailure on song-chart desync", async (t) => { - await db.songs.usc.remove({}); - t.rejects(dm({})); + t.test("Should throw an InternalFailure on song-chart desync", async (t) => { + await db.songs.usc.remove({}); + t.rejects(dm({})); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.ts b/server/src/lib/score-import/import-types/ir/usc/converter.ts index b037580f0..72c4fc375 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.ts @@ -8,103 +8,103 @@ import { Lamps } from "tachi-common"; import { ConverterFunction } from "../../common/types"; import { DryScore } from "../../../framework/common/types"; import { - USC_DEFAULT_HOLD, - USC_DEFAULT_MISS, - USC_DEFAULT_NEAR, - USC_DEFAULT_PERFECT, - USC_DEFAULT_SLAM, + USC_DEFAULT_HOLD, + USC_DEFAULT_MISS, + USC_DEFAULT_NEAR, + USC_DEFAULT_PERFECT, + USC_DEFAULT_SLAM, } from "../../../../constants/usc-ir"; /** * Interprets the "note mod" used based on the USC score. */ export function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN" { - if (data.options.mirror && data.options.random) { - return "MIR-RAN"; - } else if (data.options.mirror) { - return "MIRROR"; - } else if (data.options.random) { - return "RANDOM"; - } + if (data.options.mirror && data.options.random) { + return "MIR-RAN"; + } else if (data.options.mirror) { + return "MIRROR"; + } else if (data.options.random) { + return "RANDOM"; + } - return "NORMAL"; + return "NORMAL"; } /** * Determines the lamp of a USC score. */ export function DeriveLamp(scoreDoc: USCClientScore, logger: KtLogger): Lamps["usc:Single"] { - if (scoreDoc.score === 10_000_000) { - return "PERFECT ULTIMATE CHAIN"; - } else if (scoreDoc.error === 0) { - return "ULTIMATE CHAIN"; - } else if (scoreDoc.options.gaugeType === 0) { - return scoreDoc.gauge >= 70 ? "CLEAR" : "FAILED"; - } else if (scoreDoc.options.gaugeType === 1) { - return scoreDoc.gauge > 0 ? "EXCESSIVE CLEAR" : "FAILED"; - } + if (scoreDoc.score === 10_000_000) { + return "PERFECT ULTIMATE CHAIN"; + } else if (scoreDoc.error === 0) { + return "ULTIMATE CHAIN"; + } else if (scoreDoc.options.gaugeType === 0) { + return scoreDoc.gauge >= 70 ? "CLEAR" : "FAILED"; + } else if (scoreDoc.options.gaugeType === 1) { + return scoreDoc.gauge > 0 ? "EXCESSIVE CLEAR" : "FAILED"; + } - logger.error(`Could not derive Lamp from Score Document`, { scoreDoc }); - throw new InternalFailure(`Could not derive Lamp from Score Document`); + logger.error(`Could not derive Lamp from Score Document`, { scoreDoc }); + throw new InternalFailure(`Could not derive Lamp from Score Document`); } export const ConverterIRUSC: ConverterFunction = async ( - data, - context, - importType, - logger + data, + context, + importType, + logger ) => { - if ( - data.windows.perfect !== USC_DEFAULT_PERFECT || - data.windows.good !== USC_DEFAULT_NEAR || - data.windows.hold !== USC_DEFAULT_HOLD || - data.windows.miss !== USC_DEFAULT_MISS || - data.windows.slam !== USC_DEFAULT_SLAM - ) { - logger.verbose(`Ignored score because hitWindows were modified.`); - throw new InvalidScoreFailure(`HitWindows have been modified - Score is invalid.`); - } + if ( + data.windows.perfect !== USC_DEFAULT_PERFECT || + data.windows.good !== USC_DEFAULT_NEAR || + data.windows.hold !== USC_DEFAULT_HOLD || + data.windows.miss !== USC_DEFAULT_MISS || + data.windows.slam !== USC_DEFAULT_SLAM + ) { + logger.verbose(`Ignored score because hitWindows were modified.`); + throw new InvalidScoreFailure(`HitWindows have been modified - Score is invalid.`); + } - // if any auto-like option is enabled, reject score. - if (data.options.autoFlags !== 0) { - logger.verbose(`Ignored score because autoplay was enabled.`); - throw new InvalidScoreFailure(`Autoplay was enabled - Score is invalid.`); - } + // if any auto-like option is enabled, reject score. + if (data.options.autoFlags !== 0) { + logger.verbose(`Ignored score because autoplay was enabled.`); + throw new InvalidScoreFailure(`Autoplay was enabled - Score is invalid.`); + } - const song = await FindSongOnID("usc", context.chart.songID); + const song = await FindSongOnID("usc", context.chart.songID); - if (!song) { - logger.severe(`Song-Chart desync on USCIR ${context.chart.songID}.`); - throw new InternalFailure(`Song-Chart desync on USCIR ${context.chart.songID}.`); - } + if (!song) { + logger.severe(`Song-Chart desync on USCIR ${context.chart.songID}.`); + throw new InternalFailure(`Song-Chart desync on USCIR ${context.chart.songID}.`); + } - const { grade, percent } = GenericGetGradeAndPercent("usc", data.score, context.chart); + const { grade, percent } = GenericGetGradeAndPercent("usc", data.score, context.chart); - const dryScore: DryScore<"usc:Single"> = { - comment: null, - game: "usc", - importType, - timeAchieved: Date.now(), - service: "USC-IR", - scoreData: { - grade, - percent, - score: data.score, - lamp: DeriveLamp(data, logger), - hitData: { - critical: data.crit, - near: data.near, - miss: data.error, - }, - hitMeta: { - gauge: data.gauge, - }, - }, - scoreMeta: { - gaugeMod: data.options.gaugeOpt === 0 ? "NORMAL" : "HARD", - noteMod: DeriveNoteMod(data), - }, - }; + const dryScore: DryScore<"usc:Single"> = { + comment: null, + game: "usc", + importType, + timeAchieved: Date.now(), + service: "USC-IR", + scoreData: { + grade, + percent, + score: data.score, + lamp: DeriveLamp(data, logger), + hitData: { + critical: data.crit, + near: data.near, + miss: data.error, + }, + hitMeta: { + gauge: data.gauge, + }, + }, + scoreMeta: { + gaugeMod: data.options.gaugeOpt === 0 ? "NORMAL" : "HARD", + noteMod: DeriveNoteMod(data), + }, + }; - return { chart: context.chart, song, dryScore }; + return { chart: context.chart, song, dryScore }; }; diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts index c44ca3543..065f1974a 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts @@ -7,34 +7,34 @@ import { ParseIRUSC } from "./parser"; const logger = CreateLogCtx(__filename); t.test("#ParseIRUSC", (t) => { - t.test("Should validate and convert a score into an iterable", (t) => { - const res = ParseIRUSC( - { score: uscScore } as unknown as Record, - uscChart, - logger - ); + t.test("Should validate and convert a score into an iterable", (t) => { + const res = ParseIRUSC( + { score: uscScore } as unknown as Record, + uscChart, + logger + ); - t.hasStrict(res, { - game: "usc", - context: { - chart: uscChart, - }, - iterable: [uscScore], - }); + t.hasStrict(res, { + game: "usc", + context: { + chart: uscChart, + }, + iterable: [uscScore], + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject empty bodies", (t) => { - t.throws(() => ParseIRUSC({}, uscChart, logger), { - statusCode: 400, - message: /invalid usc score/iu, - } as any); + t.test("Should reject empty bodies", (t) => { + t.throws(() => ParseIRUSC({}, uscChart, logger), { + statusCode: 400, + message: /invalid usc score/iu, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.ts b/server/src/lib/score-import/import-types/ir/usc/parser.ts index 7af8e439a..f45180a5c 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.ts @@ -9,42 +9,42 @@ import { ParserFunctionReturns } from "../../common/types"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; const PR_USCIRScore: PrudenceSchema = { - score: p.isBoundedInteger(0, 10_000_000), - gauge: p.isBetween(0, 100), - timestamp: p.isPositiveInteger, - crit: p.isPositiveInteger, - near: p.isPositiveInteger, - error: p.isPositiveInteger, - options: { - gaugeType: p.isIn(0, 1), - mirror: "boolean", - random: "boolean", - autoFlags: p.isInteger, - }, + score: p.isBoundedInteger(0, 10_000_000), + gauge: p.isBetween(0, 100), + timestamp: p.isPositiveInteger, + crit: p.isPositiveInteger, + near: p.isPositiveInteger, + error: p.isPositiveInteger, + options: { + gaugeType: p.isIn(0, 1), + mirror: "boolean", + random: "boolean", + autoFlags: p.isInteger, + }, }; export function ParseIRUSC( - body: Record, - chart: ChartDocument<"usc:Single">, - logger: KtLogger + body: Record, + chart: ChartDocument<"usc:Single">, + logger: KtLogger ): ParserFunctionReturns { - const err = p( - body.score, - PR_USCIRScore, - {}, - { throwOnNonObject: false, allowExcessKeys: true } - ); + const err = p( + body.score, + PR_USCIRScore, + {}, + { throwOnNonObject: false, allowExcessKeys: true } + ); - if (err) { - throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid USC Score.")); - } + if (err) { + throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid USC Score.")); + } - return { - context: { - chart, - }, - game: "usc", - iterable: [body.score] as USCClientScore[], - classHandler: null, - }; + return { + context: { + chart, + }, + game: "usc", + iterable: [body.score] as USCClientScore[], + classHandler: null, + }; } diff --git a/server/src/lib/score-import/import-types/ir/usc/types.ts b/server/src/lib/score-import/import-types/ir/usc/types.ts index 3d74db04b..3d8f51e2e 100644 --- a/server/src/lib/score-import/import-types/ir/usc/types.ts +++ b/server/src/lib/score-import/import-types/ir/usc/types.ts @@ -1,5 +1,5 @@ import { ChartDocument } from "tachi-common"; export interface IRUSCContext { - chart: ChartDocument<"usc:Single">; + chart: ChartDocument<"usc:Single">; } diff --git a/server/src/lib/search/search.test.ts b/server/src/lib/search/search.test.ts index ea9de8618..c6df32a51 100644 --- a/server/src/lib/search/search.test.ts +++ b/server/src/lib/search/search.test.ts @@ -6,35 +6,35 @@ import { LoadKTBlackIIDXData } from "../../test-utils/test-data"; import { SearchGameSongs } from "./search"; t.test("#SearchGameSongs", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); - t.beforeEach(async () => { - await db.songs.iidx.dropIndexes(); - await db.songs.iidx.createIndex( - { - title: "text", - artist: "text", - "alt-titles": "text", - "search-titles": "text", - } as any /* known bug with monk */ - ); - }); + t.beforeEach(ResetDBState); + t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(async () => { + await db.songs.iidx.dropIndexes(); + await db.songs.iidx.createIndex( + { + title: "text", + artist: "text", + "alt-titles": "text", + "search-titles": "text", + } as any /* known bug with monk */ + ); + }); - t.test("Should return songs like the query.", async (t) => { - const res = await SearchGameSongs("iidx", "amuro"); + t.test("Should return songs like the query.", async (t) => { + const res = await SearchGameSongs("iidx", "amuro"); - t.strictSame( - // for simplicity of testing (and because the - // return order is ambiguous) we sort on - // songID here and expect this. - res.sort((a, b) => a.id - b.id).map((e) => e.title), - ["A", "AA", "冥", "F", "HAERETICUS", "ZZ", "X", "AA -rebuild-", "∀"] - ); + t.strictSame( + // for simplicity of testing (and because the + // return order is ambiguous) we sort on + // songID here and expect this. + res.sort((a, b) => a.id - b.id).map((e) => e.title), + ["A", "AA", "冥", "F", "HAERETICUS", "ZZ", "X", "AA -rebuild-", "∀"] + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/lib/search/search.ts b/server/src/lib/search/search.ts index e5bf17f6c..27ba8edf3 100644 --- a/server/src/lib/search/search.ts +++ b/server/src/lib/search/search.ts @@ -4,12 +4,12 @@ import CreateLogCtx from "../logger/logger"; import deepmerge from "deepmerge"; import db from "../../external/mongo/db"; import { - Game, - SessionDocument, - Playtypes, - SongDocument, - AnyChartDocument, - integer, + Game, + SessionDocument, + Playtypes, + SongDocument, + AnyChartDocument, + integer, } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -23,93 +23,93 @@ const logger = CreateLogCtx(__filename); * by. */ export function SearchCollection( - collection: ICollection, - search: string, - existingMatch: FilterQuery = {}, - limit = 100 + collection: ICollection, + search: string, + existingMatch: FilterQuery = {}, + limit = 100 ): Promise<(T & { __textScore: number })[]> { - return collection - .aggregate([ - { - $match: deepmerge({ $text: { $search: search } }, existingMatch), - }, - // Project the __textScore field on so we can sort - // based on search proximity to query - { - $addFields: { - __textScore: { $meta: "textScore" }, - }, - }, - // sort by quality of match - { $sort: { __textScore: -1 } }, - // hide nonsense - { $match: { __textScore: { $gt: 0.25 } } }, - { $limit: limit }, - ]) - .catch((err) => { - logger.error( - `An error occured while trying to $text search collection ${collection.name}. Does this have a $text index?`, - { err } - ); + return collection + .aggregate([ + { + $match: deepmerge({ $text: { $search: search } }, existingMatch), + }, + // Project the __textScore field on so we can sort + // based on search proximity to query + { + $addFields: { + __textScore: { $meta: "textScore" }, + }, + }, + // sort by quality of match + { $sort: { __textScore: -1 } }, + // hide nonsense + { $match: { __textScore: { $gt: 0.25 } } }, + { $limit: limit }, + ]) + .catch((err) => { + logger.error( + `An error occured while trying to $text search collection ${collection.name}. Does this have a $text index?`, + { err } + ); - // throw it up further - throw err; - }); + // throw it up further + throw err; + }); } export type SongSearchReturn = { - __textScore: number; + __textScore: number; } & SongDocument; export function SearchGameSongs( - game: Game, - search: string, - limit = 100 + game: Game, + search: string, + limit = 100 ): Promise { - return SearchCollection(db.songs[game], search, {}, limit); + return SearchCollection(db.songs[game], search, {}, limit); } export async function SearchGameSongsAndCharts( - game: Game, - search: string, - playtype?: Playtypes[Game], - limit = 100 + game: Game, + search: string, + playtype?: Playtypes[Game], + limit = 100 ) { - const songs = await SearchGameSongs(game, search, limit); + const songs = await SearchGameSongs(game, search, limit); - const chartQuery: FilterQuery = { - songID: { $in: songs.map((e) => e.id) }, - }; + const chartQuery: FilterQuery = { + songID: { $in: songs.map((e) => e.id) }, + }; - if (playtype) { - chartQuery.playtype = playtype; - } + if (playtype) { + chartQuery.playtype = playtype; + } - const charts = (await db.charts[game].find(chartQuery)) as AnyChartDocument[]; + const charts = (await db.charts[game].find(chartQuery)) as AnyChartDocument[]; - return { songs, charts }; + return { songs, charts }; } export function SearchSessions( - search: string, - game?: Game, - playtype?: Playtypes[Game], - userID?: integer, - limit = 100 + search: string, + game?: Game, + playtype?: Playtypes[Game], + userID?: integer, + limit = 100 ) { - const baseMatch: FilterQuery = {}; + const baseMatch: FilterQuery = {}; - if (game) { - baseMatch.game = game; - } + if (game) { + baseMatch.game = game; + } - if (playtype) { - baseMatch.playtype = playtype; - } + if (playtype) { + baseMatch.playtype = playtype; + } - if (userID) { - baseMatch.userID = userID; - } + if (userID) { + baseMatch.userID = userID; + } - return SearchCollection(db.sessions, search, baseMatch, limit); + return SearchCollection(db.sessions, search, baseMatch, limit); } diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index c3f456676..5049a20cb 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -13,70 +13,70 @@ const confFile = fs.readFileSync("./conf.json5", "utf-8"); const config = JSON5.parse(confFile); function isValidURL(self: unknown) { - if (typeof self !== "string") { - return `Expected URL, received type ${typeof self}`; - } + if (typeof self !== "string") { + return `Expected URL, received type ${typeof self}`; + } - try { - new URL(self); - return true; - } catch (err) { - return `Invalid URL ${self}.`; - } + try { + new URL(self); + return true; + } catch (err) { + return `Invalid URL ${self}.`; + } } export interface TachiConfig { - MONGO_CONNECTION_URL: string; - MONGO_DATABASE_NAME: string; - LOG_LEVEL: "debug" | "verbose" | "info" | "warn" | "error" | "severe" | "crit"; - CAPTCHA_SECRET_KEY: string; - SESSION_SECRET: string; - FLO_API_URL: string; - EAG_API_URL: string; - ARC_API_URL: string; - ARC_AUTH_TOKEN: string; - CDN_ROOT: string; - TYPE: "ktchi" | "btchi" | "omni"; - PORT: integer; - TYPE_INFO: { - NAME: string; - SUPPORTED_GAMES: Game[]; - }; + MONGO_CONNECTION_URL: string; + MONGO_DATABASE_NAME: string; + LOG_LEVEL: "debug" | "verbose" | "info" | "warn" | "error" | "severe" | "crit"; + CAPTCHA_SECRET_KEY: string; + SESSION_SECRET: string; + FLO_API_URL: string; + EAG_API_URL: string; + ARC_API_URL: string; + ARC_AUTH_TOKEN: string; + CDN_ROOT: string; + TYPE: "ktchi" | "btchi" | "omni"; + PORT: integer; + TYPE_INFO: { + NAME: string; + SUPPORTED_GAMES: Game[]; + }; } const err = p(config, { - MONGO_CONNECTION_URL: "string", - MONGO_DATABASE_NAME: "string", - LOG_LEVEL: p.isIn("debug", "verbose", "info", "warn", "error", "severe", "crit"), - CAPTCHA_SECRET_KEY: "string", - SESSION_SECRET: "string", - FLO_API_URL: isValidURL, - EAG_API_URL: isValidURL, - ARC_API_URL: isValidURL, - ARC_AUTH_TOKEN: "string", - CDN_ROOT: "string", - PORT: p.isPositiveInteger, - TYPE: p.isIn("ktchi", "btchi", "omni"), + MONGO_CONNECTION_URL: "string", + MONGO_DATABASE_NAME: "string", + LOG_LEVEL: p.isIn("debug", "verbose", "info", "warn", "error", "severe", "crit"), + CAPTCHA_SECRET_KEY: "string", + SESSION_SECRET: "string", + FLO_API_URL: isValidURL, + EAG_API_URL: isValidURL, + ARC_API_URL: isValidURL, + ARC_AUTH_TOKEN: "string", + CDN_ROOT: "string", + PORT: p.isPositiveInteger, + TYPE: p.isIn("ktchi", "btchi", "omni"), }); if (err) { - throw FormatPrError(err, "Invalid conf.json5 file."); + throw FormatPrError(err, "Invalid conf.json5 file."); } /** * KTCHI | Kamaitachi is the arcade build of Tachi. */ const KTCHI_INFO: TachiConfig["TYPE_INFO"] = { - NAME: "Kamaitachi", - SUPPORTED_GAMES: ["iidx", "gitadora", "chunithm", "maimai", "museca", "sdvx"], + NAME: "Kamaitachi", + SUPPORTED_GAMES: ["iidx", "gitadora", "chunithm", "maimai", "museca", "sdvx"], }; /** * BTCHI | Bokutachi is the home-simulator build of Tachi. */ const BTCHI_INFO: TachiConfig["TYPE_INFO"] = { - NAME: "Bokutachi", - SUPPORTED_GAMES: ["usc", "bms"], + NAME: "Bokutachi", + SUPPORTED_GAMES: ["usc", "bms"], }; /** @@ -86,16 +86,16 @@ const BTCHI_INFO: TachiConfig["TYPE_INFO"] = { * this codebase this is what you're looking for. */ const OMNI_INFO: TachiConfig["TYPE_INFO"] = { - NAME: "Omnitachi", - SUPPORTED_GAMES: [...KTCHI_INFO.SUPPORTED_GAMES, ...BTCHI_INFO.SUPPORTED_GAMES], + NAME: "Omnitachi", + SUPPORTED_GAMES: [...KTCHI_INFO.SUPPORTED_GAMES, ...BTCHI_INFO.SUPPORTED_GAMES], }; if (config.TYPE === "ktchi") { - config.TYPE_INFO = KTCHI_INFO; + config.TYPE_INFO = KTCHI_INFO; } else if (config.TYPE === "btchi") { - config.TYPE_INFO = BTCHI_INFO; + config.TYPE_INFO = BTCHI_INFO; } else if (config.TYPE === "omni") { - config.TYPE_INFO = OMNI_INFO; + config.TYPE_INFO = OMNI_INFO; } const tachiConfig = config as TachiConfig; diff --git a/server/src/server/middleware/auth.test.ts b/server/src/server/middleware/auth.test.ts index ce38eadc2..6ba947863 100644 --- a/server/src/server/middleware/auth.test.ts +++ b/server/src/server/middleware/auth.test.ts @@ -6,111 +6,111 @@ import { SYMBOL_TachiAPIAuth } from "../../lib/constants/tachi"; import { CloseAllConnections } from "../../test-utils/close-connections"; t.test("#SetRequestPermissions", (t) => { - t.test("Should assign valid APIKey information to req[SYMBOL_TachiAPIData]", async (t) => { - await db["api-tokens"].insert({ - userID: 1, - identifier: "Mock API Token", - permissions: { - "create:goal": true, - }, - token: "mock_token", - }); + t.test("Should assign valid APIKey information to req[SYMBOL_TachiAPIData]", async (t) => { + await db["api-tokens"].insert({ + userID: 1, + identifier: "Mock API Token", + permissions: { + "create:goal": true, + }, + token: "mock_token", + }); - const { req } = await expMiddlewareMock(SetRequestPermissions, { - headers: { - authorization: "Bearer mock_token", - }, - }); + const { req } = await expMiddlewareMock(SetRequestPermissions, { + headers: { + authorization: "Bearer mock_token", + }, + }); - t.strictSame(req[SYMBOL_TachiAPIAuth], { - userID: 1, - identifier: "Mock API Token", - permissions: { - "create:goal": true, - }, - token: "mock_token", - }); + t.strictSame(req[SYMBOL_TachiAPIAuth], { + userID: 1, + identifier: "Mock API Token", + permissions: { + "create:goal": true, + }, + token: "mock_token", + }); - t.end(); - }); + t.end(); + }); - t.test("Should assign guest APIKey information if no auth present", async (t) => { - const { req } = await expMiddlewareMock(SetRequestPermissions); + t.test("Should assign guest APIKey information if no auth present", async (t) => { + const { req } = await expMiddlewareMock(SetRequestPermissions); - t.strictSame(req[SYMBOL_TachiAPIAuth], { - userID: null, - identifier: "Guest Token", - permissions: {}, - token: null, - }); + t.strictSame(req[SYMBOL_TachiAPIAuth], { + userID: null, + identifier: "Guest Token", + permissions: {}, + token: null, + }); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if auth type is not Bearer", async (t) => { - const { res } = await expMiddlewareMock(SetRequestPermissions, { - headers: { - authorization: "Basic Foo", - }, - }); + t.test("Should return 400 if auth type is not Bearer", async (t) => { + const { res } = await expMiddlewareMock(SetRequestPermissions, { + headers: { + authorization: "Basic Foo", + }, + }); - t.equal(res.statusCode, 400); + t.equal(res.statusCode, 400); - const json = res._getJSONData(); - t.match(json.description, /Invalid Authorization Type - Expected Bearer/u); - t.end(); - }); + const json = res._getJSONData(); + t.match(json.description, /Invalid Authorization Type - Expected Bearer/u); + t.end(); + }); - t.test("Should return 401 if no token is present", async (t) => { - const { res } = await expMiddlewareMock(SetRequestPermissions, { - headers: { - authorization: "Bearer ", - }, - }); + t.test("Should return 401 if no token is present", async (t) => { + const { res } = await expMiddlewareMock(SetRequestPermissions, { + headers: { + authorization: "Bearer ", + }, + }); - t.equal(res.statusCode, 401); + t.equal(res.statusCode, 401); - const json = res._getJSONData(); - t.match(json.description, /Invalid token/u); - t.end(); - }); + const json = res._getJSONData(); + t.match(json.description, /Invalid token/u); + t.end(); + }); - t.test("Should return 401 if token is unknown", async (t) => { - const { res } = await expMiddlewareMock(SetRequestPermissions, { - headers: { - authorization: "Bearer unknown_token", - }, - }); + t.test("Should return 401 if token is unknown", async (t) => { + const { res } = await expMiddlewareMock(SetRequestPermissions, { + headers: { + authorization: "Bearer unknown_token", + }, + }); - t.equal(res.statusCode, 401); + t.equal(res.statusCode, 401); - const json = res._getJSONData(); - t.match( - json.description, - /The provided API token does not correspond with any key in the database/u - ); - t.end(); - }); + const json = res._getJSONData(); + t.match( + json.description, + /The provided API token does not correspond with any key in the database/u + ); + t.end(); + }); - t.test("Should assign a Session-Key if user authenticates with cookie.", async (t) => { - const { req } = await expMiddlewareMock(SetRequestPermissions, { - session: { - tachi: { - userID: 1, - }, - }, - }); + t.test("Should assign a Session-Key if user authenticates with cookie.", async (t) => { + const { req } = await expMiddlewareMock(SetRequestPermissions, { + session: { + tachi: { + userID: 1, + }, + }, + }); - t.strictSame(req[SYMBOL_TachiAPIAuth], { - userID: 1, - identifier: `Session-Key 1`, - token: null, - permissions: AllPermissions, - }); - t.end(); - }); + t.strictSame(req[SYMBOL_TachiAPIAuth], { + userID: 1, + identifier: `Session-Key 1`, + token: null, + permissions: AllPermissions, + }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/middleware/auth.ts b/server/src/server/middleware/auth.ts index 5e5a3f6b8..d94403fc8 100644 --- a/server/src/server/middleware/auth.ts +++ b/server/src/server/middleware/auth.ts @@ -8,16 +8,16 @@ import CreateLogCtx from "../../lib/logger/logger"; const logger = CreateLogCtx(__filename); const GuestToken: APITokenDocument = { - token: null, - userID: null, - identifier: "Guest Token", - permissions: {}, + token: null, + userID: null, + identifier: "Guest Token", + permissions: {}, }; export const AllPermissions: Record = { - "create:goal": true, - "manage:goal": true, - "submit:score": true, + "create:goal": true, + "manage:goal": true, + "submit:score": true, }; /** @@ -35,59 +35,59 @@ export const AllPermissions: Record = { * This is set on req[SYMBOL_TachiAPIData]. */ export const SetRequestPermissions: RequestHandler = async (req, res, next) => { - if (req.session?.tachi?.userID) { - req[SYMBOL_TachiAPIAuth] = { - userID: req.session.tachi.userID, - identifier: `Session-Key ${req.session.tachi.userID}`, - token: null, - permissions: AllPermissions, - }; - return next(); - } + if (req.session?.tachi?.userID) { + req[SYMBOL_TachiAPIAuth] = { + userID: req.session.tachi.userID, + identifier: `Session-Key ${req.session.tachi.userID}`, + token: null, + permissions: AllPermissions, + }; + return next(); + } - const header = req.header("Authorization"); + const header = req.header("Authorization"); - // if no auth was attempted, default to the guest token. - if (!header) { - req[SYMBOL_TachiAPIAuth] = GuestToken; - return next(); - } + // if no auth was attempted, default to the guest token. + if (!header) { + req[SYMBOL_TachiAPIAuth] = GuestToken; + return next(); + } - const { token, type } = SplitAuthorizationHeader(header); + const { token, type } = SplitAuthorizationHeader(header); - if (type !== "Bearer") { - return res.status(400).json({ - success: false, - description: "Invalid Authorization Type - Expected Bearer.", - }); - } + if (type !== "Bearer") { + return res.status(400).json({ + success: false, + description: "Invalid Authorization Type - Expected Bearer.", + }); + } - if (!token) { - return res.status(401).json({ - success: false, - description: "Invalid token.", - }); - } + if (!token) { + return res.status(401).json({ + success: false, + description: "Invalid token.", + }); + } - const apiTokenData = await db["api-tokens"].findOne({ - token, - }); + const apiTokenData = await db["api-tokens"].findOne({ + token, + }); - if (!apiTokenData) { - return res.status(401).json({ - success: false, - description: "The provided API token does not correspond with any key in the database.", - }); - } + if (!apiTokenData) { + return res.status(401).json({ + success: false, + description: "The provided API token does not correspond with any key in the database.", + }); + } - req[SYMBOL_TachiAPIAuth] = { - userID: apiTokenData.userID, - token, - permissions: apiTokenData.permissions, - identifier: apiTokenData.identifier, - }; + req[SYMBOL_TachiAPIAuth] = { + userID: apiTokenData.userID, + token, + permissions: apiTokenData.permissions, + identifier: apiTokenData.identifier, + }; - return next(); + return next(); }; /** @@ -96,59 +96,59 @@ export const SetRequestPermissions: RequestHandler = async (req, res, next) => { * @returns A middleware function. */ export const RequirePermissions = - (...perms: APIPermissions[]): RequestHandler => - (req, res, next) => { - if (!req[SYMBOL_TachiAPIAuth]) { - logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); + (...perms: APIPermissions[]): RequestHandler => + (req, res, next) => { + if (!req[SYMBOL_TachiAPIAuth]) { + logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); - return res.status(500).json({ - success: false, - description: "An internal error has occured.", - }); - } + return res.status(500).json({ + success: false, + description: "An internal error has occured.", + }); + } - const missingPerms = []; - for (const perm of perms) { - if (!req[SYMBOL_TachiAPIAuth]!.permissions[perm]) { - missingPerms.push(perm); - } - } + const missingPerms = []; + for (const perm of perms) { + if (!req[SYMBOL_TachiAPIAuth]!.permissions[perm]) { + missingPerms.push(perm); + } + } - if (missingPerms.length > 0) { - logger.info( - `IP ${req.ip} - userID ${ - req[SYMBOL_TachiAPIAuth].userID - } had insufficient permissions for request ${req.method} ${ - req.url - }. ${missingPerms.join(", ")}` - ); - return res.status(401).json({ - success: false, - description: `You are missing the following permissions necessary for this request: ${missingPerms.join( - ", " - )}`, - }); - } + if (missingPerms.length > 0) { + logger.info( + `IP ${req.ip} - userID ${ + req[SYMBOL_TachiAPIAuth].userID + } had insufficient permissions for request ${req.method} ${ + req.url + }. ${missingPerms.join(", ")}` + ); + return res.status(401).json({ + success: false, + description: `You are missing the following permissions necessary for this request: ${missingPerms.join( + ", " + )}`, + }); + } - return next(); - }; + return next(); + }; export const RequireNotGuest: RequestHandler = (req, res, next) => { - if (!req[SYMBOL_TachiAPIAuth]) { - logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); - return res.status(500).json({ - success: false, - description: "An internal error has occured.", - }); - } + if (!req[SYMBOL_TachiAPIAuth]) { + logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); + return res.status(500).json({ + success: false, + description: "An internal error has occured.", + }); + } - if (!req[SYMBOL_TachiAPIAuth].userID) { - logger.info(`Request to ${req.method} ${req.url} was attempted by guest.`); - return res.status(401).json({ - success: false, - description: "This endpoint requires authentication.", - }); - } + if (!req[SYMBOL_TachiAPIAuth].userID) { + logger.info(`Request to ${req.method} ${req.url} was attempted by guest.`); + return res.status(401).json({ + success: false, + description: "This endpoint requires authentication.", + }); + } - return next(); + return next(); }; diff --git a/server/src/server/middleware/multer-upload.ts b/server/src/server/middleware/multer-upload.ts index 3f5b1caa3..13fb4adab 100644 --- a/server/src/server/middleware/multer-upload.ts +++ b/server/src/server/middleware/multer-upload.ts @@ -11,32 +11,32 @@ const defaultLogger = CreateLogCtx(__filename); export const DefaultMulterUpload = multer({ limits: { fileSize: 1024 * 1024 * 16 } }); // 16MB export const CreateMulterSingleUploadMiddleware = ( - fieldName: string, - fileSize: integer = SIXTEEN_MEGABTYES, - logger = defaultLogger + fieldName: string, + fileSize: integer = SIXTEEN_MEGABTYES, + logger = defaultLogger ): RequestHandler => { - const UploadMW = multer({ limits: { fileSize } }).single(fieldName); + const UploadMW = multer({ limits: { fileSize } }).single(fieldName); - return (req, res, next) => { - UploadMW(req, res, (err: unknown) => { - if (err instanceof MulterError) { - logger.info(`Multer Error.`, { err }); + return (req, res, next) => { + UploadMW(req, res, (err: unknown) => { + if (err instanceof MulterError) { + logger.info(`Multer Error.`, { err }); - return res.status(400).json({ - success: false, - description: - "File provided was too large, corrupt, or provided in the wrong field.", - }); - } else if (err) { - logger.error(`Unknown file import error: ${err}`, { err }); + return res.status(400).json({ + success: false, + description: + "File provided was too large, corrupt, or provided in the wrong field.", + }); + } else if (err) { + logger.error(`Unknown file import error: ${err}`, { err }); - return res.status(500).json({ - success: false, - description: `An internal server error has occured.`, - }); - } + return res.status(500).json({ + success: false, + description: `An internal server error has occured.`, + }); + } - return next(); - }); - }; + return next(); + }); + }; }; diff --git a/server/src/server/middleware/prudence-validate.test.ts b/server/src/server/middleware/prudence-validate.test.ts index 0fe8ab15e..f5365e62a 100644 --- a/server/src/server/middleware/prudence-validate.test.ts +++ b/server/src/server/middleware/prudence-validate.test.ts @@ -4,140 +4,140 @@ import prValidate from "./prudence-validate"; import Prudence from "prudence"; t.test("#PrudenceMiddleware", (t) => { - const mw = prValidate({ foo: Prudence.regex(/^baz$/u) }, { foo: "example error message" }); + const mw = prValidate({ foo: Prudence.regex(/^baz$/u) }, { foo: "example error message" }); - t.test("Should return 400 on invalid prudence validation", async (t) => { - const { res } = await expMiddlewareMock(mw, { - query: { - foo: "bar", - }, - }); + t.test("Should return 400 on invalid prudence validation", async (t) => { + const { res } = await expMiddlewareMock(mw, { + query: { + foo: "bar", + }, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - const json = res._getJSONData(); - t.equal( - json.description, - "example error message (Received bar) [K:foo]", - "Should return error message" - ); + const json = res._getJSONData(); + t.equal( + json.description, + "example error message (Received bar) [K:foo]", + "Should return error message" + ); - t.end(); - }); + t.end(); + }); - t.test("Should return 'nothing' instead of undefined for missing fields", async (t) => { - const { res } = await expMiddlewareMock(mw, { - query: {}, - }); + t.test("Should return 'nothing' instead of undefined for missing fields", async (t) => { + const { res } = await expMiddlewareMock(mw, { + query: {}, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - const json = res._getJSONData(); - t.equal( - json.description, - "example error message (Received nothing) [K:foo]", - "Should return error message with recieved nothing" - ); + const json = res._getJSONData(); + t.equal( + json.description, + "example error message (Received nothing) [K:foo]", + "Should return error message with recieved nothing" + ); - t.end(); - }); + t.end(); + }); - t.test("Should allow valid prudence data.", async (t) => { - const { res } = await expMiddlewareMock(mw, { - query: { - foo: "baz", - }, - }); + t.test("Should allow valid prudence data.", async (t) => { + const { res } = await expMiddlewareMock(mw, { + query: { + foo: "baz", + }, + }); - t.equal(res.statusCode, 200, "Should stay as 200"); + t.equal(res.statusCode, 200, "Should stay as 200"); - // no body -- not returned. - t.equal(res._isJSON(), false, "Should not have any data set"); + // no body -- not returned. + t.equal(res._isJSON(), false, "Should not have any data set"); - t.end(); - }); + t.end(); + }); - t.test("Should allow valid bodies on non-GET requests", async (t) => { - const { res } = await expMiddlewareMock(mw, { - method: "POST", - body: { - foo: "baz", - }, - }); + t.test("Should allow valid bodies on non-GET requests", async (t) => { + const { res } = await expMiddlewareMock(mw, { + method: "POST", + body: { + foo: "baz", + }, + }); - t.equal(res.statusCode, 200, "Should stay as 200"); + t.equal(res.statusCode, 200, "Should stay as 200"); - // no body -- not returned. - t.equal(res._isJSON(), false, "Should not have any data set"); + // no body -- not returned. + t.equal(res._isJSON(), false, "Should not have any data set"); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 on invalid prudence validation for non-GET requests", async (t) => { - const { res } = await expMiddlewareMock(mw, { - method: "POST", - body: { - foo: "bar", - }, - }); + t.test("Should return 400 on invalid prudence validation for non-GET requests", async (t) => { + const { res } = await expMiddlewareMock(mw, { + method: "POST", + body: { + foo: "bar", + }, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - const json = res._getJSONData(); - t.equal( - json.description, - "example error message (Received bar) [K:foo]", - "Should return error message" - ); + const json = res._getJSONData(); + t.equal( + json.description, + "example error message (Received bar) [K:foo]", + "Should return error message" + ); - t.end(); - }); + t.end(); + }); - const mwWithPassword = prValidate({ password: "string" }, { password: "invalid password" }); + const mwWithPassword = prValidate({ password: "string" }, { password: "invalid password" }); - t.test( - "Should not return the contents of the error message if the field matches /password/", - async (t) => { - const { res } = await expMiddlewareMock(mwWithPassword, { - query: { - password: 123, - }, - }); + t.test( + "Should not return the contents of the error message if the field matches /password/", + async (t) => { + const { res } = await expMiddlewareMock(mwWithPassword, { + query: { + password: 123, + }, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - const json = res._getJSONData(); - t.equal( - json.description, - "invalid password (Received ****) [K:password]", - "Should return obscured error message" - ); + const json = res._getJSONData(); + t.equal( + json.description, + "invalid password (Received ****) [K:password]", + "Should return obscured error message" + ); - t.end(); - } - ); + t.end(); + } + ); - t.test( - "Should not return the contents of the error message if the field matches /password/", - async (t) => { - const { res } = await expMiddlewareMock(mwWithPassword, { - query: { - password: undefined, - }, - }); + t.test( + "Should not return the contents of the error message if the field matches /password/", + async (t) => { + const { res } = await expMiddlewareMock(mwWithPassword, { + query: { + password: undefined, + }, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - const json = res._getJSONData(); - t.equal( - json.description, - "invalid password (Received nothing) [K:password]", - "Should indicate if no data was sent in obscured error message" - ); + const json = res._getJSONData(); + t.equal( + json.description, + "invalid password (Received nothing) [K:password]", + "Should indicate if no data was sent in obscured error message" + ); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); diff --git a/server/src/server/middleware/prudence-validate.ts b/server/src/server/middleware/prudence-validate.ts index dff72eeb7..8d725dd31 100644 --- a/server/src/server/middleware/prudence-validate.ts +++ b/server/src/server/middleware/prudence-validate.ts @@ -4,31 +4,31 @@ import CreateLogCtx from "../../lib/logger/logger"; const logger = CreateLogCtx(__filename); const printf = (message: string, stringVal: string | null, keychain: string | null) => - `${message}${stringVal ? ` (Received ${stringVal})` : ""} [K:${keychain}]`; + `${message}${stringVal ? ` (Received ${stringVal})` : ""} [K:${keychain}]`; const API_ERR_HANDLER: MiddlewareErrorHandler = (req, res, next, error) => { - let stringVal = error.userVal; - if (error.keychain && error.keychain.includes("password") && error.userVal) { - stringVal = "****"; - } + let stringVal = error.userVal; + if (error.keychain && error.keychain.includes("password") && error.userVal) { + stringVal = "****"; + } - if (typeof stringVal === "object" && stringVal !== null && !stringVal.toString) { - // this is probably null-prototype - stringVal = null; - } else if (stringVal === undefined) { - stringVal = "nothing"; - } else { - stringVal = String(stringVal); - } + if (typeof stringVal === "object" && stringVal !== null && !stringVal.toString) { + // this is probably null-prototype + stringVal = null; + } else if (stringVal === undefined) { + stringVal = "nothing"; + } else { + stringVal = String(stringVal); + } - logger.info(`Prudence rejection: ${error.message}, ${stringVal} [K:${error.keychain}]`, { - userVal: error.userVal, - }); + logger.info(`Prudence rejection: ${error.message}, ${stringVal} [K:${error.keychain}]`, { + userVal: error.userVal, + }); - return res.status(400).json({ - success: false, - description: printf(error.message, stringVal as string | null, error.keychain), - }); + return res.status(400).json({ + success: false, + description: printf(error.message, stringVal as string | null, error.keychain), + }); }; const prValidate = Prudence.CurryMiddleware(API_ERR_HANDLER); diff --git a/server/src/server/middleware/rate-limiter.ts b/server/src/server/middleware/rate-limiter.ts index 4ec2b9631..3893c72bb 100644 --- a/server/src/server/middleware/rate-limiter.ts +++ b/server/src/server/middleware/rate-limiter.ts @@ -7,28 +7,28 @@ import { RedisClient } from "../../external/redis/redis"; const logger = CreateLogCtx(__filename); const store = - process.env.NODE_ENV === "production" - ? new RateLimitRedis({ prefix: `${CONFIG.TYPE}-RL:`, client: RedisClient }) - : undefined; // undefined forces a default to an in-memory store + process.env.NODE_ENV === "production" + ? new RateLimitRedis({ prefix: `${CONFIG.TYPE}-RL:`, client: RedisClient }) + : undefined; // undefined forces a default to an in-memory store export function ClearTestingRateLimitCache() { - // ??? - RateLimitMiddleware.resetKey(`::ffff:127.0.0.1`); + // ??? + RateLimitMiddleware.resetKey(`::ffff:127.0.0.1`); } // 100 requests / minute is the current cap export const RateLimitMiddleware = rateLimit({ - max: 100, - onLimitReached: (req) => { - logger.warn(`User ${req.ip} hit rate limit.`, { - url: req.url, - method: req.method, - hideFromConsole: ["req"], - }); - }, - store, - message: { - success: false, - description: "You have exceeded 100 requests per minute. Please wait.", - } as any, // @todo #170 report this as a bug with rate-limit types., + max: 100, + onLimitReached: (req) => { + logger.warn(`User ${req.ip} hit rate limit.`, { + url: req.url, + method: req.method, + hideFromConsole: ["req"], + }); + }, + store, + message: { + success: false, + description: "You have exceeded 100 requests per minute. Please wait.", + } as any, // @todo #170 report this as a bug with rate-limit types., }); diff --git a/server/src/server/middleware/require-logged-in.test.ts b/server/src/server/middleware/require-logged-in.test.ts index 87104524d..7fef5696b 100644 --- a/server/src/server/middleware/require-logged-in.test.ts +++ b/server/src/server/middleware/require-logged-in.test.ts @@ -3,34 +3,34 @@ import expMiddlewareMock from "express-request-mock"; import t from "tap"; t.test("#RequireLoggedIn", (t) => { - t.test("Should reject users that are not logged in.", async (t) => { - const { res } = await expMiddlewareMock(RequireLoggedInSession, { - session: { - tachi: null, - }, - }); + t.test("Should reject users that are not logged in.", async (t) => { + const { res } = await expMiddlewareMock(RequireLoggedInSession, { + session: { + tachi: null, + }, + }); - t.equal(res.statusCode, 401, "Should return 401."); + t.equal(res.statusCode, 401, "Should return 401."); - const json = res._getJSONData(); + const json = res._getJSONData(); - t.strictSame(json, { - success: false, - description: "You are not authorised to perform this action.", - }); - }); + t.strictSame(json, { + success: false, + description: "You are not authorised to perform this action.", + }); + }); - t.test("Should allow users that are logged in.", async (t) => { - const { res } = await expMiddlewareMock(RequireLoggedInSession, { - session: { - tachi: { - userID: 1, - }, - }, - }); + t.test("Should allow users that are logged in.", async (t) => { + const { res } = await expMiddlewareMock(RequireLoggedInSession, { + session: { + tachi: { + userID: 1, + }, + }, + }); - t.equal(res.statusCode, 200, "Request should still be 200."); - }); + t.equal(res.statusCode, 200, "Request should still be 200."); + }); - t.end(); + t.end(); }); diff --git a/server/src/server/middleware/require-logged-in.ts b/server/src/server/middleware/require-logged-in.ts index bcb3ea72e..d761682f1 100644 --- a/server/src/server/middleware/require-logged-in.ts +++ b/server/src/server/middleware/require-logged-in.ts @@ -4,26 +4,26 @@ import CreateLogCtx from "../../lib/logger/logger"; const logger = CreateLogCtx(__filename); export const RequireLoggedInSession: RequestHandler = (req, res, next) => { - if (!req.session.tachi?.userID) { - logger.info(`Received unauthorised request from ${req.ip} from ${req.originalUrl}`); + if (!req.session.tachi?.userID) { + logger.info(`Received unauthorised request from ${req.ip} from ${req.originalUrl}`); - return res.status(401).json({ - success: false, - description: `You are not authorised to perform this action.`, - }); - } + return res.status(401).json({ + success: false, + description: `You are not authorised to perform this action.`, + }); + } - next(); + next(); }; export const RequireNotLoggedInSession: RequestHandler = (req, res, next) => { - if (req.session.tachi?.userID) { - logger.info(`Dual log-in attempted from ${req.session.tachi.userID}`); - return res.status(409).json({ - success: false, - description: `You cannot perform this while logged in.`, - }); - } + if (req.session.tachi?.userID) { + logger.info(`Dual log-in attempted from ${req.session.tachi.userID}`); + return res.status(409).json({ + success: false, + description: `You cannot perform this while logged in.`, + }); + } - return next(); + return next(); }; diff --git a/server/src/server/middleware/sanitise-qstring.test.ts b/server/src/server/middleware/sanitise-qstring.test.ts index af58ce784..9a2c8f9f5 100644 --- a/server/src/server/middleware/sanitise-qstring.test.ts +++ b/server/src/server/middleware/sanitise-qstring.test.ts @@ -3,33 +3,33 @@ import t from "tap"; import expMiddlewareMock from "express-request-mock"; t.test("#SanitiseQString", (t) => { - t.test("Should allow GET requests with valid data.", async (t) => { - const { res } = await expMiddlewareMock(SanitiseQString, { - method: "GET", - query: { - foo: "bar", - }, - }); + t.test("Should allow GET requests with valid data.", async (t) => { + const { res } = await expMiddlewareMock(SanitiseQString, { + method: "GET", + query: { + foo: "bar", + }, + }); - t.not(res.statusCode, 400, "Status code should NOT be 400"); + t.not(res.statusCode, 400, "Status code should NOT be 400"); - t.end(); - }); + t.end(); + }); - t.test("Should disallow GET requests with nested data.", async (t) => { - const { res } = await expMiddlewareMock(SanitiseQString, { - method: "GET", - query: { - foo: { - bar: "baz", - }, - }, - }); + t.test("Should disallow GET requests with nested data.", async (t) => { + const { res } = await expMiddlewareMock(SanitiseQString, { + method: "GET", + query: { + foo: { + bar: "baz", + }, + }, + }); - t.equal(res.statusCode, 400, "Status code should be 400"); + t.equal(res.statusCode, 400, "Status code should be 400"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); diff --git a/server/src/server/middleware/sanitise-qstring.ts b/server/src/server/middleware/sanitise-qstring.ts index fe3962170..3b769d1bf 100644 --- a/server/src/server/middleware/sanitise-qstring.ts +++ b/server/src/server/middleware/sanitise-qstring.ts @@ -4,18 +4,18 @@ import { NextFunction, Request, Response } from "express"; * Disallow complex query strings by enforcing single key:value pairs. */ function SanitiseQString(req: Request, res: Response, next: NextFunction) { - if (req.method === "GET") { - for (const data in req.query) { - if (typeof req.query[data] !== "string") { - return res.status(400).json({ - success: false, - description: `Invalid request with key ${data}`, - }); - } - } - } + if (req.method === "GET") { + for (const data in req.query) { + if (typeof req.query[data] !== "string") { + return res.status(400).json({ + success: false, + description: `Invalid request with key ${data}`, + }); + } + } + } - next(); + next(); } export default SanitiseQString; diff --git a/server/src/server/middleware/type-require.ts b/server/src/server/middleware/type-require.ts index 5a6ecfbc8..9e7448c97 100644 --- a/server/src/server/middleware/type-require.ts +++ b/server/src/server/middleware/type-require.ts @@ -7,14 +7,14 @@ import { RequestHandler } from "express"; * this restriction is bypassed. */ export const RequireBokutachi: RequestHandler = (req, res, next) => { - if (CONFIG.TYPE === "btchi" || CONFIG.TYPE === "omni") { - return next(); - } + if (CONFIG.TYPE === "btchi" || CONFIG.TYPE === "omni") { + return next(); + } - return res.status(404).send({ - success: false, - description: `The route ${req.url} is only available on Bokutachi.`, - }); + return res.status(404).send({ + success: false, + description: `The route ${req.url} is only available on Bokutachi.`, + }); }; /** @@ -23,12 +23,12 @@ export const RequireBokutachi: RequestHandler = (req, res, next) => { * this restriction is bypassed. */ export const RequireKamaitachi: RequestHandler = (req, res, next) => { - if (CONFIG.TYPE === "ktchi" || CONFIG.TYPE === "omni") { - return next(); - } + if (CONFIG.TYPE === "ktchi" || CONFIG.TYPE === "omni") { + return next(); + } - return res.status(404).send({ - success: false, - description: `The route ${req.url} is only available on Kamaitachi.`, - }); + return res.status(404).send({ + success: false, + description: `The route ${req.url} is only available on Kamaitachi.`, + }); }; diff --git a/server/src/server/router/api/v1/auth/auth.test.ts b/server/src/server/router/api/v1/auth/auth.test.ts index eda18bf7e..1d0c614a6 100644 --- a/server/src/server/router/api/v1/auth/auth.test.ts +++ b/server/src/server/router/api/v1/auth/auth.test.ts @@ -6,67 +6,67 @@ import { MockBasicFetch } from "../../../../../test-utils/mock-fetch"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; t.test("#ReinstateInvite", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should change the 'consumed' property of an invite to true.", async (t) => { - // mock insert - const inviteDoc = await db.invites.insert({ - code: "foobar", - consumed: true, - createdBy: 1, - createdOn: 1, - }); + t.test("Should change the 'consumed' property of an invite to true.", async (t) => { + // mock insert + const inviteDoc = await db.invites.insert({ + code: "foobar", + consumed: true, + createdBy: 1, + createdOn: 1, + }); - const response = await ReinstateInvite(inviteDoc); + const response = await ReinstateInvite(inviteDoc); - t.equal(response.nModified, 1, "Should modify one document"); + t.equal(response.nModified, 1, "Should modify one document"); - const invite2 = await db.invites.findOne({ - code: inviteDoc.code, // lol - }); + const invite2 = await db.invites.findOne({ + code: inviteDoc.code, // lol + }); - t.equal(invite2!.consumed, false, "Should no longer be consumed"); + t.equal(invite2!.consumed, false, "Should no longer be consumed"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#AddNewInvite", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should create a new invite from a given user", async (t) => { - const userDoc = await db.users.findOne({ id: 1 }); + t.test("Should create a new invite from a given user", async (t) => { + const userDoc = await db.users.findOne({ id: 1 }); - const result = await AddNewInvite(userDoc!); + const result = await AddNewInvite(userDoc!); - t.equal(result.createdBy, userDoc!.id, "Invite should be created by the requesting user."); - t.equal(result.consumed, false, "Invite should not be consumed."); + t.equal(result.createdBy, userDoc!.id, "Invite should be created by the requesting user."); + t.equal(result.consumed, false, "Invite should not be consumed."); - // was created +/- 6 seconds from now. This is perhaps too lenient, but we're only really testing its just around now ish. - t.ok(Math.abs(result.createdOn - Date.now()) <= 6000, "Invite was created roughly now."); + // was created +/- 6 seconds from now. This is perhaps too lenient, but we're only really testing its just around now ish. + t.ok(Math.abs(result.createdOn - Date.now()) <= 6000, "Invite was created roughly now."); - t.match(result.code, /^[0-9a-f]{40}$/u, "Invite code should be a 40 character hex string."); - }); + t.match(result.code, /^[0-9a-f]{40}$/u, "Invite code should be a 40 character hex string."); + }); - t.end(); + t.end(); }); t.test("#ValidateCaptcha", async (t) => { - t.equal( - await ValidateCaptcha("200", "bar", MockBasicFetch({ status: 200 })), - true, - "Validates captcha when status return is 200" - ); + t.equal( + await ValidateCaptcha("200", "bar", MockBasicFetch({ status: 200 })), + true, + "Validates captcha when status return is 200" + ); - t.equal( - await ValidateCaptcha("400", "bar", MockBasicFetch({ status: 400 })), - false, - "Invalidates captcha when status return is not 200" - ); + t.equal( + await ValidateCaptcha("400", "bar", MockBasicFetch({ status: 400 })), + false, + "Invalidates captcha when status return is not 200" + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/auth/auth.ts b/server/src/server/router/api/v1/auth/auth.ts index 4be777857..8340f60e3 100644 --- a/server/src/server/router/api/v1/auth/auth.ts +++ b/server/src/server/router/api/v1/auth/auth.ts @@ -15,7 +15,7 @@ const logger = CreateLogCtx(__filename); const BCRYPT_SALT_ROUNDS = 12; export const ValidatePassword = (self: unknown) => - (typeof self === "string" && self.length >= 8) || "Passwords must be 8 characters or more."; + (typeof self === "string" && self.length >= 8) || "Passwords must be 8 characters or more."; /** * Compares a plaintext string of a users password to a hash. @@ -23,97 +23,97 @@ export const ValidatePassword = (self: unknown) => * @param password The hash to compare against. */ export function PasswordCompare(plaintext: string, password: string) { - return bcrypt.compare(plaintext, password); + return bcrypt.compare(plaintext, password); } export function ReinstateInvite(inviteDoc: InviteCodeDocument) { - logger.info(`Reinstated Invite ${inviteDoc.code}`); - return db.invites.update( - { - _id: inviteDoc._id, - }, - { - $set: { - consumed: false, - }, - } - ); + logger.info(`Reinstated Invite ${inviteDoc.code}`); + return db.invites.update( + { + _id: inviteDoc._id, + }, + { + $set: { + consumed: false, + }, + } + ); } export async function AddNewInvite(user: PublicUserDocument) { - const code = Random20Hex(); + const code = Random20Hex(); - const result = await db.invites.insert({ - code, - consumed: false, - createdBy: user.id, - createdOn: Date.now(), - }); + const result = await db.invites.insert({ + code, + consumed: false, + createdBy: user.id, + createdOn: Date.now(), + }); - logger.info(`User ${FormatUserDoc(user)} created an invite.`); + logger.info(`User ${FormatUserDoc(user)} created an invite.`); - if (!result) { - logger.error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - throw new Error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - } + if (!result) { + logger.error( + `Fatal error in creating ${FormatUserDoc( + user + )}'s invite code. Database refused key ${code}.` + ); + throw new Error( + `Fatal error in creating ${FormatUserDoc( + user + )}'s invite code. Database refused key ${code}.` + ); + } - return result; + return result; } export async function AddNewUser( - username: string, - password: string, - email: string + username: string, + password: string, + email: string ): Promise> { - const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); + const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); - logger.verbose(`Hashed password for ${username}.`); + logger.verbose(`Hashed password for ${username}.`); - const userID = await GetNextCounterValue("users"); + const userID = await GetNextCounterValue("users"); - const userDoc: PrivateUserDocument = { - id: userID, - username: username, - usernameLowercase: username.toLowerCase(), - password: hashedPassword, - about: "I'm a fairly nondescript person.", - email: email, - clan: null, - friends: [], - socialMedia: {}, - settings: { - invisible: false, - nsfwSplashes: false, - }, - customBanner: false, - customPfp: false, - lastSeen: Date.now(), // lol - }; + const userDoc: PrivateUserDocument = { + id: userID, + username: username, + usernameLowercase: username.toLowerCase(), + password: hashedPassword, + about: "I'm a fairly nondescript person.", + email: email, + clan: null, + friends: [], + socialMedia: {}, + settings: { + invisible: false, + nsfwSplashes: false, + }, + customBanner: false, + customPfp: false, + lastSeen: Date.now(), // lol + }; - return db.users.insert(userDoc); + return db.users.insert(userDoc); } export async function ValidateCaptcha( - recaptcha: string, - remoteAddr: string | undefined, - fetch = nodeFetch + recaptcha: string, + remoteAddr: string | undefined, + fetch = nodeFetch ) { - const r = await fetch( - `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET_KEY}&response=${recaptcha}&remoteip=${remoteAddr}` - ); + const r = await fetch( + `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET_KEY}&response=${recaptcha}&remoteip=${remoteAddr}` + ); - if (r.status !== 200) { - logger.verbose(`Failed GCaptcha response ${r.status}, ${r.body}`); - return false; - } + if (r.status !== 200) { + logger.verbose(`Failed GCaptcha response ${r.status}, ${r.body}`); + return false; + } - return true; + return true; } diff --git a/server/src/server/router/api/v1/auth/router.test.ts b/server/src/server/router/api/v1/auth/router.test.ts index c4fc4cc55..5e0fd0351 100644 --- a/server/src/server/router/api/v1/auth/router.test.ts +++ b/server/src/server/router/api/v1/auth/router.test.ts @@ -4,111 +4,111 @@ import mockApi from "../../../../../test-utils/mock-api"; import ResetDBState from "../../../../../test-utils/resets"; t.test("POST /api/v1/auth/login", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should log a user in with right credentials", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "test_zkldi", - password: "password", - captcha: "foo", - }); + t.test("Should log a user in with right credentials", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "test_zkldi", + password: "password", + captcha: "foo", + }); - t.equal(res.status, 200); - t.equal(res.body.success, true); - t.strictSame(res.body.body, { - userID: 1, - }); + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.strictSame(res.body.body, { + userID: 1, + }); - const cookie = res.headers["set-cookie"]; + const cookie = res.headers["set-cookie"]; - const stat = await mockApi.get("/api/v1/status").set("Cookie", cookie); + const stat = await mockApi.get("/api/v1/status").set("Cookie", cookie); - t.ok(stat.body.body.permissions.length > 0); + t.ok(stat.body.body.permissions.length > 0); - t.end(); - }); + t.end(); + }); - t.test("Should return 409 if user already logged in", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "test_zkldi", - password: "password", - captcha: "foo", - }); + t.test("Should return 409 if user already logged in", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "test_zkldi", + password: "password", + captcha: "foo", + }); - const cookie = res.headers["set-cookie"]; + const cookie = res.headers["set-cookie"]; - const res2 = await mockApi - .post("/api/v1/auth/login") - .send({ - username: "test_zkldi", - password: "password", - captcha: "foo", - }) - .set("Cookie", cookie); + const res2 = await mockApi + .post("/api/v1/auth/login") + .send({ + username: "test_zkldi", + password: "password", + captcha: "foo", + }) + .set("Cookie", cookie); - t.equal(res2.status, 409); + t.equal(res2.status, 409); - t.end(); - }); + t.end(); + }); - t.test("Should return 401 if password invalid", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "test_zkldi", - password: "invalid_password", - captcha: "foo", - }); + t.test("Should return 401 if password invalid", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "test_zkldi", + password: "invalid_password", + captcha: "foo", + }); - t.equal(res.status, 401); + t.equal(res.status, 401); - t.end(); - }); + t.end(); + }); - t.test("Should return 404 if user invalid", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "invalid_user", - password: "password", - captcha: "foo", - }); + t.test("Should return 404 if user invalid", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "invalid_user", + password: "password", + captcha: "foo", + }); - t.equal(res.status, 404); + t.equal(res.status, 404); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if no password", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "invalid_user", - captcha: "foo", - }); + t.test("Should return 400 if no password", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "invalid_user", + captcha: "foo", + }); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if no username", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - password: "password", - captcha: "foo", - }); + t.test("Should return 400 if no username", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + password: "password", + captcha: "foo", + }); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if no captcha", async (t) => { - const res = await mockApi.post("/api/v1/auth/login").send({ - password: "password", - username: "test_zkldi", - }); + t.test("Should return 400 if no captcha", async (t) => { + const res = await mockApi.post("/api/v1/auth/login").send({ + password: "password", + username: "test_zkldi", + }); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/auth/router.ts b/server/src/server/router/api/v1/auth/router.ts index ed8d9862b..4eb57108f 100644 --- a/server/src/server/router/api/v1/auth/router.ts +++ b/server/src/server/router/api/v1/auth/router.ts @@ -1,16 +1,16 @@ import { Router } from "express"; import Prudence from "prudence"; import { - AddNewUser, - PasswordCompare, - ReinstateInvite, - ValidatePassword, - ValidateCaptcha, + AddNewUser, + PasswordCompare, + ReinstateInvite, + ValidatePassword, + ValidateCaptcha, } from "./auth"; import { - FormatUserDoc, - GetUserCaseInsensitive, - PRIVATEINFO_GetUserCaseInsensitive, + FormatUserDoc, + GetUserCaseInsensitive, + PRIVATEINFO_GetUserCaseInsensitive, } from "../../../../../utils/user"; import db from "../../../../../external/mongo/db"; import CreateLogCtx from "../../../../../lib/logger/logger"; @@ -28,86 +28,86 @@ const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u; * @name POST /api/v1/auth/login */ router.post( - "/login", - prValidate( - { - username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - password: ValidatePassword, - captcha: "string", - }, - { - username: "Invalid username.", - captcha: "Please fill out the captcha.", - } - ), - async (req, res) => { - if (req.session.tachi?.userID) { - logger.info(`Dual log-in attempted from ${req.session.tachi.userID}`); - return res.status(409).json({ - success: false, - description: `You are already logged in as someone.`, - }); - } + "/login", + prValidate( + { + username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), + password: ValidatePassword, + captcha: "string", + }, + { + username: "Invalid username.", + captcha: "Please fill out the captcha.", + } + ), + async (req, res) => { + if (req.session.tachi?.userID) { + logger.info(`Dual log-in attempted from ${req.session.tachi.userID}`); + return res.status(409).json({ + success: false, + description: `You are already logged in as someone.`, + }); + } - logger.verbose(`Recieved login request with username ${req.body.username} (${req.ip})`); + logger.verbose(`Recieved login request with username ${req.body.username} (${req.ip})`); - /* istanbul ignore next */ - if (process.env.NODE_ENV === "production") { - logger.verbose("Validating captcha..."); - const validCaptcha = await ValidateCaptcha( - req.body.recaptcha, - req.socket.remoteAddress - ); + /* istanbul ignore next */ + if (process.env.NODE_ENV === "production") { + logger.verbose("Validating captcha..."); + const validCaptcha = await ValidateCaptcha( + req.body.recaptcha, + req.socket.remoteAddress + ); - if (!validCaptcha) { - logger.verbose("Captcha failed."); - return res.status(400).json({ - success: false, - description: `Captcha failed.`, - }); - } + if (!validCaptcha) { + logger.verbose("Captcha failed."); + return res.status(400).json({ + success: false, + description: `Captcha failed.`, + }); + } - logger.verbose("Captcha validated!"); - } else { - logger.verbose("Skipped captcha check because not in production."); - } + logger.verbose("Captcha validated!"); + } else { + logger.verbose("Skipped captcha check because not in production."); + } - const requestedUser = await PRIVATEINFO_GetUserCaseInsensitive(req.body.username); + const requestedUser = await PRIVATEINFO_GetUserCaseInsensitive(req.body.username); - if (!requestedUser) { - logger.verbose(`Invalid username for login ${req.body.username}.`); - return res.status(404).json({ - success: false, - description: `This user does not exist.`, - }); - } + if (!requestedUser) { + logger.verbose(`Invalid username for login ${req.body.username}.`); + return res.status(404).json({ + success: false, + description: `This user does not exist.`, + }); + } - const passwordMatch = await PasswordCompare(req.body.password, requestedUser.password); + const passwordMatch = await PasswordCompare(req.body.password, requestedUser.password); - if (!passwordMatch) { - logger.verbose("Invalid password provided."); - return res.status(401).json({ - success: false, - description: `Invalid password.`, - }); - } + if (!passwordMatch) { + logger.verbose("Invalid password provided."); + return res.status(401).json({ + success: false, + description: `Invalid password.`, + }); + } - req.session.tachi = { - userID: requestedUser.id, - }; + req.session.tachi = { + userID: requestedUser.id, + }; - req.session.cookie.maxAge = 3.154e10; // 1 year + req.session.cookie.maxAge = 3.154e10; // 1 year - logger.verbose(`${FormatUserDoc(requestedUser)} Logged in.`); + logger.verbose(`${FormatUserDoc(requestedUser)} Logged in.`); - return res.status(200).json({ - success: true, - description: `Successfully logged in as ${FormatUserDoc(requestedUser)}`, - body: { - userID: requestedUser.id, - }, - }); - } + return res.status(200).json({ + success: true, + description: `Successfully logged in as ${FormatUserDoc(requestedUser)}`, + body: { + userID: requestedUser.id, + }, + }); + } ); /** @@ -115,110 +115,110 @@ router.post( * @name POST /api/v1/auth/register */ router.post( - "/register", - prValidate( - { - username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - password: ValidatePassword, - email: Prudence.regex(LAZY_EMAIL_REGEX), - inviteCode: "string", - captcha: "string", - }, - { - username: - "Usernames must be between 3 and 20 characters long, and can only contain alphanumeric characters!", - email: "Invalid email.", - inviteCode: "Invalid invite code.", - captcha: "Please fill out the captcha.", - } - ), - async (req, res) => { - logger.verbose(`Recieved register request with username ${req.body.username} (${req.ip})`); + "/register", + prValidate( + { + username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), + password: ValidatePassword, + email: Prudence.regex(LAZY_EMAIL_REGEX), + inviteCode: "string", + captcha: "string", + }, + { + username: + "Usernames must be between 3 and 20 characters long, and can only contain alphanumeric characters!", + email: "Invalid email.", + inviteCode: "Invalid invite code.", + captcha: "Please fill out the captcha.", + } + ), + async (req, res) => { + logger.verbose(`Recieved register request with username ${req.body.username} (${req.ip})`); - /* istanbul ignore next */ - if (process.env.NODE_ENV === "production") { - logger.verbose("Validating captcha..."); - const validCaptcha = await ValidateCaptcha( - req.body.recaptcha, - req.socket.remoteAddress - ); + /* istanbul ignore next */ + if (process.env.NODE_ENV === "production") { + logger.verbose("Validating captcha..."); + const validCaptcha = await ValidateCaptcha( + req.body.recaptcha, + req.socket.remoteAddress + ); - if (!validCaptcha) { - logger.verbose("Captcha failed."); - return res.status(400).json({ - success: false, - description: `Captcha failed.`, - }); - } + if (!validCaptcha) { + logger.verbose("Captcha failed."); + return res.status(400).json({ + success: false, + description: `Captcha failed.`, + }); + } - logger.debug("Captcha validated."); - } else { - logger.info("Skipped captcha check because not in production."); - } + logger.debug("Captcha validated."); + } else { + logger.info("Skipped captcha check because not in production."); + } - const existingUser = await GetUserCaseInsensitive(req.body.username); + const existingUser = await GetUserCaseInsensitive(req.body.username); - if (existingUser) { - logger.verbose(`Invalid username ${req.body.username}, already in use.`); - return res.status(409).json({ - success: false, - description: "This username is already in use.", - }); - } + if (existingUser) { + logger.verbose(`Invalid username ${req.body.username}, already in use.`); + return res.status(409).json({ + success: false, + description: "This username is already in use.", + }); + } - const inviteCodeDoc = await db.invites.findOneAndUpdate( - { - code: req.body.inviteCode, - consumed: false, - }, - { - $set: { - consumed: true, - }, - } - ); + const inviteCodeDoc = await db.invites.findOneAndUpdate( + { + code: req.body.inviteCode, + consumed: false, + }, + { + $set: { + consumed: true, + }, + } + ); - if (!inviteCodeDoc) { - logger.info(`Invalid invite code given: ${req.body.inviteCode}.`); - return res.status(401).json({ - success: false, - description: `This invite code is not valid.`, - }); - } + if (!inviteCodeDoc) { + logger.info(`Invalid invite code given: ${req.body.inviteCode}.`); + return res.status(401).json({ + success: false, + description: `This invite code is not valid.`, + }); + } - logger.info(`Consumed invite ${inviteCodeDoc.code}.`); + logger.info(`Consumed invite ${inviteCodeDoc.code}.`); - // if we get to this point, We're good to create the user. + // if we get to this point, We're good to create the user. - try { - const newUser = await AddNewUser(req.body.username, req.body.password, req.body.email); + try { + const newUser = await AddNewUser(req.body.username, req.body.password, req.body.email); - if (!newUser) { - throw new Error("AddNewUser failed to create a user."); - } + if (!newUser) { + throw new Error("AddNewUser failed to create a user."); + } - return res.status(200).json({ - success: true, - description: `Successfully created account ${req.body.username}!`, - body: { - id: newUser.id, - username: newUser.username, - }, - }); - } catch (err) { - logger.error( - `Bailed on user creation ${req.body.username} with invite code ${req.body.inviteCode}.`, - { err } - ); + return res.status(200).json({ + success: true, + description: `Successfully created account ${req.body.username}!`, + body: { + id: newUser.id, + username: newUser.username, + }, + }); + } catch (err) { + logger.error( + `Bailed on user creation ${req.body.username} with invite code ${req.body.inviteCode}.`, + { err } + ); - await ReinstateInvite(inviteCodeDoc); + await ReinstateInvite(inviteCodeDoc); - return res.status(500).json({ - success: false, - description: "An internal server error has occured.", - }); - } - } + return res.status(500).json({ + success: false, + description: "An internal server error has occured.", + }); + } + } ); /** @@ -226,13 +226,13 @@ router.post( * @name POST /api/v1/auth/logout */ router.post("/logout", RequireLoggedInSession, (req, res) => { - req.session.destroy(() => 0); + req.session.destroy(() => 0); - return res.status(200).json({ - success: true, - description: `Logged Out.`, - body: {}, - }); + return res.status(200).json({ + success: true, + description: `Logged Out.`, + body: {}, + }); }); export default router; diff --git a/server/src/server/router/api/v1/import/router.test.ts b/server/src/server/router/api/v1/import/router.test.ts index 56f8b97d2..5c0fd2332 100644 --- a/server/src/server/router/api/v1/import/router.test.ts +++ b/server/src/server/router/api/v1/import/router.test.ts @@ -1,10 +1,10 @@ import t from "tap"; import mockApi from "../../../../../test-utils/mock-api"; import { - GetKTDataBuffer, - LoadKTBlackIIDXData, - TestingIIDXEamusementCSV26, - TestingIIDXEamusementCSV27, + GetKTDataBuffer, + LoadKTBlackIIDXData, + TestingIIDXEamusementCSV26, + TestingIIDXEamusementCSV27, } from "../../../../../test-utils/test-data"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; import { RequireNeutralAuthentication } from "../../../../../test-utils/api-common"; @@ -13,310 +13,310 @@ import ResetDBState, { SetIndexesForDB } from "../../../../../test-utils/resets" import db from "../../../../../external/mongo/db"; t.test("POST /api/v1/import/file", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); + const cookie = await CreateFakeAuthCookie(mockApi); - t.before(SetIndexesForDB); - t.beforeEach(ResetDBState); - - RequireNeutralAuthentication("/api/v1/import/file", "POST"); - - t.test("file/eamusement-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Mini HV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), - "my_csv.csv" - ) - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); + t.before(SetIndexesForDB); + t.beforeEach(ResetDBState); + + RequireNeutralAuthentication("/api/v1/import/file", "POST"); + + t.test("file/eamusement-iidx-csv", (t) => { + t.beforeEach(LoadKTBlackIIDXData); + + t.test("Mini HV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach( + "scoreData", + GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), + "my_csv.csv" + ) + .field("importType", "file/eamusement-iidx-csv") + .field("playtype", "SP"); - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); + t.equal(res.body.success, true, "Should be successful."); + + t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); - t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); + t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); + + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); + + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); + + t.end(); + }); - t.test("Valid Rootage CSV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); + t.test("Valid Rootage CSV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") + .field("importType", "file/eamusement-iidx-csv") + .field("playtype", "SP"); + + t.equal(res.body.success, true, "Should be successful."); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); + + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); + + t.end(); + }); - t.test("Valid Heroic Verse CSV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); + t.test("Valid Heroic Verse CSV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") + .field("importType", "file/eamusement-iidx-csv") + .field("playtype", "SP"); + + t.equal(res.body.success, true, "Should be successful."); + + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.end(); - }); - - // thats right i literally just copied it - t.test("file/pli-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Mini HV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), - "my_csv.csv" - ) - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Rootage CSV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Heroic Verse CSV import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); + + t.end(); + }); + + t.end(); + }); + + // thats right i literally just copied it + t.test("file/pli-iidx-csv", (t) => { + t.beforeEach(LoadKTBlackIIDXData); + + t.test("Mini HV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach( + "scoreData", + GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), + "my_csv.csv" + ) + .field("importType", "file/pli-iidx-csv") + .field("playtype", "SP"); + + t.equal(res.body.success, true, "Should be successful."); + + t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); + + t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); + + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); + + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); + + t.end(); + }); + + t.test("Valid Rootage CSV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") + .field("importType", "file/pli-iidx-csv") + .field("playtype", "SP"); + + t.equal(res.body.success, true, "Should be successful."); + + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); + + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); + + t.end(); + }); + + t.test("Valid Heroic Verse CSV import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") + .field("importType", "file/pli-iidx-csv") + .field("playtype", "SP"); - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.success, true, "Should be successful."); + + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); + + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("file/batch-manual", (t) => { - t.test("Empty import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./batch-manual/empty-file.json"), - "empty-file.json" - ) - .field("importType", "file/batch-manual"); + t.test("file/batch-manual", (t) => { + t.test("Empty import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach( + "scoreData", + GetKTDataBuffer("./batch-manual/empty-file.json"), + "empty-file.json" + ) + .field("importType", "file/batch-manual"); - t.equal(res.body.success, true, "Should be successful."); + t.equal(res.body.success, true, "Should be successful."); - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - t.equal(res.body.body.scoreIDs.length, 0, "Should have 0 successful scores."); + t.equal(res.body.body.scoreIDs.length, 0, "Should have 0 successful scores."); - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); - t.end(); - }); + t.end(); + }); - t.test("Single import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./batch-manual/small-file.json"), - "small-file.json" - ) - .field("importType", "file/batch-manual"); + t.test("Single import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach( + "scoreData", + GetKTDataBuffer("./batch-manual/small-file.json"), + "small-file.json" + ) + .field("importType", "file/batch-manual"); - t.equal(res.body.success, true, "Should be successful."); + t.equal(res.body.success, true, "Should be successful."); - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - t.equal(res.body.body.scoreIDs.length, 1, "Should have 1 successful score."); + t.equal(res.body.body.scoreIDs.length, 1, "Should have 1 successful score."); - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); + const scoreCount = await db.scores.find({ + scoreID: { $in: res.body.body.scoreIDs }, + }); - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); + t.equal( + scoreCount.length, + res.body.body.scoreIDs.length, + "All returned scoreIDs should be inserted to the DB." + ); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("file/mer-iidx", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.test("file/mer-iidx", (t) => { + t.beforeEach(LoadKTBlackIIDXData); - t.test("Example Import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./mer/base.json"), "base.json") - .field("importType", "file/mer-iidx"); + t.test("Example Import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", GetKTDataBuffer("./mer/base.json"), "base.json") + .field("importType", "file/mer-iidx"); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - t.equal(res.body.body.scoreIDs.length, 3, "Should have 3 successful scores."); + t.equal(res.body.body.scoreIDs.length, 3, "Should have 3 successful scores."); - t.end(); - }); + t.end(); + }); - t.test("Example Import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./mer/large.json"), "base.json") - .field("importType", "file/mer-iidx"); + t.test("Example Import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", GetKTDataBuffer("./mer/large.json"), "base.json") + .field("importType", "file/mer-iidx"); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - t.equal(res.body.body.scoreIDs.length, 627, "Should have 627 successful scores."); + t.equal(res.body.body.scoreIDs.length, 627, "Should have 627 successful scores."); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.skip("file/solid-state-squad", (t) => { - t.beforeEach(LoadKTBlackIIDXData); + t.skip("file/solid-state-squad", (t) => { + t.beforeEach(LoadKTBlackIIDXData); - t.test("Large Import", async (t) => { - const res = await mockApi - .post("/api/v1/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./s3/large-example.xml"), "large.xml") - .field("importType", "file/solid-state-squad"); + t.test("Large Import", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", GetKTDataBuffer("./s3/large-example.xml"), "large.xml") + .field("importType", "file/solid-state-squad"); - t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.scoreIDs.length, null, "Should parse N scores."); + t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.body.scoreIDs.length, null, "Should parse N scores."); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/import/router.ts b/server/src/server/router/api/v1/import/router.ts index 2db1ed66f..a3a759e32 100644 --- a/server/src/server/router/api/v1/import/router.ts +++ b/server/src/server/router/api/v1/import/router.ts @@ -23,9 +23,9 @@ const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware( - "scoreData", - SIXTEEN_MEGABTYES, - logger + "scoreData", + SIXTEEN_MEGABTYES, + logger ); /** @@ -33,47 +33,47 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware( * @name POST /api/v1/import/file */ router.post( - "/file", - RequireLoggedInSession, - RequireKamaitachi, // This is only useful for Kamaitachi, so. - ParseMultipartScoredata, - prValidate( - { - importType: Prudence.isIn(fileImportTypes), - }, - {}, - { allowExcessKeys: true } - ), - async (req, res) => { - if (!req.file) { - return res.status(400).json({ - success: false, - description: `No file provided.`, - }); - } + "/file", + RequireLoggedInSession, + RequireKamaitachi, // This is only useful for Kamaitachi, so. + ParseMultipartScoredata, + prValidate( + { + importType: Prudence.isIn(fileImportTypes), + }, + {}, + { allowExcessKeys: true } + ), + async (req, res) => { + if (!req.file) { + return res.status(400).json({ + success: false, + description: `No file provided.`, + }); + } - const importType = req.body.importType as FileUploadImportTypes; + const importType = req.body.importType as FileUploadImportTypes; - const inputParser = (logger: KtLogger) => - ResolveFileUploadData(importType, req.file, req.body, logger); + const inputParser = (logger: KtLogger) => + ResolveFileUploadData(importType, req.file, req.body, logger); - const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID); + const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID); - // The here is deliberate - TS picks the IIDX-CSV generic values - // for this function call because it sees them first - // but that is ABSOLUTELY not what is actually occuring. - // We use this as an override because we know better. - // see: https://www.typescriptlang.org/play?ts=4.3.0-beta#code/GYVwdgxgLglg9mABAQQDwBUB8AKYc4BciAYvhpgJSIDeiAsAFCKID0LiAJnAKYDOivKCGDBGAX0aMYYKNwBOwAIYRuJMlhqNmzAEaK5RdOMkNQkWAkQAlbkLlgAynAC23UnGxVqW7W0QAHOVtuMA5EKAALGH5o8IjVIN4QABsoRDhgARdVaX8QNOlBbkUwjMQ5RVCXH2YYTOwAWUVIgDoKqudPRFREAAYWgFYvGu1mILskWj0DRAAiQTlpAHNZxAkmbXXRkfGQexpEaaIARgAmAGY14wZGZNt0vfdEAF5rWz3HbPdPAG4TZGwcEe+AoPyAA - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - true, - importType, - inputParser - ); + // The here is deliberate - TS picks the IIDX-CSV generic values + // for this function call because it sees them first + // but that is ABSOLUTELY not what is actually occuring. + // We use this as an override because we know better. + // see: https://www.typescriptlang.org/play?ts=4.3.0-beta#code/GYVwdgxgLglg9mABAQQDwBUB8AKYc4BciAYvhpgJSIDeiAsAFCKID0LiAJnAKYDOivKCGDBGAX0aMYYKNwBOwAIYRuJMlhqNmzAEaK5RdOMkNQkWAkQAlbkLlgAynAC23UnGxVqW7W0QAHOVtuMA5EKAALGH5o8IjVIN4QABsoRDhgARdVaX8QNOlBbkUwjMQ5RVCXH2YYTOwAWUVIgDoKqudPRFREAAYWgFYvGu1mILskWj0DRAAiQTlpAHNZxAkmbXXRkfGQexpEaaIARgAmAGY14wZGZNt0vfdEAF5rWz3HbPdPAG4TZGwcEe+AoPyAA + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + true, + importType, + inputParser + ); - return res.status(responseData.statusCode).json(responseData.body); - } + return res.status(responseData.statusCode).json(responseData.body); + } ); /** @@ -87,28 +87,28 @@ router.post( * @param body - Other data passed by the user in the request body. */ export function ResolveFileUploadData( - importType: FileUploadImportTypes, - fileData: Express.Multer.File, - body: Record, - logger: KtLogger + importType: FileUploadImportTypes, + fileData: Express.Multer.File, + body: Record, + logger: KtLogger ) { - switch (importType) { - case "file/eamusement-iidx-csv": - return ParseEamusementIIDXCSV(fileData, body, logger); - case "file/pli-iidx-csv": - return ParsePLIIIDXCSV(fileData, body, logger); - case "file/batch-manual": - return ParseBatchManual(fileData, body, logger); - case "file/solid-state-squad": - return ParseSolidStateXML(fileData, body, logger); - case "file/mer-iidx": - return ParseMerIIDX(fileData, body, logger); - default: - logger.error( - `importType ${importType} made it into ResolveFileUploadData, but should have been rejected by Prudence.` - ); - throw new ScoreImportFatalError(400, `Invalid importType of ${importType}.`); - } + switch (importType) { + case "file/eamusement-iidx-csv": + return ParseEamusementIIDXCSV(fileData, body, logger); + case "file/pli-iidx-csv": + return ParsePLIIIDXCSV(fileData, body, logger); + case "file/batch-manual": + return ParseBatchManual(fileData, body, logger); + case "file/solid-state-squad": + return ParseSolidStateXML(fileData, body, logger); + case "file/mer-iidx": + return ParseMerIIDX(fileData, body, logger); + default: + logger.error( + `importType ${importType} made it into ResolveFileUploadData, but should have been rejected by Prudence.` + ); + throw new ScoreImportFatalError(400, `Invalid importType of ${importType}.`); + } } export default router; diff --git a/server/src/server/router/api/v1/router.test.ts b/server/src/server/router/api/v1/router.test.ts index c5c79b9dc..000349550 100644 --- a/server/src/server/router/api/v1/router.test.ts +++ b/server/src/server/router/api/v1/router.test.ts @@ -9,31 +9,31 @@ t.beforeEach(ClearTestingRateLimitCache); // (which does a server status check) // and then check any of them return 429. t.test("Rate Limiting Test", async (t) => { - const promises = []; + const promises = []; - for (let i = 0; i < 150; i++) { - promises.push(mockApi.get("/api/v1/status")); - } + for (let i = 0; i < 150; i++) { + promises.push(mockApi.get("/api/v1/status")); + } - const res = await Promise.all(promises); + const res = await Promise.all(promises); - const rateLimited = res.filter((e) => e.statusCode === 429); + const rateLimited = res.filter((e) => e.statusCode === 429); - t.ok(rateLimited.length > 0, "Some requests should be rate limited."); + t.ok(rateLimited.length > 0, "Some requests should be rate limited."); - t.end(); + t.end(); }); t.test("404 Handler", async (t) => { - const res = await mockApi.get("/api/v1/invalid_route_that_will_never_exist"); + const res = await mockApi.get("/api/v1/invalid_route_that_will_never_exist"); - t.equal(res.statusCode, 404); - t.strictSame(res.body, { - success: false, - description: "Endpoint Not Found.", - }); + t.equal(res.statusCode, 404); + t.strictSame(res.body, { + success: false, + description: "Endpoint Not Found.", + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/router.ts b/server/src/server/router/api/v1/router.ts index 33c9071ca..70275d235 100644 --- a/server/src/server/router/api/v1/router.ts +++ b/server/src/server/router/api/v1/router.ts @@ -12,10 +12,10 @@ router.use("/import", importRouter); router.use("/users", usersRouter); router.all("*", (req, res) => - res.status(404).json({ - success: false, - description: "Endpoint Not Found.", - }) + res.status(404).json({ + success: false, + description: "Endpoint Not Found.", + }) ); export default router; diff --git a/server/src/server/router/api/v1/status/router.test.ts b/server/src/server/router/api/v1/status/router.test.ts index bb62b2679..335e83bb3 100644 --- a/server/src/server/router/api/v1/status/router.test.ts +++ b/server/src/server/router/api/v1/status/router.test.ts @@ -5,75 +5,75 @@ import t from "tap"; import { CloseAllConnections } from "../../../../../test-utils/close-connections"; t.test("GET /api/v1/status", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); + const cookie = await CreateFakeAuthCookie(mockApi); - t.test("Should return the current time and the server version.", async (t) => { - const res = await mockApi.get("/api/v1/status").set("Cookie", cookie); + t.test("Should return the current time and the server version.", async (t) => { + const res = await mockApi.get("/api/v1/status").set("Cookie", cookie); - t.equal(res.statusCode, 200); - t.equal(res.body.success, true); - t.ok( - Math.abs(Date.now() - res.body.body.serverTime) < 5_000, - "Should be roughly the current time (5 seconds lenience)" - ); - t.equal(res.body.body.version, FormatVersion()); + t.equal(res.statusCode, 200); + t.equal(res.body.success, true); + t.ok( + Math.abs(Date.now() - res.body.body.serverTime) < 5_000, + "Should be roughly the current time (5 seconds lenience)" + ); + t.equal(res.body.body.version, FormatVersion()); - t.end(); - }); + t.end(); + }); - t.test("Should echo the provided echo param.", async (t) => { - const res = await mockApi.get("/api/v1/status?echo=foobar").set("Cookie", cookie); + t.test("Should echo the provided echo param.", async (t) => { + const res = await mockApi.get("/api/v1/status?echo=foobar").set("Cookie", cookie); - t.equal(res.statusCode, 200); - t.equal(res.body.success, true); - t.equal(res.body.body.echo, "foobar"); - t.ok( - Math.abs(Date.now() - res.body.body.serverTime) < 5_000, - "Should be roughly the current time (5 seconds lenience)" - ); - t.equal(res.body.body.version, FormatVersion()); + t.equal(res.statusCode, 200); + t.equal(res.body.success, true); + t.equal(res.body.body.echo, "foobar"); + t.ok( + Math.abs(Date.now() - res.body.body.serverTime) < 5_000, + "Should be roughly the current time (5 seconds lenience)" + ); + t.equal(res.body.body.version, FormatVersion()); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("POST /api/v1/status", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); + const cookie = await CreateFakeAuthCookie(mockApi); - t.test("Should return the current time and the server version.", async (t) => { - const res = await mockApi.post("/api/v1/status").set("Cookie", cookie); + t.test("Should return the current time and the server version.", async (t) => { + const res = await mockApi.post("/api/v1/status").set("Cookie", cookie); - t.equal(res.statusCode, 200); - t.equal(res.body.success, true); - t.ok( - Math.abs(Date.now() - res.body.body.serverTime) < 5_000, - "Should be roughly the current time (5 seconds lenience)" - ); - t.equal(res.body.body.version, FormatVersion()); + t.equal(res.statusCode, 200); + t.equal(res.body.success, true); + t.ok( + Math.abs(Date.now() - res.body.body.serverTime) < 5_000, + "Should be roughly the current time (5 seconds lenience)" + ); + t.equal(res.body.body.version, FormatVersion()); - t.end(); - }); + t.end(); + }); - t.test("Should echo the provided echo param.", async (t) => { - const res = await mockApi.post("/api/v1/status").set("Cookie", cookie).send({ - echo: "foobar", - }); + t.test("Should echo the provided echo param.", async (t) => { + const res = await mockApi.post("/api/v1/status").set("Cookie", cookie).send({ + echo: "foobar", + }); - t.equal(res.statusCode, 200); - t.equal(res.body.success, true); - t.equal(res.body.body.echo, "foobar"); - t.ok( - Math.abs(Date.now() - res.body.body.serverTime) < 5_000, - "Should be roughly the current time (5 seconds lenience)" - ); - t.equal(res.body.body.version, FormatVersion()); + t.equal(res.statusCode, 200); + t.equal(res.body.success, true); + t.equal(res.body.body.echo, "foobar"); + t.ok( + Math.abs(Date.now() - res.body.body.serverTime) < 5_000, + "Should be roughly the current time (5 seconds lenience)" + ); + t.equal(res.body.body.version, FormatVersion()); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/status/router.ts b/server/src/server/router/api/v1/status/router.ts index 21e81694b..979838756 100644 --- a/server/src/server/router/api/v1/status/router.ts +++ b/server/src/server/router/api/v1/status/router.ts @@ -5,45 +5,45 @@ import { FormatVersion } from "../../../../../lib/constants/version"; const router: Router = Router({ mergeParams: true }); router.get("/", (req, res) => { - let echo; - if (req.query.echo && typeof req.query.echo === "string") { - echo = req.query.echo; - } + let echo; + if (req.query.echo && typeof req.query.echo === "string") { + echo = req.query.echo; + } - res.status(200).json({ - success: true, - description: "Status check successful.", - body: { - serverTime: Date.now(), - version: FormatVersion(), - // converts {foo: true, bar: false, baz: true} into [foo, baz] - permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) - .filter((e) => e[1]) - .map((e) => e[0]), - echo, - }, - }); + res.status(200).json({ + success: true, + description: "Status check successful.", + body: { + serverTime: Date.now(), + version: FormatVersion(), + // converts {foo: true, bar: false, baz: true} into [foo, baz] + permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) + .filter((e) => e[1]) + .map((e) => e[0]), + echo, + }, + }); }); router.post("/", (req, res) => { - let echo; - if (req.body.echo && typeof req.body.echo === "string") { - echo = req.body.echo; - } + let echo; + if (req.body.echo && typeof req.body.echo === "string") { + echo = req.body.echo; + } - res.status(200).json({ - success: true, - description: "Status check successful.", - body: { - serverTime: Date.now(), - version: FormatVersion(), - // converts {foo: true, bar: false, baz: true} into [foo, baz] - permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) - .filter((e) => e[1]) - .map((e) => e[0]), - echo, - }, - }); + res.status(200).json({ + success: true, + description: "Status check successful.", + body: { + serverTime: Date.now(), + version: FormatVersion(), + // converts {foo: true, bar: false, baz: true} into [foo, baz] + permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) + .filter((e) => e[1]) + .map((e) => e[0]), + echo, + }, + }); }); export default router; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts index 1c9817390..26a089ec2 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts @@ -5,40 +5,40 @@ import { IsValidGame, IsValidPlaytype } from "../../../../../../../../../utils/m import { AssignToReqTachiData } from "../../../../../../../../../utils/req-tachi-data"; export const CheckUserPlayedGamePlaytype: RequestHandler = async (req, res, next) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TachiData]!.requestedUser!; - if (!IsValidGame(req.params.game)) { - return res.status(400).json({ - success: false, - description: `The game ${req.params.game} is not supported.`, - }); - } + if (!IsValidGame(req.params.game)) { + return res.status(400).json({ + success: false, + description: `The game ${req.params.game} is not supported.`, + }); + } - if (!IsValidPlaytype(req.params.game, req.params.playtype)) { - return res.status(400).json({ - success: false, - description: `The game ${req.params.game} does not have a playtype called ${req.params.playtype}.`, - }); - } + if (!IsValidPlaytype(req.params.game, req.params.playtype)) { + return res.status(400).json({ + success: false, + description: `The game ${req.params.game} does not have a playtype called ${req.params.playtype}.`, + }); + } - const stats = await db["game-stats"].findOne({ - userID: user.id, - game: req.params.game, - playtype: req.params.playtype, - }); + const stats = await db["game-stats"].findOne({ + userID: user.id, + game: req.params.game, + playtype: req.params.playtype, + }); - if (!stats) { - return res.status(404).json({ - success: false, - description: `The user ${user.username} has not played ${req.params.game} (${req.params.playtype})`, - }); - } + if (!stats) { + return res.status(404).json({ + success: false, + description: `The user ${user.username} has not played ${req.params.game} (${req.params.playtype})`, + }); + } - AssignToReqTachiData(req, { - requestedUserGameStats: stats, - game: req.params.game, - playtype: req.params.playtype, - }); + AssignToReqTachiData(req, { + requestedUserGameStats: stats, + game: req.params.game, + playtype: req.params.playtype, + }); - return next(); + return next(); }; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts index c42a48809..06c7c9397 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts @@ -4,632 +4,632 @@ import { CloseAllConnections } from "../../../../../../../../../test-utils/close import mockApi from "../../../../../../../../../test-utils/mock-api"; import ResetDBState, { SetIndexesForDB } from "../../../../../../../../../test-utils/resets"; import { - SessionDocument, - ScoreDocument, - UserGoalDocument, - GoalDocument, - MilestoneDocument, - UserMilestoneDocument, - PBScoreDocument, + SessionDocument, + ScoreDocument, + UserGoalDocument, + GoalDocument, + MilestoneDocument, + UserMilestoneDocument, + PBScoreDocument, } from "tachi-common"; import { - GetKTDataJSON, - LoadKTBlackIIDXData, - Testing511Song, - Testing511SPA, + GetKTDataJSON, + LoadKTBlackIIDXData, + Testing511Song, + Testing511SPA, } from "../../../../../../../../../test-utils/test-data"; t.before(SetIndexesForDB); t.test("GET /api/v1/users/:userID/games/:game/:playtype/", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a users statistics for that game.", async (t) => { - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); + t.test("Should return a users statistics for that game.", async (t) => { + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); - t.hasStrict(res.body, { - success: true, - description: "Retrieved user statistics for test_zkldi (iidx SP)", - body: { - gameStats: { - userID: 1, - game: "iidx", - playtype: "SP", - classes: {}, - ratings: {}, - }, - firstScore: null, - mostRecentScore: null, - totalScores: 1, - rankingData: { - ranking: 1, - outOf: 1, - }, - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved user statistics for test_zkldi (iidx SP)", + body: { + gameStats: { + userID: 1, + game: "iidx", + playtype: "SP", + classes: {}, + ratings: {}, + }, + firstScore: null, + mostRecentScore: null, + totalScores: 1, + rankingData: { + ranking: 1, + outOf: 1, + }, + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should return a users first score if one exists.", async (t) => { - await db.scores.insert([ - { - userID: 1, - timeAchieved: 100, - scoreID: "foo", - game: "iidx", - playtype: "SP", - }, - { - userID: 1, - timeAchieved: 200, - scoreID: "bar", - game: "iidx", - playtype: "SP", - }, - { - userID: 1, - timeAchieved: 300, - scoreID: "baz", - game: "iidx", - playtype: "SP", - }, - ] as ScoreDocument[]); + t.test("Should return a users first score if one exists.", async (t) => { + await db.scores.insert([ + { + userID: 1, + timeAchieved: 100, + scoreID: "foo", + game: "iidx", + playtype: "SP", + }, + { + userID: 1, + timeAchieved: 200, + scoreID: "bar", + game: "iidx", + playtype: "SP", + }, + { + userID: 1, + timeAchieved: 300, + scoreID: "baz", + game: "iidx", + playtype: "SP", + }, + ] as ScoreDocument[]); - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); - t.hasStrict(res.body, { - success: true, - description: "Retrieved user statistics for test_zkldi (iidx SP)", - body: { - gameStats: { - userID: 1, - game: "iidx", - playtype: "SP", - classes: {}, - ratings: {}, - }, - firstScore: { - timeAchieved: 100, - scoreID: "foo", - }, - mostRecentScore: { - timeAchieved: 300, - scoreID: "baz", - }, - totalScores: 4, - rankingData: { - ranking: 1, - outOf: 1, - }, - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved user statistics for test_zkldi (iidx SP)", + body: { + gameStats: { + userID: 1, + game: "iidx", + playtype: "SP", + classes: {}, + ratings: {}, + }, + firstScore: { + timeAchieved: 100, + scoreID: "foo", + }, + mostRecentScore: { + timeAchieved: 300, + scoreID: "baz", + }, + totalScores: 4, + rankingData: { + ranking: 1, + outOf: 1, + }, + }, + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/goals", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test( - "Should return all of a users goals, and only unachieved goals if the argument is set.", - async (t) => { - await db["user-goals"].insert([ - { - goalID: "foo", - userID: 1, - game: "iidx", - playtype: "SP", - achieved: false, - }, - { - goalID: "bar", - userID: 1, - game: "iidx", - playtype: "SP", - achieved: true, - }, - ] as UserGoalDocument[]); + t.test( + "Should return all of a users goals, and only unachieved goals if the argument is set.", + async (t) => { + await db["user-goals"].insert([ + { + goalID: "foo", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: false, + }, + { + goalID: "bar", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: true, + }, + ] as UserGoalDocument[]); - await db.goals.insert([{ goalID: "foo" }, { goalID: "bar" }] as GoalDocument[]); + await db.goals.insert([{ goalID: "foo" }, { goalID: "bar" }] as GoalDocument[]); - const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/goals"); + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/goals"); - t.hasStrict(res.body, { - success: true, - description: "Successfully returned 2 goal(s).", - body: { - userGoals: [ - { - goalID: "foo", - achieved: false, - }, - { - goalID: "bar", - achieved: true, - }, - ], - goals: [{ goalID: "bar" }, { goalID: "foo" }], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Successfully returned 2 goal(s).", + body: { + userGoals: [ + { + goalID: "foo", + achieved: false, + }, + { + goalID: "bar", + achieved: true, + }, + ], + goals: [{ goalID: "bar" }, { goalID: "foo" }], + }, + }); - const resUnachieved = await mockApi.get( - "/api/v1/users/1/games/iidx/SP/goals?unachieved=true" - ); + const resUnachieved = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/goals?unachieved=true" + ); - t.hasStrict(resUnachieved.body, { - success: true, - description: "Successfully returned 1 goal(s).", - body: { - userGoals: [ - { - goalID: "foo", - achieved: false, - }, - ], - goals: [{ goalID: "foo" }], - }, - }); + t.hasStrict(resUnachieved.body, { + success: true, + description: "Successfully returned 1 goal(s).", + body: { + userGoals: [ + { + goalID: "foo", + achieved: false, + }, + ], + goals: [{ goalID: "foo" }], + }, + }); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/milestones", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test( - "Should return all of a users milestones, and only unachieved milestones if the argument is set.", - async (t) => { - await db["user-milestones"].insert([ - { - milestoneID: "foo", - userID: 1, - game: "iidx", - playtype: "SP", - achieved: false, - }, - { - milestoneID: "bar", - userID: 1, - game: "iidx", - playtype: "SP", - achieved: true, - }, - ] as UserMilestoneDocument[]); + t.test( + "Should return all of a users milestones, and only unachieved milestones if the argument is set.", + async (t) => { + await db["user-milestones"].insert([ + { + milestoneID: "foo", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: false, + }, + { + milestoneID: "bar", + userID: 1, + game: "iidx", + playtype: "SP", + achieved: true, + }, + ] as UserMilestoneDocument[]); - await db.milestones.insert([ - { milestoneID: "foo" }, - { milestoneID: "bar" }, - ] as MilestoneDocument[]); + await db.milestones.insert([ + { milestoneID: "foo" }, + { milestoneID: "bar" }, + ] as MilestoneDocument[]); - const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/milestones"); + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/milestones"); - t.hasStrict(res.body, { - success: true, - description: "Successfully returned 2 milestone(s).", - body: { - userMilestones: [ - { - milestoneID: "foo", - achieved: false, - }, - { - milestoneID: "bar", - achieved: true, - }, - ], - milestones: [{ milestoneID: "bar" }, { milestoneID: "foo" }], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Successfully returned 2 milestone(s).", + body: { + userMilestones: [ + { + milestoneID: "foo", + achieved: false, + }, + { + milestoneID: "bar", + achieved: true, + }, + ], + milestones: [{ milestoneID: "bar" }, { milestoneID: "foo" }], + }, + }); - const resUnachieved = await mockApi.get( - "/api/v1/users/1/games/iidx/SP/milestones?unachieved=true" - ); + const resUnachieved = await mockApi.get( + "/api/v1/users/1/games/iidx/SP/milestones?unachieved=true" + ); - t.hasStrict(resUnachieved.body, { - success: true, - description: "Successfully returned 1 milestone(s).", - body: { - userMilestones: [ - { - milestoneID: "foo", - achieved: false, - }, - ], - milestones: [{ milestoneID: "foo" }], - }, - }); + t.hasStrict(resUnachieved.body, { + success: true, + description: "Successfully returned 1 milestone(s).", + body: { + userMilestones: [ + { + milestoneID: "foo", + achieved: false, + }, + ], + milestones: [{ milestoneID: "foo" }], + }, + }); - t.end(); - } - ); + t.end(); + } + ); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/recent", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a users 100 most recent scores.", async (t) => { - const mockScores: ScoreDocument[] = []; + t.test("Should return a users 100 most recent scores.", async (t) => { + const mockScores: ScoreDocument[] = []; - for (let i = 0; i < 200; i++) { - mockScores.push({ - userID: 1, - game: "iidx", - playtype: "SP", - timeAchieved: i * 100, - scoreID: `scoreID_${i}`, - chartID: Testing511SPA.chartID, - songID: Testing511Song.id, - } as ScoreDocument); - } + for (let i = 0; i < 200; i++) { + mockScores.push({ + userID: 1, + game: "iidx", + playtype: "SP", + timeAchieved: i * 100, + scoreID: `scoreID_${i}`, + chartID: Testing511SPA.chartID, + songID: Testing511Song.id, + } as ScoreDocument); + } - await db.scores.insert(mockScores); + await db.scores.insert(mockScores); - for (const sc of mockScores) { - delete sc._id; // lol - } + for (const sc of mockScores) { + delete sc._id; // lol + } - const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/scores/recent"); + const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/scores/recent"); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 100 scores.", - body: { - scores: mockScores.slice(100).reverse(), - songs: [Testing511Song], - charts: [Testing511SPA], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 100 scores.", + body: { + scores: mockScores.slice(100).reverse(), + songs: [Testing511Song], + charts: [Testing511SPA], + }, + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return a users best 100 personal bests.", async (t) => { - const mockPBs: PBScoreDocument[] = []; + t.test("Should return a users best 100 personal bests.", async (t) => { + const mockPBs: PBScoreDocument[] = []; - for (let i = 0; i < 200; i++) { - mockPBs.push({ - userID: 1, - game: "iidx", - playtype: "SP", - isPrimary: true, - chartID: i.toString(), // hack to generate some random chartIDs - songID: Testing511Song.id, - calculatedData: { - ktRating: i, - }, - } as PBScoreDocument); - } + for (let i = 0; i < 200; i++) { + mockPBs.push({ + userID: 1, + game: "iidx", + playtype: "SP", + isPrimary: true, + chartID: i.toString(), // hack to generate some random chartIDs + songID: Testing511Song.id, + calculatedData: { + ktRating: i, + }, + } as PBScoreDocument); + } - await db["personal-bests"].insert(mockPBs); + await db["personal-bests"].insert(mockPBs); - for (const sc of mockPBs) { - delete sc._id; // lol - } + for (const sc of mockPBs) { + delete sc._id; // lol + } - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs/best"); + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs/best"); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 100 personal bests.", - body: { - scores: mockPBs.slice(100).reverse(), - songs: [Testing511Song], - charts: [], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 100 personal bests.", + body: { + scores: mockPBs.slice(100).reverse(), + songs: [Testing511Song], + charts: [], + }, + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(ResetDBState); + t.beforeEach(LoadKTBlackIIDXData); - t.test("Should return 400 if no search param is given", async (t) => { - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs"); + t.test("Should return 400 if no search param is given", async (t) => { + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs"); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if invalid search param is given", async (t) => { - const res = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=foo&search=bar" - ); + t.test("Should return 400 if invalid search param is given", async (t) => { + const res = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=foo&search=bar" + ); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - const res2 = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/pbs?search[$where]=process.exit(1)" - ); + const res2 = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/pbs?search[$where]=process.exit(1)" + ); - t.equal(res2.statusCode, 400); - t.equal(res2.body.success, false); + t.equal(res2.statusCode, 400); + t.equal(res2.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should search a user's personal bests.", async (t) => { - const mockPBs: PBScoreDocument[] = []; + t.test("Should search a user's personal bests.", async (t) => { + const mockPBs: PBScoreDocument[] = []; - const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); + const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); - for (let i = 0; i < 200; i++) { - mockPBs.push({ - userID: 1, - game: "iidx", - playtype: "SP", - isPrimary: true, - chartID: charts[i].chartID, - songID: charts[i].songID, - calculatedData: { - ktRating: i, - }, - } as PBScoreDocument); - } + for (let i = 0; i < 200; i++) { + mockPBs.push({ + userID: 1, + game: "iidx", + playtype: "SP", + isPrimary: true, + chartID: charts[i].chartID, + songID: charts[i].songID, + calculatedData: { + ktRating: i, + }, + } as PBScoreDocument); + } - await db["personal-bests"].insert(mockPBs); + await db["personal-bests"].insert(mockPBs); - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=5.1.1."); + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=5.1.1."); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 2 personal bests.", - body: { - pbs: [ - { - chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", - }, - { - chartID: "c641238220d73faf82659513ba03bde71b0b45f0", - }, - ], - songs: [ - { - title: "5.1.1.", - }, - ], - charts: [ - { - chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", - }, - { - chartID: "c641238220d73faf82659513ba03bde71b0b45f0", - }, - ], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 2 personal bests.", + body: { + pbs: [ + { + chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", + }, + { + chartID: "c641238220d73faf82659513ba03bde71b0b45f0", + }, + ], + songs: [ + { + title: "5.1.1.", + }, + ], + charts: [ + { + chartID: "c2311194e3897ddb5745b1760d2c0141f933e683", + }, + { + chartID: "c641238220d73faf82659513ba03bde71b0b45f0", + }, + ], + }, + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(ResetDBState); + t.beforeEach(LoadKTBlackIIDXData); - t.test("Should return 400 if no search param is given", async (t) => { - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/scores"); + t.test("Should return 400 if no search param is given", async (t) => { + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/scores"); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if invalid search param is given", async (t) => { - const res = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/scores?search=foo&search=bar" - ); + t.test("Should return 400 if invalid search param is given", async (t) => { + const res = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/scores?search=foo&search=bar" + ); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - // evil eval attempts - const res2 = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/scores?search[$where]=process.exit(1)" - ); + // evil eval attempts + const res2 = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/scores?search[$where]=process.exit(1)" + ); - t.equal(res2.statusCode, 400); - t.equal(res2.body.success, false); + t.equal(res2.statusCode, 400); + t.equal(res2.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should search a user's scores.", async (t) => { - const mockScores: ScoreDocument[] = []; + t.test("Should search a user's scores.", async (t) => { + const mockScores: ScoreDocument[] = []; - const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); + const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); - for (let i = 0; i < 200; i++) { - mockScores.push({ - scoreID: i.toString(), - userID: 1, - game: "iidx", - playtype: "SP", - isPrimary: true, - chartID: charts[i].chartID, - songID: charts[i].songID, - calculatedData: { - ktRating: i, - }, - } as ScoreDocument); - } + for (let i = 0; i < 200; i++) { + mockScores.push({ + scoreID: i.toString(), + userID: 1, + game: "iidx", + playtype: "SP", + isPrimary: true, + chartID: charts[i].chartID, + songID: charts[i].songID, + calculatedData: { + ktRating: i, + }, + } as ScoreDocument); + } - await db.scores.insert(mockScores); + await db.scores.insert(mockScores); - const res = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/scores?search=5.1.1." - ); + const res = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/scores?search=5.1.1." + ); - t.equal(res.body.body.scores.length, 3); - t.equal(res.body.body.songs.length, 1); - t.equal(res.body.body.charts.length, 2); + t.equal(res.body.body.scores.length, 3); + t.equal(res.body.body.songs.length, 1); + t.equal(res.body.body.charts.length, 2); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 3 scores.", - body: { - scores: [ - { - songID: 1, - }, - { - songID: 1, - }, - { - songID: 1, - }, - ], - songs: [ - { - title: "5.1.1.", - }, - ], - charts: [ - { - songID: 1, - }, - { - songID: 1, - }, - ], - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 3 scores.", + body: { + scores: [ + { + songID: 1, + }, + { + songID: 1, + }, + { + songID: 1, + }, + ], + songs: [ + { + title: "5.1.1.", + }, + ], + charts: [ + { + songID: 1, + }, + { + songID: 1, + }, + ], + }, + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(ResetDBState); + t.beforeEach(LoadKTBlackIIDXData); - t.test("Should return 400 if no search param is given", async (t) => { - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions"); + t.test("Should return 400 if no search param is given", async (t) => { + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions"); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should return 400 if invalid search param is given", async (t) => { - const res = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=foo&search=bar" - ); + t.test("Should return 400 if invalid search param is given", async (t) => { + const res = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=foo&search=bar" + ); - t.equal(res.statusCode, 400); - t.equal(res.body.success, false); + t.equal(res.statusCode, 400); + t.equal(res.body.success, false); - // evil eval attempts - const res2 = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search[$where]=process.exit(1)" - ); + // evil eval attempts + const res2 = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search[$where]=process.exit(1)" + ); - t.equal(res2.statusCode, 400); - t.equal(res2.body.success, false); + t.equal(res2.statusCode, 400); + t.equal(res2.body.success, false); - t.end(); - }); + t.end(); + }); - t.test("Should search a user's sessions.", async (t) => { - await db.sessions.insert( - ["Epic Session", "Session Of Epic", "Epic Gaming", "something else", "bad session"].map( - (e) => ({ - userID: 1, - game: "iidx", - playtype: "SP", - name: e, - desc: "something", - sessionID: e, // hack to avoid db nonsense - }) - ) as SessionDocument[] - ); + t.test("Should search a user's sessions.", async (t) => { + await db.sessions.insert( + ["Epic Session", "Session Of Epic", "Epic Gaming", "something else", "bad session"].map( + (e) => ({ + userID: 1, + game: "iidx", + playtype: "SP", + name: e, + desc: "something", + sessionID: e, // hack to avoid db nonsense + }) + ) as SessionDocument[] + ); - const res = await mockApi.get( - "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=Epic" - ); + const res = await mockApi.get( + "/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=Epic" + ); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 3 sessions.", - body: [], - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 3 sessions.", + body: [], + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(LoadKTBlackIIDXData); + t.beforeEach(ResetDBState); + t.beforeEach(LoadKTBlackIIDXData); - t.test("Should return a user's best 100 sessions.", async (t) => { - const sessions: SessionDocument[] = []; + t.test("Should return a user's best 100 sessions.", async (t) => { + const sessions: SessionDocument[] = []; - for (let i = 0; i < 200; i++) { - sessions.push({ - sessionID: i.toString(), - game: "iidx", - playtype: "SP", - userID: 1, - calculatedData: { - ktRating: i, - }, - } as SessionDocument); - } + for (let i = 0; i < 200; i++) { + sessions.push({ + sessionID: i.toString(), + game: "iidx", + playtype: "SP", + userID: 1, + calculatedData: { + ktRating: i, + }, + } as SessionDocument); + } - await db.sessions.remove({}); - await db.sessions.insert(sessions); + await db.sessions.remove({}); + await db.sessions.insert(sessions); - const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions/best"); + const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions/best"); - t.hasStrict(res.body, { - success: true, - description: "Retrieved 100 sessions.", - }); + t.hasStrict(res.body, { + success: true, + description: "Retrieved 100 sessions.", + }); - t.strictSame( - res.body.body.map((e: SessionDocument) => e.sessionID), - sessions - .slice(100) - .reverse() - .map((e) => e.sessionID) - ); + t.strictSame( + res.body.body.map((e: SessionDocument) => e.sessionID), + sessions + .slice(100) + .reverse() + .map((e) => e.sessionID) + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts index 8354ff783..eea3d7194 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts @@ -3,16 +3,16 @@ import db from "../../../../../../../../../external/mongo/db"; import { SYMBOL_TachiData } from "../../../../../../../../../lib/constants/tachi"; import { GetRelevantSongsAndCharts } from "../../../../../../../../../utils/db"; import { - GetDefaultScoreRatingAlg, - GetDefaultSessionRatingAlg, - GetUsersRanking, + GetDefaultScoreRatingAlg, + GetDefaultSessionRatingAlg, + GetUsersRanking, } from "../../../../../../../../../utils/user"; import { CheckUserPlayedGamePlaytype } from "./middleware"; import { FilterQuery } from "mongodb"; import { UserGoalDocument, UserMilestoneDocument } from "tachi-common"; import { - SearchGameSongsAndCharts, - SearchSessions, + SearchGameSongsAndCharts, + SearchSessions, } from "../../../../../../../../../lib/search/search"; import { FilterChartsAndSongs } from "../../../../../../../../../utils/scores"; @@ -25,57 +25,57 @@ router.use(CheckUserPlayedGamePlaytype); * @name GET /api/v1/users/:userID/games/:game/:playtype */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const stats = req[SYMBOL_TachiData]!.requestedUserGameStats!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const stats = req[SYMBOL_TachiData]!.requestedUserGameStats!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const [totalScores, firstScore, mostRecentScore, rankingData] = await Promise.all([ - db.scores.count({ - userID: user.id, - game, - playtype, - }), - db.scores.findOne( - { - userID: user.id, - game, - playtype, - timeAchieved: { $ne: null }, - }, - { - sort: { - timeAchieved: 1, - }, - } - ), - db.scores.findOne( - { - userID: user.id, - game, - playtype, - timeAchieved: { $ne: null }, - }, - { - sort: { - timeAchieved: -1, - }, - } - ), - GetUsersRanking(stats), - ]); + const [totalScores, firstScore, mostRecentScore, rankingData] = await Promise.all([ + db.scores.count({ + userID: user.id, + game, + playtype, + }), + db.scores.findOne( + { + userID: user.id, + game, + playtype, + timeAchieved: { $ne: null }, + }, + { + sort: { + timeAchieved: 1, + }, + } + ), + db.scores.findOne( + { + userID: user.id, + game, + playtype, + timeAchieved: { $ne: null }, + }, + { + sort: { + timeAchieved: -1, + }, + } + ), + GetUsersRanking(stats), + ]); - return res.status(200).json({ - success: true, - description: `Retrieved user statistics for ${user.username} (${game} ${playtype})`, - body: { - gameStats: stats, - firstScore, - mostRecentScore, - totalScores, - rankingData, - }, - }); + return res.status(200).json({ + success: true, + description: `Retrieved user statistics for ${user.username} (${game} ${playtype})`, + body: { + gameStats: stats, + firstScore, + mostRecentScore, + totalScores, + rankingData, + }, + }); }); /** @@ -85,34 +85,34 @@ router.get("/", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/goals */ router.get("/goals", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const query: FilterQuery = { - userID: user.id, - game, - playtype, - }; + const query: FilterQuery = { + userID: user.id, + game, + playtype, + }; - if (req.query.unachieved) { - query.achieved = false; - } + if (req.query.unachieved) { + query.achieved = false; + } - const userGoals = await db["user-goals"].find(query); + const userGoals = await db["user-goals"].find(query); - const goals = await db.goals.find({ - goalID: { $in: userGoals.map((e) => e.goalID) }, - }); + const goals = await db.goals.find({ + goalID: { $in: userGoals.map((e) => e.goalID) }, + }); - return res.status(200).json({ - success: true, - description: `Successfully returned ${userGoals.length} goal(s).`, - body: { - userGoals, - goals, - }, - }); + return res.status(200).json({ + success: true, + description: `Successfully returned ${userGoals.length} goal(s).`, + body: { + userGoals, + goals, + }, + }); }); /** @@ -122,34 +122,34 @@ router.get("/goals", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/milestones */ router.get("/milestones", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const query: FilterQuery = { - userID: user.id, - game, - playtype, - }; + const query: FilterQuery = { + userID: user.id, + game, + playtype, + }; - if (req.query.unachieved) { - query.achieved = false; - } + if (req.query.unachieved) { + query.achieved = false; + } - const userMilestones = await db["user-milestones"].find(query); + const userMilestones = await db["user-milestones"].find(query); - const milestones = await db.milestones.find({ - milestoneID: { $in: userMilestones.map((e) => e.milestoneID) }, - }); + const milestones = await db.milestones.find({ + milestoneID: { $in: userMilestones.map((e) => e.milestoneID) }, + }); - return res.status(200).json({ - success: true, - description: `Successfully returned ${userMilestones.length} milestone(s).`, - body: { - userMilestones, - milestones, - }, - }); + return res.status(200).json({ + success: true, + description: `Successfully returned ${userMilestones.length} milestone(s).`, + body: { + userMilestones, + milestones, + }, + }); }); /** @@ -158,47 +158,47 @@ router.get("/milestones", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/scores */ router.get("/scores", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - if (typeof req.query.search !== "string") { - return res.status(400).json({ - success: false, - description: `Invalid value of ${req.query.search} for search parameter.`, - }); - } + if (typeof req.query.search !== "string") { + return res.status(400).json({ + success: false, + description: `Invalid value of ${req.query.search} for search parameter.`, + }); + } - const { songs: allSongs, charts: allCharts } = await SearchGameSongsAndCharts( - game, - req.query.search, - playtype - ); + const { songs: allSongs, charts: allCharts } = await SearchGameSongsAndCharts( + game, + req.query.search, + playtype + ); - const scores = await db.scores.find( - { - chartID: { $in: allCharts.map((e) => e.chartID) }, - userID: user.id, - }, - { - sort: { - timeAchieved: -1, - }, - limit: 30, - } - ); + const scores = await db.scores.find( + { + chartID: { $in: allCharts.map((e) => e.chartID) }, + userID: user.id, + }, + { + sort: { + timeAchieved: -1, + }, + limit: 30, + } + ); - const { songs, charts } = FilterChartsAndSongs(scores, allCharts, allSongs); + const { songs, charts } = FilterChartsAndSongs(scores, allCharts, allSongs); - return res.status(200).json({ - success: true, - description: `Retrieved ${scores.length} scores.`, - body: { - scores, - songs, - charts, - }, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${scores.length} scores.`, + body: { + scores, + songs, + charts, + }, + }); }); /** @@ -207,35 +207,35 @@ router.get("/scores", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/scores/recent */ router.get("/scores/recent", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const recentScores = await db.scores.find( - { - userID: user.id, - game, - playtype, - }, - { - limit: 100, - sort: { - timeAchieved: -1, - }, - } - ); + const recentScores = await db.scores.find( + { + userID: user.id, + game, + playtype, + }, + { + limit: 100, + sort: { + timeAchieved: -1, + }, + } + ); - const { songs, charts } = await GetRelevantSongsAndCharts(recentScores, game); + const { songs, charts } = await GetRelevantSongsAndCharts(recentScores, game); - return res.status(200).json({ - success: true, - description: `Retrieved ${recentScores.length} scores.`, - body: { - scores: recentScores, - songs, - charts, - }, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${recentScores.length} scores.`, + body: { + scores: recentScores, + songs, + charts, + }, + }); }); /** @@ -244,47 +244,47 @@ router.get("/scores/recent", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/scores */ router.get("/pbs", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - if (typeof req.query.search !== "string") { - return res.status(400).json({ - success: false, - description: `Invalid value of ${req.query.search} for search parameter.`, - }); - } + if (typeof req.query.search !== "string") { + return res.status(400).json({ + success: false, + description: `Invalid value of ${req.query.search} for search parameter.`, + }); + } - const { songs: allSongs, charts: allCharts } = await SearchGameSongsAndCharts( - game, - req.query.search, - playtype - ); + const { songs: allSongs, charts: allCharts } = await SearchGameSongsAndCharts( + game, + req.query.search, + playtype + ); - const pbs = await db["personal-bests"].find( - { - chartID: { $in: allCharts.map((e) => e.chartID) }, - userID: user.id, - }, - { - sort: { - timeAchieved: -1, - }, - limit: 30, - } - ); + const pbs = await db["personal-bests"].find( + { + chartID: { $in: allCharts.map((e) => e.chartID) }, + userID: user.id, + }, + { + sort: { + timeAchieved: -1, + }, + limit: 30, + } + ); - const { songs, charts } = FilterChartsAndSongs(pbs, allCharts, allSongs); + const { songs, charts } = FilterChartsAndSongs(pbs, allCharts, allSongs); - return res.status(200).json({ - success: true, - description: `Retrieved ${pbs.length} personal bests.`, - body: { - pbs, - songs, - charts, - }, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${pbs.length} personal bests.`, + body: { + pbs, + songs, + charts, + }, + }); }); /** @@ -295,36 +295,36 @@ router.get("/pbs", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best */ router.get("/pbs/best", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const pbs = await db["personal-bests"].find( - { - userID: user.id, - game, - playtype, - isPrimary: true, - }, - { - limit: 100, - sort: { - [`calculatedData.${GetDefaultScoreRatingAlg(game, playtype)}`]: -1, - }, - } - ); + const pbs = await db["personal-bests"].find( + { + userID: user.id, + game, + playtype, + isPrimary: true, + }, + { + limit: 100, + sort: { + [`calculatedData.${GetDefaultScoreRatingAlg(game, playtype)}`]: -1, + }, + } + ); - const { songs, charts } = await GetRelevantSongsAndCharts(pbs, game); + const { songs, charts } = await GetRelevantSongsAndCharts(pbs, game); - return res.status(200).json({ - success: true, - description: `Retrieved ${pbs.length} personal bests.`, - body: { - scores: pbs, - songs, - charts, - }, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${pbs.length} personal bests.`, + body: { + scores: pbs, + songs, + charts, + }, + }); }); /** @@ -333,24 +333,24 @@ router.get("/pbs/best", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/sessions */ router.get("/sessions", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - if (typeof req.query.search !== "string") { - return res.status(400).json({ - success: false, - description: `Invalid value of ${req.query.search} for search parameter.`, - }); - } + if (typeof req.query.search !== "string") { + return res.status(400).json({ + success: false, + description: `Invalid value of ${req.query.search} for search parameter.`, + }); + } - const sessions = await SearchSessions(req.query.search, game, playtype, user.id, 100); + const sessions = await SearchSessions(req.query.search, game, playtype, user.id, 100); - return res.status(200).json({ - success: true, - description: `Retrieved ${sessions.length} sessions.`, - body: sessions, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${sessions.length} sessions.`, + body: sessions, + }); }); /** @@ -362,29 +362,29 @@ router.get("/sessions", async (req, res) => { * @name GET /api/v1/users/:userID/games/:game/:playtype/sessions/best */ router.get("/sessions/best", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const game = req[SYMBOL_TachiData]!.game!; - const playtype = req[SYMBOL_TachiData]!.playtype!; + const user = req[SYMBOL_TachiData]!.requestedUser!; + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; - const sessions = await db.sessions.find( - { - userID: user.id, - game, - playtype, - }, - { - limit: 100, - sort: { - [`calculatedData.${GetDefaultSessionRatingAlg(game, playtype)}`]: -1, - }, - } - ); + const sessions = await db.sessions.find( + { + userID: user.id, + game, + playtype, + }, + { + limit: 100, + sort: { + [`calculatedData.${GetDefaultSessionRatingAlg(game, playtype)}`]: -1, + }, + } + ); - return res.status(200).json({ - success: true, - description: `Retrieved ${sessions.length} sessions.`, - body: sessions, - }); + return res.status(200).json({ + success: true, + description: `Retrieved ${sessions.length} sessions.`, + body: sessions, + }); }); export default router; diff --git a/server/src/server/router/api/v1/users/_userID/middleware.ts b/server/src/server/router/api/v1/users/_userID/middleware.ts index 75ccc73c9..734380036 100644 --- a/server/src/server/router/api/v1/users/_userID/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/middleware.ts @@ -3,23 +3,23 @@ import { AssignToReqTachiData } from "../../../../../../utils/req-tachi-data"; import { ResolveUser } from "../../../../../../utils/user"; export const GetUserFromParam: RequestHandler = async (req, res, next) => { - if (!req.params.userID) { - return res.status(400).json({ - success: false, - description: "No userID given.", - }); - } + if (!req.params.userID) { + return res.status(400).json({ + success: false, + description: "No userID given.", + }); + } - const user = await ResolveUser(req.params.userID); + const user = await ResolveUser(req.params.userID); - if (!user) { - return res.status(404).json({ - success: false, - description: `The user ${req.params.userID} does not exist.`, - }); - } + if (!user) { + return res.status(404).json({ + success: false, + description: `The user ${req.params.userID} does not exist.`, + }); + } - AssignToReqTachiData(req, { requestedUser: user }); + AssignToReqTachiData(req, { requestedUser: user }); - return next(); + return next(); }; diff --git a/server/src/server/router/api/v1/users/_userID/router.test.ts b/server/src/server/router/api/v1/users/_userID/router.test.ts index c976fd6e6..f3ccd8fd7 100644 --- a/server/src/server/router/api/v1/users/_userID/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/router.test.ts @@ -6,130 +6,130 @@ import ResetDBState from "../../../../../../test-utils/resets"; import { UserGameStats } from "tachi-common"; t.test("GET /api/v1/users/:userID", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should retrieve a user on their ID.", async (t) => { - const res = await mockApi.get("/api/v1/users/1"); + t.test("Should retrieve a user on their ID.", async (t) => { + const res = await mockApi.get("/api/v1/users/1"); - t.hasStrict(res.body, { - success: true, - description: "Found user test_zkldi.", - body: { - id: 1, - username: "test_zkldi", - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Found user test_zkldi.", + body: { + id: 1, + username: "test_zkldi", + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should retrieve a user on their name.", async (t) => { - const res = await mockApi.get("/api/v1/users/test_zkldi"); + t.test("Should retrieve a user on their name.", async (t) => { + const res = await mockApi.get("/api/v1/users/test_zkldi"); - t.hasStrict(res.body, { - success: true, - description: "Found user test_zkldi.", - body: { - id: 1, - username: "test_zkldi", - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Found user test_zkldi.", + body: { + id: 1, + username: "test_zkldi", + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should retrieve a user on their name case insensitively.", async (t) => { - const res = await mockApi.get("/api/v1/users/TeSt_ZklDI"); + t.test("Should retrieve a user on their name case insensitively.", async (t) => { + const res = await mockApi.get("/api/v1/users/TeSt_ZklDI"); - t.hasStrict(res.body, { - success: true, - description: "Found user test_zkldi.", - body: { - id: 1, - username: "test_zkldi", - }, - }); + t.hasStrict(res.body, { + success: true, + description: "Found user test_zkldi.", + body: { + id: 1, + username: "test_zkldi", + }, + }); - t.end(); - }); + t.end(); + }); - t.test("Should return 404 on users that don't exist.", async (t) => { - const res = await mockApi.get("/api/v1/users/someguy"); + t.test("Should return 404 on users that don't exist.", async (t) => { + const res = await mockApi.get("/api/v1/users/someguy"); - t.hasStrict(res.body, { - success: false, - description: "The user someguy does not exist.", - }); + t.hasStrict(res.body, { + success: false, + description: "The user someguy does not exist.", + }); - t.equal(res.statusCode, 404); + t.equal(res.statusCode, 404); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /api/v1/users/:userID/game-stats", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return all of a user's game stats.", async (t) => { - await db["game-stats"].remove({}); + t.test("Should return all of a user's game stats.", async (t) => { + await db["game-stats"].remove({}); - const stats: UserGameStats[] = [ - { - userID: 1, - game: "iidx", - playtype: "SP", - classes: {}, - ratings: { - ktRating: 12, - }, - }, - { - userID: 1, - game: "iidx", - playtype: "DP", - classes: {}, - ratings: { - ktRating: 11, - }, - }, - { - userID: 1, - game: "gitadora", - playtype: "Dora", - classes: {}, - ratings: { - skill: 4843, - }, - }, - ]; + const stats: UserGameStats[] = [ + { + userID: 1, + game: "iidx", + playtype: "SP", + classes: {}, + ratings: { + ktRating: 12, + }, + }, + { + userID: 1, + game: "iidx", + playtype: "DP", + classes: {}, + ratings: { + ktRating: 11, + }, + }, + { + userID: 1, + game: "gitadora", + playtype: "Dora", + classes: {}, + ratings: { + skill: 4843, + }, + }, + ]; - await db["game-stats"].insert(stats); + await db["game-stats"].insert(stats); - for (const s of stats) { - delete s._id; - } + for (const s of stats) { + delete s._id; + } - const res = await mockApi.get("/api/v1/users/test_zkldi/game-stats"); + const res = await mockApi.get("/api/v1/users/test_zkldi/game-stats"); - t.hasStrict(res.body, { - success: true, - description: "Returned 3 stats objects.", - }); + t.hasStrict(res.body, { + success: true, + description: "Returned 3 stats objects.", + }); - const returns = res.body.body.map((e: UserGameStats) => `${e.game}-${e.playtype}`); + const returns = res.body.body.map((e: UserGameStats) => `${e.game}-${e.playtype}`); - // amusing small hacks - t.equal(returns.length, 3); - t.ok(returns.includes("iidx-SP")); - t.ok(returns.includes("iidx-DP")); - t.ok(returns.includes("gitadora-Dora")); + // amusing small hacks + t.equal(returns.length, 3); + t.ok(returns.includes("iidx-SP")); + t.ok(returns.includes("iidx-DP")); + t.ok(returns.includes("gitadora-Dora")); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/users/_userID/router.ts b/server/src/server/router/api/v1/users/_userID/router.ts index 046eb5634..e7f16e0ae 100644 --- a/server/src/server/router/api/v1/users/_userID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/router.ts @@ -13,13 +13,13 @@ router.use(GetUserFromParam); * @name GET /api/v1/users/:userID */ router.get("/", (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TachiData]!.requestedUser!; - return res.status(200).json({ - success: true, - description: `Found user ${user.username}.`, - body: user, - }); + return res.status(200).json({ + success: true, + description: `Found user ${user.username}.`, + body: user, + }); }); /** @@ -29,16 +29,16 @@ router.get("/", (req, res) => { * @name GET /api/v1/users/:userID/game-stats */ router.get("/game-stats", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TachiData]!.requestedUser!; - // a user has played a game if and only if they have stats for it. - const stats = await db["game-stats"].find({ userID: user.id }); + // a user has played a game if and only if they have stats for it. + const stats = await db["game-stats"].find({ userID: user.id }); - return res.status(200).json({ - success: true, - description: `Returned ${stats.length} stats objects.`, - body: stats, - }); + return res.status(200).json({ + success: true, + description: `Returned ${stats.length} stats objects.`, + body: stats, + }); }); router.use("/games/:game/:playtype", gamePTRouter); diff --git a/server/src/server/router/api/v1/users/router.ts b/server/src/server/router/api/v1/users/router.ts index 08e12b1cf..faa0399ad 100644 --- a/server/src/server/router/api/v1/users/router.ts +++ b/server/src/server/router/api/v1/users/router.ts @@ -11,7 +11,7 @@ const router: Router = Router({ mergeParams: true }); * @name GET /api/v1/users */ router.get("/", async (req, res) => { - // i dont know yet. + // i dont know yet. }); router.use("/:userID", userIDRouter); diff --git a/server/src/server/router/ir/barbatos/router.test.ts b/server/src/server/router/ir/barbatos/router.test.ts index 8eaabaeae..f4ef0ba14 100644 --- a/server/src/server/router/ir/barbatos/router.test.ts +++ b/server/src/server/router/ir/barbatos/router.test.ts @@ -7,39 +7,39 @@ import { TestingBarbatosScore } from "../../../../test-utils/test-data"; import db from "../../../../external/mongo/db"; t.test("POST /ir/barbatos/score/submit", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); + const cookie = await CreateFakeAuthCookie(mockApi); - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should import a valid score", async (t) => { - const res = await mockApi - .post("/ir/barbatos/score/submit") - .set("Cookie", cookie) - .send(TestingBarbatosScore); + t.test("Should import a valid score", async (t) => { + const res = await mockApi + .post("/ir/barbatos/score/submit") + .set("Cookie", cookie) + .send(TestingBarbatosScore); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scores = await db.scores.count({ - service: "Barbatos", - }); + const scores = await db.scores.count({ + service: "Barbatos", + }); - t.equal(scores, 1, "Should import 1 score."); + t.equal(scores, 1, "Should import 1 score."); - t.end(); - }); + t.end(); + }); - t.test("Should reject an invalid body", async (t) => { - const res = await mockApi.post("/ir/barbatos/score/submit").set("Cookie", cookie).send({}); + t.test("Should reject an invalid body", async (t) => { + const res = await mockApi.post("/ir/barbatos/score/submit").set("Cookie", cookie).send({}); - t.equal(res.body.success, false, "Should not be successful."); - t.equal(res.status, 400, "Should return 400."); + t.equal(res.body.success, false, "Should not be successful."); + t.equal(res.status, 400, "Should return 400."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/barbatos/router.ts b/server/src/server/router/ir/barbatos/router.ts index 5ba326bff..ab5c8d670 100644 --- a/server/src/server/router/ir/barbatos/router.ts +++ b/server/src/server/router/ir/barbatos/router.ts @@ -14,16 +14,16 @@ router.use(RequirePermissions("submit:score")); * @name POST /ir/barbatos/score/submit */ router.post("/score/submit", async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!); - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - false, - "ir/barbatos", - (logger) => ParseBarbatosSingle(req.body, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + false, + "ir/barbatos", + (logger) => ParseBarbatosSingle(req.body, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); + return res.status(responseData.statusCode).json(responseData.body); }); export default router; diff --git a/server/src/server/router/ir/beatoraja/auth.test.ts b/server/src/server/router/ir/beatoraja/auth.test.ts index 990585fc1..f6fed5238 100644 --- a/server/src/server/router/ir/beatoraja/auth.test.ts +++ b/server/src/server/router/ir/beatoraja/auth.test.ts @@ -4,47 +4,47 @@ import expMiddlewareMock from "express-request-mock"; import { CloseAllConnections } from "../../../../test-utils/close-connections"; t.test("#ValidateIRClientVersion", (t) => { - t.test("Should reject clients that are not supported", async (t) => { - const { res } = await expMiddlewareMock(ValidateIRClientVersion, { - headers: { - "X-BokutachiIR-Version": "1.2.0", - }, - }); + t.test("Should reject clients that are not supported", async (t) => { + const { res } = await expMiddlewareMock(ValidateIRClientVersion, { + headers: { + "X-BokutachiIR-Version": "1.2.0", + }, + }); - const json = res._getJSONData(); + const json = res._getJSONData(); - t.equal(res.statusCode, 400); - t.equal(json.success, false); - t.match(json.description, /Invalid BokutachiIR client version/u); + t.equal(res.statusCode, 400); + t.equal(json.success, false); + t.match(json.description, /Invalid BokutachiIR client version/u); - t.end(); - }); + t.end(); + }); - t.test("Should reject no client header", async (t) => { - const { res } = await expMiddlewareMock(ValidateIRClientVersion, {}); + t.test("Should reject no client header", async (t) => { + const { res } = await expMiddlewareMock(ValidateIRClientVersion, {}); - const json = res._getJSONData(); + const json = res._getJSONData(); - t.equal(res.statusCode, 400); - t.equal(json.success, false); - t.match(json.description, /Invalid BokutachiIR client version/u); + t.equal(res.statusCode, 400); + t.equal(json.success, false); + t.match(json.description, /Invalid BokutachiIR client version/u); - t.end(); - }); + t.end(); + }); - t.test("Should accept 2.0.0", async (t) => { - const { res } = await expMiddlewareMock(ValidateIRClientVersion, { - headers: { - "X-BokutachiIR-Version": "2.0.0", - }, - }); + t.test("Should accept 2.0.0", async (t) => { + const { res } = await expMiddlewareMock(ValidateIRClientVersion, { + headers: { + "X-BokutachiIR-Version": "2.0.0", + }, + }); - t.equal(res.statusCode, 200); + t.equal(res.statusCode, 200); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/beatoraja/auth.ts b/server/src/server/router/ir/beatoraja/auth.ts index d6ae92ba6..80cefc4a5 100644 --- a/server/src/server/router/ir/beatoraja/auth.ts +++ b/server/src/server/router/ir/beatoraja/auth.ts @@ -1,14 +1,14 @@ import { RequestHandler } from "express"; export const ValidateIRClientVersion: RequestHandler = (req, res, next) => { - const header = req.header("X-BokutachiIR-Version"); + const header = req.header("X-BokutachiIR-Version"); - if (header !== "2.0.0") { - return res.status(400).json({ - success: false, - description: "Invalid BokutachiIR client version.", - }); - } + if (header !== "2.0.0") { + return res.status(400).json({ + success: false, + description: "Invalid BokutachiIR client version.", + }); + } - return next(); + return next(); }; diff --git a/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts b/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts index 055a364e8..a92f33506 100644 --- a/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts @@ -11,181 +11,181 @@ import { CloseAllConnections } from "../../../../../test-utils/close-connections const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json"); const pbScore = { - composedFrom: { - lampPB: "mock_lampPB", - }, - scoreData: { - lampIndex: 4, - score: 1234, - hitMeta: {}, - }, - playtype: "7K", - scoreMeta: {}, - chartID: gazerChart.chartID, - userID: 1, + composedFrom: { + lampPB: "mock_lampPB", + }, + scoreData: { + lampIndex: 4, + score: 1234, + hitMeta: {}, + }, + playtype: "7K", + scoreMeta: {}, + chartID: gazerChart.chartID, + userID: 1, } as unknown as PBScoreDocument<"bms:7K" | "bms:14K">; t.test("#TachiPBScoreToBeatorajaFormat", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(async () => { - await db.scores.insert( - [1, 2, 3, 4, 5].map(() => ({ - userID: 1, - chartID: gazerChart.chartID, - scoreID: Random20Hex(), - })) as ScoreDocument[] - ); + t.beforeEach(ResetDBState); + t.beforeEach(async () => { + await db.scores.insert( + [1, 2, 3, 4, 5].map(() => ({ + userID: 1, + chartID: gazerChart.chartID, + scoreID: Random20Hex(), + })) as ScoreDocument[] + ); - await db.scores.insert({ - scoreID: "mock_lampPB", - scoreData: { - hitMeta: {}, - }, - scoreMeta: { - inputDevice: "KEYBOARD", - }, - } as ScoreDocument); - }); + await db.scores.insert({ + scoreID: "mock_lampPB", + scoreData: { + hitMeta: {}, + }, + scoreMeta: { + inputDevice: "KEYBOARD", + }, + } as ScoreDocument); + }); - t.test("Should convert score.", async (t) => { - const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 1); + t.test("Should convert score.", async (t) => { + const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 1); - t.strictSame( - res, - { - sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", - player: "", - playcount: 5, - clear: 5, - date: 0, - maxcombo: 0, - deviceType: "KEYBOARD", - gauge: 0, - random: 0, - passnotes: 2256, - minbp: 0, - notes: 2256, - }, - "Should return the beatoraja score format." - ); + t.strictSame( + res, + { + sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", + player: "", + playcount: 5, + clear: 5, + date: 0, + maxcombo: 0, + deviceType: "KEYBOARD", + gauge: 0, + random: 0, + passnotes: 2256, + minbp: 0, + notes: 2256, + }, + "Should return the beatoraja score format." + ); - t.end(); - }); + t.end(); + }); - t.test("Should emplace username if requestingUserID is not the pbscore owner", async (t) => { - const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2); + t.test("Should emplace username if requestingUserID is not the pbscore owner", async (t) => { + const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2); - t.strictSame( - res, - { - sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", - player: "test_zkldi", - playcount: 5, - clear: 5, - date: 0, - maxcombo: 0, - deviceType: "KEYBOARD", - gauge: 0, - random: 0, - passnotes: 2256, - minbp: 0, - notes: 2256, - }, - "Should return the beatoraja score format." - ); + t.strictSame( + res, + { + sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", + player: "test_zkldi", + playcount: 5, + clear: 5, + date: 0, + maxcombo: 0, + deviceType: "KEYBOARD", + gauge: 0, + random: 0, + passnotes: 2256, + minbp: 0, + notes: 2256, + }, + "Should return the beatoraja score format." + ); - t.end(); - }); + t.end(); + }); - t.test("Should return random if one is present", async (t) => { - await db.scores.remove({ scoreID: "mock_lampPB" }); - await db.scores.insert({ - scoreID: "mock_lampPB", - scoreData: { - hitMeta: {}, - }, - scoreMeta: { - inputDevice: "KEYBOARD", - random: "RANDOM", - }, - } as ScoreDocument); + t.test("Should return random if one is present", async (t) => { + await db.scores.remove({ scoreID: "mock_lampPB" }); + await db.scores.insert({ + scoreID: "mock_lampPB", + scoreData: { + hitMeta: {}, + }, + scoreMeta: { + inputDevice: "KEYBOARD", + random: "RANDOM", + }, + } as ScoreDocument); - const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2); + const res = await TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2); - t.strictSame( - res, - { - sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", - player: "test_zkldi", - playcount: 5, - clear: 5, - date: 0, - maxcombo: 0, - deviceType: "KEYBOARD", - gauge: 0, - random: 2, - passnotes: 2256, - minbp: 0, - notes: 2256, - }, - "Should return the beatoraja score format." - ); + t.strictSame( + res, + { + sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", + player: "test_zkldi", + playcount: 5, + clear: 5, + date: 0, + maxcombo: 0, + deviceType: "KEYBOARD", + gauge: 0, + random: 2, + passnotes: 2256, + minbp: 0, + notes: 2256, + }, + "Should return the beatoraja score format." + ); - t.end(); - }); + t.end(); + }); - t.test("Should skip random if score is 14K", async (t) => { - await db.scores.remove({ scoreID: "mock_lampPB" }); - await db.scores.insert({ - scoreID: "mock_lampPB", - scoreData: { - hitMeta: {}, - }, - scoreMeta: { - inputDevice: "KEYBOARD", - random: "RANDOM", - }, - } as ScoreDocument); + t.test("Should skip random if score is 14K", async (t) => { + await db.scores.remove({ scoreID: "mock_lampPB" }); + await db.scores.insert({ + scoreID: "mock_lampPB", + scoreData: { + hitMeta: {}, + }, + scoreMeta: { + inputDevice: "KEYBOARD", + random: "RANDOM", + }, + } as ScoreDocument); - const res = await TachiPBScoreToBeatorajaFormat( - deepmerge(pbScore, { playtype: "14K" }), - gazerChart, - 2 - ); + const res = await TachiPBScoreToBeatorajaFormat( + deepmerge(pbScore, { playtype: "14K" }), + gazerChart, + 2 + ); - t.strictSame( - res, - { - sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", - player: "test_zkldi", - playcount: 5, - clear: 5, - date: 0, - maxcombo: 0, - deviceType: "KEYBOARD", - gauge: 0, - random: 0, - passnotes: 2256, - minbp: 0, - notes: 2256, - }, - "Should return the beatoraja score format." - ); + t.strictSame( + res, + { + sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", + player: "test_zkldi", + playcount: 5, + clear: 5, + date: 0, + maxcombo: 0, + deviceType: "KEYBOARD", + gauge: 0, + random: 0, + passnotes: 2256, + minbp: 0, + notes: 2256, + }, + "Should return the beatoraja score format." + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw severe if no lampPB exists.", async (t) => { - await db.scores.remove({ scoreID: "mock_lampPB" }); + t.test("Should throw severe if no lampPB exists.", async (t) => { + await db.scores.remove({ scoreID: "mock_lampPB" }); - t.rejects(() => TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2), { - message: /User 1's PB on.*has no lampPB/u, - } as any); + t.rejects(() => TachiPBScoreToBeatorajaFormat(pbScore, gazerChart, 2), { + message: /User 1's PB on.*has no lampPB/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/beatoraja/charts/convert-scores.ts b/server/src/server/router/ir/beatoraja/charts/convert-scores.ts index 6e4105857..d85a7a1ae 100644 --- a/server/src/server/router/ir/beatoraja/charts/convert-scores.ts +++ b/server/src/server/router/ir/beatoraja/charts/convert-scores.ts @@ -6,32 +6,32 @@ import { GetUsernameFromUserID } from "../../../../../utils/user"; const LAMP_TO_BEATORAJA = [0, 1, 3, 4, 5, 6, 7, 8] as const; const RAN_INDEXES = { - NONRAN: 0, - MIRROR: 1, - RANDOM: 2, - "R-RANDOM": 3, - "S-RANDOM": 4, + NONRAN: 0, + MIRROR: 1, + RANDOM: 2, + "R-RANDOM": 3, + "S-RANDOM": 4, } as const; type BeatorajaJudgements = `${"e" | "l"}${"pg" | "gr" | "gd" | "bd" | "pr"}`; type BeatorajaScoreJudgements = { - [K in BeatorajaJudgements]: integer; + [K in BeatorajaJudgements]: integer; }; type BeatorajaPartialScoreFormat = { - sha256: string; - player: string; - playcount: integer; - clear: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; - date: number; - deviceType: string | null; - gauge: number; - random: 0 | 1 | 2 | 3 | 4 | null; - passnotes: integer; - minbp: integer; - notes: integer; - maxcombo: integer | null; + sha256: string; + player: string; + playcount: integer; + clear: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + date: number; + deviceType: string | null; + gauge: number; + random: 0 | 1 | 2 | 3 | 4 | null; + passnotes: integer; + minbp: integer; + notes: integer; + maxcombo: integer | null; }; export type BeatorajaIRScoreFormat = BeatorajaPartialScoreFormat & BeatorajaScoreJudgements; @@ -47,35 +47,35 @@ const logger = CreateLogCtx(__filename); * the requesting user. */ export async function TachiPBScoreToBeatorajaFormat( - pbScore: PBScoreDocument<"bms:7K" | "bms:14K">, - chart: ChartDocument<"bms:7K" | "bms:14K">, - requestingUserID: integer + pbScore: PBScoreDocument<"bms:7K" | "bms:14K">, + chart: ChartDocument<"bms:7K" | "bms:14K">, + requestingUserID: integer ) { - const playcount = await db.scores.count({ userID: pbScore.userID, chartID: chart.chartID }); - const username = - pbScore.userID === requestingUserID ? "" : await GetUsernameFromUserID(pbScore.userID); - const lampPB = (await db.scores.findOne({ - scoreID: pbScore.composedFrom.lampPB, - })) as ScoreDocument<"bms:7K" | "bms:14K"> | null; + const playcount = await db.scores.count({ userID: pbScore.userID, chartID: chart.chartID }); + const username = + pbScore.userID === requestingUserID ? "" : await GetUsernameFromUserID(pbScore.userID); + const lampPB = (await db.scores.findOne({ + scoreID: pbScore.composedFrom.lampPB, + })) as ScoreDocument<"bms:7K" | "bms:14K"> | null; - if (!lampPB) { - logger.severe( - `User ${pbScore.userID}'s PB on ${chart.chartID} has no lampPB, but references ${pbScore.composedFrom.lampPB}.` - ); - throw new Error( - `User ${pbScore.userID}'s PB on ${chart.chartID} has no lampPB, but references ${pbScore.composedFrom.lampPB}.` - ); - } + if (!lampPB) { + logger.severe( + `User ${pbScore.userID}'s PB on ${chart.chartID} has no lampPB, but references ${pbScore.composedFrom.lampPB}.` + ); + throw new Error( + `User ${pbScore.userID}'s PB on ${chart.chartID} has no lampPB, but references ${pbScore.composedFrom.lampPB}.` + ); + } - return TachiScoreDataToBeatorajaFormat( - pbScore, - chart.data.hashSHA256, - username, - chart.data.notecount, - playcount, - lampPB.scoreMeta.inputDevice, - lampPB.scoreMeta.random - ); + return TachiScoreDataToBeatorajaFormat( + pbScore, + chart.data.hashSHA256, + username, + chart.data.notecount, + playcount, + lampPB.scoreMeta.inputDevice, + lampPB.scoreMeta.random + ); } /** @@ -90,62 +90,62 @@ export async function TachiPBScoreToBeatorajaFormat( * @returns A Beatoraja Score Document. */ function TachiScoreDataToBeatorajaFormat( - pbScore: PBScoreDocument<"bms:7K" | "bms:14K">, - sha256: string, - username: string, - notecount: integer, - playcount: integer, - inputDevice: ScoreDocument<"bms:7K" | "bms:14K">["scoreMeta"]["inputDevice"], - random: ScoreDocument<"bms:7K" | "bms:14K">["scoreMeta"]["random"] + pbScore: PBScoreDocument<"bms:7K" | "bms:14K">, + sha256: string, + username: string, + notecount: integer, + playcount: integer, + inputDevice: ScoreDocument<"bms:7K" | "bms:14K">["scoreMeta"]["inputDevice"], + random: ScoreDocument<"bms:7K" | "bms:14K">["scoreMeta"]["random"] ) { - const scoreData = pbScore.scoreData; + const scoreData = pbScore.scoreData; - let rajaRandom = 0 as const; + let rajaRandom = 0 as const; - // Beatoraja does not support DP randoms - it only stores them in replay files, - // and only sends the left hand random here. - if (pbScore.playtype === "7K") { - if (random) { - // @ts-expect-error Invalid indexing because playtype removes the random tuple. - rajaRandom = RAN_INDEXES[random]; - } - } + // Beatoraja does not support DP randoms - it only stores them in replay files, + // and only sends the left hand random here. + if (pbScore.playtype === "7K") { + if (random) { + // @ts-expect-error Invalid indexing because playtype removes the random tuple. + rajaRandom = RAN_INDEXES[random]; + } + } - const beatorajaScore: BeatorajaPartialScoreFormat = { - sha256, - player: username, - playcount, - clear: LAMP_TO_BEATORAJA[scoreData.lampIndex] ?? 0, - date: pbScore.timeAchieved ?? 0, - maxcombo: scoreData.hitMeta.maxCombo ?? 0, - deviceType: inputDevice ?? null, - gauge: scoreData.hitMeta.gauge ?? 0, - random: rajaRandom, - passnotes: scoreData.hitMeta.diedAt ?? notecount, - minbp: scoreData.hitMeta.bp ?? 0, - notes: notecount, - }; + const beatorajaScore: BeatorajaPartialScoreFormat = { + sha256, + player: username, + playcount, + clear: LAMP_TO_BEATORAJA[scoreData.lampIndex] ?? 0, + date: pbScore.timeAchieved ?? 0, + maxcombo: scoreData.hitMeta.maxCombo ?? 0, + deviceType: inputDevice ?? null, + gauge: scoreData.hitMeta.gauge ?? 0, + random: rajaRandom, + passnotes: scoreData.hitMeta.diedAt ?? notecount, + minbp: scoreData.hitMeta.bp ?? 0, + notes: notecount, + }; - const judgements: Partial = {}; + const judgements: Partial = {}; - // Not everything exports these properties. If they're not there, they should default to 0. - // For cases like LR2/manual - this will just result in a set of 0s. - for (const key of [ - "epg", - "lpg", - "egr", - "lgr", - "egd", - "lgd", - "ebd", - "lbd", - "epr", - "lpr", - "ems", - "lms", - ] as BeatorajaJudgements[]) { - judgements[key] = scoreData.hitMeta[key] ?? 0; - } + // Not everything exports these properties. If they're not there, they should default to 0. + // For cases like LR2/manual - this will just result in a set of 0s. + for (const key of [ + "epg", + "lpg", + "egr", + "lgr", + "egd", + "lgd", + "ebd", + "lbd", + "epr", + "lpr", + "ems", + "lms", + ] as BeatorajaJudgements[]) { + judgements[key] = scoreData.hitMeta[key] ?? 0; + } - return beatorajaScore; + return beatorajaScore; } diff --git a/server/src/server/router/ir/beatoraja/charts/router.test.ts b/server/src/server/router/ir/beatoraja/charts/router.test.ts index c71d771aa..9a4bab673 100644 --- a/server/src/server/router/ir/beatoraja/charts/router.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/router.test.ts @@ -5,54 +5,54 @@ import mockApi from "../../../../../test-utils/mock-api"; import db from "../../../../../external/mongo/db"; t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; - const GAZER_CHARTID = "88eb6cc5683e2740cbd07f588a5f3db1db8d467b"; + const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; + const GAZER_CHARTID = "88eb6cc5683e2740cbd07f588a5f3db1db8d467b"; - t.test("Should return PB scores on a chart", async (t) => { - await db["personal-bests"].insert({ - composedFrom: { - lampPB: "mock_lampPB", - }, - scoreData: { - lampIndex: 4, - score: 1234, - hitMeta: {}, - }, - scoreMeta: {}, - chartID: GAZER_CHARTID, - userID: 1, - } as any); // very lazy fake scores + t.test("Should return PB scores on a chart", async (t) => { + await db["personal-bests"].insert({ + composedFrom: { + lampPB: "mock_lampPB", + }, + scoreData: { + lampIndex: 4, + score: 1234, + hitMeta: {}, + }, + scoreMeta: {}, + chartID: GAZER_CHARTID, + userID: 1, + } as any); // very lazy fake scores - await db.scores.insert({ - scoreID: "mock_lampPB", - scoreMeta: { - inputDevice: "BM_CONTROLLER", - random: "MIRROR", - }, - } as any); + await db.scores.insert({ + scoreID: "mock_lampPB", + scoreMeta: { + inputDevice: "BM_CONTROLLER", + random: "MIRROR", + }, + } as any); - const res = await mockApi - .get(`/ir/beatoraja/charts/${GAZER_SHA256}/scores`) - .set("X-BokutachiIR-Version", "2.0.0"); + const res = await mockApi + .get(`/ir/beatoraja/charts/${GAZER_SHA256}/scores`) + .set("X-BokutachiIR-Version", "2.0.0"); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.end(); - }); + t.end(); + }); - t.test("Should return 404 if chart doesnt exist", async (t) => { - const res = await mockApi - .get(`/ir/beatoraja/charts/INVALID/scores`) - .set("X-BokutachiIR-Version", "2.0.0"); + t.test("Should return 404 if chart doesnt exist", async (t) => { + const res = await mockApi + .get(`/ir/beatoraja/charts/INVALID/scores`) + .set("X-BokutachiIR-Version", "2.0.0"); - t.equal(res.status, 404); + t.equal(res.status, 404); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/beatoraja/charts/router.ts b/server/src/server/router/ir/beatoraja/charts/router.ts index ee50500dd..35bbbec75 100644 --- a/server/src/server/router/ir/beatoraja/charts/router.ts +++ b/server/src/server/router/ir/beatoraja/charts/router.ts @@ -8,20 +8,20 @@ import { AssignToReqTachiData } from "../../../../../utils/req-tachi-data"; const router: Router = Router({ mergeParams: true }); const GetChartDocument: RequestHandler = async (req, res, next) => { - const chart = (await db.charts.bms.findOne({ - "data.hashSHA256": req.params.chartSHA256, - })) as ChartDocument<"bms:7K" | "bms:14K"> | null; + const chart = (await db.charts.bms.findOne({ + "data.hashSHA256": req.params.chartSHA256, + })) as ChartDocument<"bms:7K" | "bms:14K"> | null; - if (!chart) { - return res.status(404).json({ - success: false, - description: `Chart does not exist on IR yet.`, - }); - } + if (!chart) { + return res.status(404).json({ + success: false, + description: `Chart does not exist on IR yet.`, + }); + } - AssignToReqTachiData(req, { beatorajaChartDoc: chart }); + AssignToReqTachiData(req, { beatorajaChartDoc: chart }); - return next(); + return next(); }; router.use(GetChartDocument); @@ -31,25 +31,25 @@ router.use(GetChartDocument); * @name GET /ir/beatoraja/chart/:chartSHA256/scores */ router.get("/scores", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.beatorajaChartDoc!; + const chart = req[SYMBOL_TachiData]!.beatorajaChartDoc!; - const scores = (await db["personal-bests"].find({ - chartID: chart.chartID, - })) as PBScoreDocument<"bms:7K" | "bms:14K">[]; + const scores = (await db["personal-bests"].find({ + chartID: chart.chartID, + })) as PBScoreDocument<"bms:7K" | "bms:14K">[]; - // @todo #139 Optimise GET /ir/beatoraja/chart/:chartSHA256/scores - // @optimisable - This should be solved with a couple queries and a hashmap. - const beatorajaScores = await Promise.all( - scores.map((e) => - TachiPBScoreToBeatorajaFormat(e, chart, req[SYMBOL_TachiAPIAuth]!.userID!) - ) - ); + // @todo #139 Optimise GET /ir/beatoraja/chart/:chartSHA256/scores + // @optimisable - This should be solved with a couple queries and a hashmap. + const beatorajaScores = await Promise.all( + scores.map((e) => + TachiPBScoreToBeatorajaFormat(e, chart, req[SYMBOL_TachiAPIAuth]!.userID!) + ) + ); - return res.status(200).json({ - success: true, - description: `Successfully returned ${beatorajaScores.length}`, - body: beatorajaScores, - }); + return res.status(200).json({ + success: true, + description: `Successfully returned ${beatorajaScores.length}`, + body: beatorajaScores, + }); }); export default router; diff --git a/server/src/server/router/ir/beatoraja/router.test.ts b/server/src/server/router/ir/beatoraja/router.test.ts index 8f4c38264..cd5999f0a 100644 --- a/server/src/server/router/ir/beatoraja/router.test.ts +++ b/server/src/server/router/ir/beatoraja/router.test.ts @@ -7,390 +7,390 @@ import { GetKTDataJSON } from "../../../../test-utils/test-data"; import deepmerge from "deepmerge"; t.test("POST /ir/beatoraja/submit-score", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => - db["api-tokens"].insert({ - userID: 1, - identifier: "Mock API Beatoraja Token", - permissions: { - "submit:score": true, - }, - token: "mock_token", - }) - ); + t.beforeEach(ResetDBState); + t.beforeEach(() => + db["api-tokens"].insert({ + userID: 1, + identifier: "Mock API Beatoraja Token", + permissions: { + "submit:score": true, + }, + token: "mock_token", + }) + ); - const scoreReq = GetKTDataJSON("./beatoraja/base.json"); + const scoreReq = GetKTDataJSON("./beatoraja/base.json"); - t.test("Should import a valid score.", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-score") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(scoreReq); + t.test("Should import a valid score.", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(scoreReq); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.equal(res.body.success, true); - t.hasStrict(res.body.body, { - score: { - scoreData: { - score: 1004, - }, - importType: "ir/beatoraja", - }, - chart: { - chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b", - }, - song: { - id: 27339, - }, - }); + t.equal(res.body.success, true); + t.hasStrict(res.body.body, { + score: { + scoreData: { + score: 1004, + }, + importType: "ir/beatoraja", + }, + chart: { + chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b", + }, + song: { + id: 27339, + }, + }); - const score = await db.scores.findOne( - { scoreID: res.body.body.score.scoreID }, - { projection: { _id: 0 } } - ); + const score = await db.scores.findOne( + { scoreID: res.body.body.score.scoreID }, + { projection: { _id: 0 } } + ); - t.not(score, null); + t.not(score, null); - t.hasStrict(res.body.body.score, score); + t.hasStrict(res.body.body.score, score); - t.end(); - }); + t.end(); + }); - t.test("Should return an error if invalid client.", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-score") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(scoreReq, { client: "INVALID" })); + t.test("Should return an error if invalid client.", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(scoreReq, { client: "INVALID" })); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.equal(res.body.success, false); - t.match(res.body.description, /Unknown client/u); + t.equal(res.body.success, false); + t.match(res.body.description, /Unknown client/u); - t.end(); - }); + t.end(); + }); - t.test("Should return an error if invalid score.", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-score") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(scoreReq, { score: { exscore: -1 } })); + t.test("Should return an error if invalid score.", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(scoreReq, { score: { exscore: -1 } })); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.equal(res.body.success, false); - t.match(res.body.description, /Invalid Beatoraja Import - Score/u); + t.equal(res.body.success, false); + t.match(res.body.description, /Invalid Beatoraja Import - Score/u); - t.end(); - }); + t.end(); + }); - t.test("Should return an error if invalid chart.", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-score") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(scoreReq, { chart: { title: null } })); + t.test("Should return an error if invalid chart.", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(scoreReq, { chart: { title: null } })); - t.equal(res.status, 400); + t.equal(res.status, 400); - t.equal(res.body.success, false); - t.match(res.body.description, /Invalid Beatoraja Import - Chart/u); + t.equal(res.body.success, false); + t.match(res.body.description, /Invalid Beatoraja Import - Chart/u); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); const courseScore = { - course: { - name: "GENOSIDE 2018 段位認定 発狂皆伝", - charts: [ - { - md5: "cfad3baadce9e02c45021963453d7c94", - }, - { - md5: "77d23be22b2370925c573d922276bce0", - }, - { - md5: "188a99f74ab71804f2e360dcf484545c", - }, - { - md5: "c46a81cb184f5a804c119930d6eba748", - }, - ], - constraint: ["MIRROR", "GAUGE_LR2", "LN"], - trophy: [{}, {}], - lntype: 0, - }, - score: { - sha256: "", - lntype: 0, - player: "unknown", - clear: "Clear", - date: 0, - epg: 1334, - lpg: 788, - egr: 1634, - lgr: 382, - egd: 239, - lgd: 142, - ebd: 34, - lbd: 8, - epr: 0, - lpr: 93, - ems: 63, - lms: 74, - maxcombo: 225, - notes: 6005, - passnotes: 4654, - minbp: 1623, - option: 0, - assist: 0, - gauge: 0, - deviceType: "BM_CONTROLLER", - judgeAlgorithm: "Combo", - rule: "Beatoraja_7", - exscore: 6260, - }, + course: { + name: "GENOSIDE 2018 段位認定 発狂皆伝", + charts: [ + { + md5: "cfad3baadce9e02c45021963453d7c94", + }, + { + md5: "77d23be22b2370925c573d922276bce0", + }, + { + md5: "188a99f74ab71804f2e360dcf484545c", + }, + { + md5: "c46a81cb184f5a804c119930d6eba748", + }, + ], + constraint: ["MIRROR", "GAUGE_LR2", "LN"], + trophy: [{}, {}], + lntype: 0, + }, + score: { + sha256: "", + lntype: 0, + player: "unknown", + clear: "Clear", + date: 0, + epg: 1334, + lpg: 788, + egr: 1634, + lgr: 382, + egd: 239, + lgd: 142, + ebd: 34, + lbd: 8, + epr: 0, + lpr: 93, + ems: 63, + lms: 74, + maxcombo: 225, + notes: 6005, + passnotes: 4654, + minbp: 1623, + option: 0, + assist: 0, + gauge: 0, + deviceType: "BM_CONTROLLER", + judgeAlgorithm: "Combo", + rule: "Beatoraja_7", + exscore: 6260, + }, }; t.test("POST /ir/beatoraja/submit-course", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(() => - db["api-tokens"].insert({ - userID: 1, - identifier: "Mock API Beatoraja Token", - permissions: { - "submit:score": true, - }, - token: "mock_token", - }) - ); + t.beforeEach(ResetDBState); + t.beforeEach(() => + db["api-tokens"].insert({ + userID: 1, + identifier: "Mock API Beatoraja Token", + permissions: { + "submit:score": true, + }, + token: "mock_token", + }) + ); - t.test("Should accept a valid clear", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(courseScore); + t.test("Should accept a valid clear", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(courseScore); - t.equal(res.status, 200); - t.equal(res.body.success, true); - t.equal(res.body.description, "Successfully updated class."); + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Successfully updated class."); - const ugs = await db["game-stats"].findOne({ userID: 1, game: "bms", playtype: "7K" }); + const ugs = await db["game-stats"].findOne({ userID: 1, game: "bms", playtype: "7K" }); - t.equal(ugs?.classes.genocideDan, 22, "Should set their dan to insane kaiden."); + t.equal(ugs?.classes.genocideDan, 22, "Should set their dan to insane kaiden."); - t.end(); - }); + t.end(); + }); - t.test("Should silently reject a fail", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(courseScore, { score: { clear: "Failed" } })); + t.test("Should silently reject a fail", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(courseScore, { score: { clear: "Failed" } })); - t.equal(res.status, 200); - t.equal(res.body.success, true); - t.equal(res.body.description, "Class not updated."); + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Class not updated."); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores with no charts", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(courseScore, { course: { charts: [] } }, { arrayMerge: (d, s) => s })); + t.test("Should reject scores with no charts", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(courseScore, { course: { charts: [] } }, { arrayMerge: (d, s) => s })); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Course Submission."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores with invalid chart documents", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send(deepmerge(courseScore, { course: { charts: [1, 2, 3, 4] } })); + t.test("Should reject scores with invalid chart documents", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send(deepmerge(courseScore, { course: { charts: [1, 2, 3, 4] } })); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Course Submission."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores with too many chart documents", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge(courseScore, { - course: { charts: [{ md5: "a" }, { md5: "a" }, { md5: "a" }, { md5: "a" }] }, - }) - ); + t.test("Should reject scores with too many chart documents", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge(courseScore, { + course: { charts: [{ md5: "a" }, { md5: "a" }, { md5: "a" }, { md5: "a" }] }, + }) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Course Submission."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Course Submission."); - t.end(); - }); + t.end(); + }); - t.test("Should reject scores not on LN mode", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge(courseScore, { - score: { - lntype: 1, - }, - }) - ); + t.test("Should reject scores not on LN mode", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge(courseScore, { + score: { + lntype: 1, + }, + }) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "LN mode is the only supported mode for dans."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "LN mode is the only supported mode for dans."); - t.end(); - }); + t.end(); + }); - t.test("Should reject too few constraints", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge( - courseScore, - { - course: { - constraint: ["GAUGE_LR2"], - }, - }, - { arrayMerge: (d, s) => s } - ) - ); + t.test("Should reject too few constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["GAUGE_LR2"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Constraints."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); - t.end(); - }); + t.end(); + }); - t.test("Should reject non-array constraints", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge( - courseScore, - { - course: { - constraint: "foo", - }, - }, - { arrayMerge: (d, s) => s } - ) - ); + t.test("Should reject non-array constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: "foo", + }, + }, + { arrayMerge: (d, s) => s } + ) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Constraints."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); - t.end(); - }); + t.end(); + }); - t.test("Should reject too many constraints", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge( - courseScore, - { - course: { - constraint: ["a", "b", "c", "d"], - }, - }, - { arrayMerge: (d, s) => s } - ) - ); + t.test("Should reject too many constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["a", "b", "c", "d"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Constraints."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid constraints", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge( - courseScore, - { - course: { - constraint: ["foo", "bar"], - }, - }, - { arrayMerge: (d, s) => s } - ) - ); + t.test("Should reject invalid constraints", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["foo", "bar"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Constraints."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid constraints for 3", async (t) => { - const res = await mockApi - .post("/ir/beatoraja/submit-course") - .set("X-BokutachiIR-Version", "2.0.0") - .set("Authorization", "Bearer mock_token") - .send( - deepmerge( - courseScore, - { - course: { - constraint: ["GAUGE_LR2", "MIRROR", "CHEAT_MODE"], - }, - }, - { arrayMerge: (d, s) => s } - ) - ); + t.test("Should reject invalid constraints for 3", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-course") + .set("X-BokutachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge( + courseScore, + { + course: { + constraint: ["GAUGE_LR2", "MIRROR", "CHEAT_MODE"], + }, + }, + { arrayMerge: (d, s) => s } + ) + ); - t.equal(res.status, 400); - t.equal(res.body.success, false); - t.equal(res.body.description, "Invalid Constraints."); + t.equal(res.status, 400); + t.equal(res.body.success, false); + t.equal(res.body.description, "Invalid Constraints."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/beatoraja/router.ts b/server/src/server/router/ir/beatoraja/router.ts index e9d839e24..38f7e5dcb 100644 --- a/server/src/server/router/ir/beatoraja/router.ts +++ b/server/src/server/router/ir/beatoraja/router.ts @@ -21,60 +21,60 @@ router.use(ValidateIRClientVersion); * @name POST /ir/beatoraja/submit-score */ router.post("/submit-score", async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!); - const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, logger); + const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, logger); - const importRes = await ExpressWrappedScoreImportMain( - userDoc, - false, - "ir/beatoraja", - ParserFunction - ); + const importRes = await ExpressWrappedScoreImportMain( + userDoc, + false, + "ir/beatoraja", + ParserFunction + ); - if (!importRes.body.success) { - return res.status(400).json(importRes.body); - } else if (importRes.body.body.errors.length !== 0) { - // since we're only ever importing one score, we can guarantee - // that this means the score we tried to import was skipped. - return res.status(400).json({ - success: false, - description: `[${importRes.body.body.errors[0].type}] - ${importRes.body.body.errors[0].message}`, - }); - } + if (!importRes.body.success) { + return res.status(400).json(importRes.body); + } else if (importRes.body.body.errors.length !== 0) { + // since we're only ever importing one score, we can guarantee + // that this means the score we tried to import was skipped. + return res.status(400).json({ + success: false, + description: `[${importRes.body.body.errors[0].type}] - ${importRes.body.body.errors[0].message}`, + }); + } - const scoreDoc = await db.scores.findOne({ - scoreID: importRes.body.body.scoreIDs[0], - }); + const scoreDoc = await db.scores.findOne({ + scoreID: importRes.body.body.scoreIDs[0], + }); - if (!scoreDoc) { - logger.severe( - `ScoreDocument ${importRes.body.body.scoreIDs[0]} was claimed to be inserted, but wasn't.` - ); - return res.status(500).json({ - success: false, - description: "Internal Service Error.", - }); - } + if (!scoreDoc) { + logger.severe( + `ScoreDocument ${importRes.body.body.scoreIDs[0]} was claimed to be inserted, but wasn't.` + ); + return res.status(500).json({ + success: false, + description: "Internal Service Error.", + }); + } - const chart = await db.charts.bms.findOne({ - chartID: scoreDoc.chartID, - }); + const chart = await db.charts.bms.findOne({ + chartID: scoreDoc.chartID, + }); - const song = await db.songs.bms.findOne({ - id: chart!.songID, - }); + const song = await db.songs.bms.findOne({ + id: chart!.songID, + }); - return res.status(importRes.statusCode).json({ - success: true, - description: "Imported score.", - body: { - score: scoreDoc, - song, - chart, - import: importRes.body.body, - }, - }); + return res.status(importRes.statusCode).json({ + success: true, + description: "Imported score.", + body: { + score: scoreDoc, + song, + chart, + import: importRes.body.body, + }, + }); }); /** @@ -83,116 +83,116 @@ router.post("/submit-score", async (req, res) => { * @name POST /ir/beatoraja/submit-course */ router.post("/submit-course", async (req, res) => { - const charts = req.body.course?.charts; + const charts = req.body.course?.charts; - if ( - !charts || - !Array.isArray(charts) || - charts.length !== 4 || - !charts.every((e) => e && typeof e === "object" && typeof e.md5 === "string") - ) { - return res.status(400).json({ - success: false, - description: `Invalid Course Submission.`, - }); - } + if ( + !charts || + !Array.isArray(charts) || + charts.length !== 4 || + !charts.every((e) => e && typeof e === "object" && typeof e.md5 === "string") + ) { + return res.status(400).json({ + success: false, + description: `Invalid Course Submission.`, + }); + } - const clear = req.body.score?.clear; + const clear = req.body.score?.clear; - if (!clear || clear === "Failed") { - return res.status(200).json({ - success: true, - description: "Class not updated.", - }); - } + if (!clear || clear === "Failed") { + return res.status(200).json({ + success: true, + description: "Class not updated.", + }); + } - if (req.body.score?.lntype !== 0) { - return res.status(400).json({ - success: false, - description: "LN mode is the only supported mode for dans.", - }); - } + if (req.body.score?.lntype !== 0) { + return res.status(400).json({ + success: false, + description: "LN mode is the only supported mode for dans.", + }); + } - // Constraints are a bit complicated. - // We only want to accept dans with the following - // beatoraja constraints - ["MIRROR","GAUGE_LR2"] or - // ["MIRROR", "GAUGE_LR2", "LN"]. - const constraint = req.body.course.constraint; + // Constraints are a bit complicated. + // We only want to accept dans with the following + // beatoraja constraints - ["MIRROR","GAUGE_LR2"] or + // ["MIRROR", "GAUGE_LR2", "LN"]. + const constraint = req.body.course.constraint; - if ( - !constraint || - !Array.isArray(constraint) || - (constraint.length !== 2 && constraint.length !== 3) - ) { - return res.status(400).json({ - success: false, - description: `Invalid Constraints.`, - }); - } + if ( + !constraint || + !Array.isArray(constraint) || + (constraint.length !== 2 && constraint.length !== 3) + ) { + return res.status(400).json({ + success: false, + description: `Invalid Constraints.`, + }); + } - // If there are two constraints, check that they are - // MIRROR and GAUGE_LR2. - if (!constraint.includes("MIRROR") || !constraint.includes("GAUGE_LR2")) { - return res.status(400).json({ - success: false, - description: `Invalid Constraints.`, - }); - } + // If there are two constraints, check that they are + // MIRROR and GAUGE_LR2. + if (!constraint.includes("MIRROR") || !constraint.includes("GAUGE_LR2")) { + return res.status(400).json({ + success: false, + description: `Invalid Constraints.`, + }); + } - // If there are three constraints, check that the third - // is LN - if (constraint.length === 3) { - if (!constraint.includes("LN")) { - return res.status(400).json({ - success: false, - description: `Invalid Constraints.`, - }); - } - } + // If there are three constraints, check that the third + // is LN + if (constraint.length === 3) { + if (!constraint.includes("LN")) { + return res.status(400).json({ + success: false, + description: `Invalid Constraints.`, + }); + } + } - // Combine the md5s into one string in their order. - const combinedMD5s = charts.map((e) => e.md5).join(""); + // Combine the md5s into one string in their order. + const combinedMD5s = charts.map((e) => e.md5).join(""); - const course = await db["bms-course-lookup"].findOne({ - md5sums: combinedMD5s, - }); + const course = await db["bms-course-lookup"].findOne({ + md5sums: combinedMD5s, + }); - if (!course) { - return res.status(404).json({ - success: false, - description: `Unsupported course.`, - }); - } + if (!course) { + return res.status(404).json({ + success: false, + description: `Unsupported course.`, + }); + } - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TachiAPIAuth]!.userID!; - const result = await UpdateClassIfGreater( - userID, - "bms", - course.playtype, - course.set, - course.value - ); + const result = await UpdateClassIfGreater( + userID, + "bms", + course.playtype, + course.set, + course.value + ); - if (result === false) { - return res.status(200).json({ - success: true, - description: "Class not updated.", - body: { - set: course.set, - value: course.value, - }, - }); - } + if (result === false) { + return res.status(200).json({ + success: true, + description: "Class not updated.", + body: { + set: course.set, + value: course.value, + }, + }); + } - return res.status(200).json({ - success: true, - description: "Successfully updated class.", - body: { - set: course.set, - value: course.value, - }, - }); + return res.status(200).json({ + success: true, + description: "Successfully updated class.", + body: { + set: course.set, + value: course.value, + }, + }); }); router.use("/charts/:chartSHA256", chartsRouter); diff --git a/server/src/server/router/ir/chunitachi/router.test.ts b/server/src/server/router/ir/chunitachi/router.test.ts index c120cee3b..a49f8cfbf 100644 --- a/server/src/server/router/ir/chunitachi/router.test.ts +++ b/server/src/server/router/ir/chunitachi/router.test.ts @@ -8,62 +8,62 @@ import { GetKTDataJSON } from "../../../../test-utils/test-data"; import deepmerge from "deepmerge"; t.test("POST /ir/chunitachi/import", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(InsertFakeTokenWithAllPerms("mock_token")); + t.beforeEach(ResetDBState); + t.beforeEach(InsertFakeTokenWithAllPerms("mock_token")); - const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json"); + const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json"); - t.test("Should work for CHUNITACHI requests", async (t) => { - const res = await mockApi - .post("/ir/chunitachi/import") - .set("Authorization", `Bearer mock_token`) - .send(chunitachiBody); + t.test("Should work for CHUNITACHI requests", async (t) => { + const res = await mockApi + .post("/ir/chunitachi/import") + .set("Authorization", `Bearer mock_token`) + .send(chunitachiBody); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scoreCount = await db.scores.count({ service: "Chunitachi" }); + const scoreCount = await db.scores.count({ service: "Chunitachi" }); - t.equal(scoreCount, 1, "Should import one score."); + t.equal(scoreCount, 1, "Should import one score."); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid batch-manual", async (t) => { - const res = await mockApi - .post("/ir/chunitachi/import") - .set("Authorization", `Bearer mock_token`) - .send({}); + t.test("Should reject invalid batch-manual", async (t) => { + const res = await mockApi + .post("/ir/chunitachi/import") + .set("Authorization", `Bearer mock_token`) + .send({}); - t.equal(res.body.success, false, "Should not be successful"); + t.equal(res.body.success, false, "Should not be successful"); - t.end(); - }); + t.end(); + }); - t.test("Should reject batch-manual requests if game is not chunithm", async (t) => { - const res = await mockApi - .post("/ir/chunitachi/import") - .set("Authorization", `Bearer mock_token`) - .send(deepmerge(chunitachiBody, { head: { game: "iidx" } })); + t.test("Should reject batch-manual requests if game is not chunithm", async (t) => { + const res = await mockApi + .post("/ir/chunitachi/import") + .set("Authorization", `Bearer mock_token`) + .send(deepmerge(chunitachiBody, { head: { game: "iidx" } })); - t.equal(res.body.success, false, "Should not be successful"); + t.equal(res.body.success, false, "Should not be successful"); - t.end(); - }); + t.end(); + }); - t.test("Should reject batch-manual requests if service is not Chunitachi", async (t) => { - const res = await mockApi - .post("/ir/chunitachi/import") - .set("Authorization", `Bearer mock_token`) - .send(deepmerge(chunitachiBody, { head: { service: "foo bar" } })); + t.test("Should reject batch-manual requests if service is not Chunitachi", async (t) => { + const res = await mockApi + .post("/ir/chunitachi/import") + .set("Authorization", `Bearer mock_token`) + .send(deepmerge(chunitachiBody, { head: { service: "foo bar" } })); - t.equal(res.body.success, false, "Should not be successful"); + t.equal(res.body.success, false, "Should not be successful"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/chunitachi/router.ts b/server/src/server/router/ir/chunitachi/router.ts index 9b531028e..5d6043f70 100644 --- a/server/src/server/router/ir/chunitachi/router.ts +++ b/server/src/server/router/ir/chunitachi/router.ts @@ -12,30 +12,30 @@ const router: Router = Router({ mergeParams: true }); * @name POST /ir/chunitachi/score/submit */ router.post("/import", RequirePermissions("submit:score"), async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); - if (req.body?.head?.game !== "chunithm") { - return res.status(400).json({ - success: false, - description: "Invalid Game.", - }); - } + if (req.body?.head?.game !== "chunithm") { + return res.status(400).json({ + success: false, + description: "Invalid Game.", + }); + } - if (req.body.head.service !== "Chunitachi") { - return res.status(400).json({ - success: false, - description: `Unexpected service ${req.body.head.service} -- expected 'Chunitachi'`, - }); - } + if (req.body.head.service !== "Chunitachi") { + return res.status(400).json({ + success: false, + description: `Unexpected service ${req.body.head.service} -- expected 'Chunitachi'`, + }); + } - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - false, - "ir/chunitachi", - (logger) => ParseDirectManual(req.body, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + false, + "ir/chunitachi", + (logger) => ParseDirectManual(req.body, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); + return res.status(responseData.statusCode).json(responseData.body); }); export default router; diff --git a/server/src/server/router/ir/direct-manual/router.test.ts b/server/src/server/router/ir/direct-manual/router.test.ts index 86c9e84f2..9f3ce37ab 100644 --- a/server/src/server/router/ir/direct-manual/router.test.ts +++ b/server/src/server/router/ir/direct-manual/router.test.ts @@ -8,57 +8,57 @@ import ResetDBState from "../../../../test-utils/resets"; import { GetKTDataJSON } from "../../../../test-utils/test-data"; t.test("POST /ir/direct-manual/import", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); + const cookie = await CreateFakeAuthCookie(mockApi); - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should require submit:score permissions", async (t) => { - await db["api-tokens"].insert({ - token: "foo", - identifier: "bar", - permissions: { - "submit:score": false, - }, - userID: 1, - }); + t.test("Should require submit:score permissions", async (t) => { + await db["api-tokens"].insert({ + token: "foo", + identifier: "bar", + permissions: { + "submit:score": false, + }, + userID: 1, + }); - const res = await mockApi - .post("/ir/direct-manual/import") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .post("/ir/direct-manual/import") + .set("Authorization", "Bearer foo"); - t.equal(res.body.success, false); + t.equal(res.body.success, false); - t.match(res.body.description, /submit:score/u); + t.match(res.body.description, /submit:score/u); - t.end(); - }); + t.end(); + }); - t.test("Should upload BATCH-MANUAL data from the request body.", async (t) => { - const res = await mockApi - .post("/ir/direct-manual/import") - .set("Cookie", cookie) - .send(GetKTDataJSON("./batch-manual/small-file.json")); + t.test("Should upload BATCH-MANUAL data from the request body.", async (t) => { + const res = await mockApi + .post("/ir/direct-manual/import") + .set("Cookie", cookie) + .send(GetKTDataJSON("./batch-manual/small-file.json")); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scoreCount = await db.scores.count({ service: "foobar (DIRECT-MANUAL)" }); + const scoreCount = await db.scores.count({ service: "foobar (DIRECT-MANUAL)" }); - t.equal(scoreCount, 1, "Should import one score."); + t.equal(scoreCount, 1, "Should import one score."); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid BATCH-MANUAL data from the request body.", async (t) => { - const res = await mockApi.post("/ir/direct-manual/import").set("Cookie", cookie).send({}); + t.test("Should reject invalid BATCH-MANUAL data from the request body.", async (t) => { + const res = await mockApi.post("/ir/direct-manual/import").set("Cookie", cookie).send({}); - t.equal(res.body.success, false, "Should not be successful"); + t.equal(res.body.success, false, "Should not be successful"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/direct-manual/router.ts b/server/src/server/router/ir/direct-manual/router.ts index b0fdf1482..7281f3973 100644 --- a/server/src/server/router/ir/direct-manual/router.ts +++ b/server/src/server/router/ir/direct-manual/router.ts @@ -13,23 +13,23 @@ const router: Router = Router({ mergeParams: true }); * @name POST /ir/direct-manual/import */ router.post( - "/import", - RequirePermissions("submit:score"), - RequireLoggedInSession, - async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); + "/import", + RequirePermissions("submit:score"), + RequireLoggedInSession, + async (req, res) => { + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); - const intent = req.header("X-User-Intent"); + const intent = req.header("X-User-Intent"); - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - !!intent, - "ir/direct-manual", - (logger) => ParseDirectManual(req.body, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + !!intent, + "ir/direct-manual", + (logger) => ParseDirectManual(req.body, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); - } + return res.status(responseData.statusCode).json(responseData.body); + } ); export default router; diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 79265376b..4e4d4171c 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -8,332 +8,332 @@ import { GetKTDataJSON } from "../../../../test-utils/test-data"; // eslint-disable-next-line @typescript-eslint/no-explicit-any function TestHeaders(url: string, data: any) { - t.beforeEach(InsertFakeTokenWithAllPerms("mock_token")); + t.beforeEach(InsertFakeTokenWithAllPerms("mock_token")); - t.test("Should reject invalid X-Software-Models", async (t) => { - let res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // rootage - .set("X-Software-Model", "LDJ:J:B:A:2019090200") - .set("User-Agent", "fervidex/1.3.0") - .send(data); + t.test("Should reject invalid X-Software-Models", async (t) => { + let res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // rootage + .set("X-Software-Model", "LDJ:J:B:A:2019090200") + .set("User-Agent", "fervidex/1.3.0") + .send(data); - t.equal(res.body.success, false, "Should reject rootage clients"); + t.equal(res.body.success, false, "Should reject rootage clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // rootage old - .set("X-Software-Model", "LDJ:J:B:A:2019100700") - .set("User-Agent", "fervidex/1.3.0") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // rootage old + .set("X-Software-Model", "LDJ:J:B:A:2019100700") + .set("User-Agent", "fervidex/1.3.0") + .send(data); - t.equal(res.body.success, false, "Should reject rootage clients"); + t.equal(res.body.success, false, "Should reject rootage clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // cannonballers - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2018091900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // cannonballers + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2018091900") + .send(data); - t.equal(res.body.success, false, "Should reject cannonballers clients"); + t.equal(res.body.success, false, "Should reject cannonballers clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:NONSENSE") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:NONSENSE") + .send(data); - t.equal(res.body.success, false, "Should reject nonsense versions"); + t.equal(res.body.success, false, "Should reject nonsense versions"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // BMS-iidx - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:Z:2020092900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // BMS-iidx + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:Z:2020092900") + .send(data); - t.equal(res.body.success, false, "Should reject BMS-iidx clients"); + t.equal(res.body.success, false, "Should reject BMS-iidx clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // BMS-iidx - .set("User-Agent", "fervidex/1.2.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // BMS-iidx + .set("User-Agent", "fervidex/1.2.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(data); - t.equal(res.body.success, false, "Should reject outdated fervidex clients"); + t.equal(res.body.success, false, "Should reject outdated fervidex clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // BMS-iidx - .set("User-Agent", "fervidex/.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // BMS-iidx + .set("User-Agent", "fervidex/.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(data); - t.equal(res.body.success, false, "Should reject invalid fervidex clients"); + t.equal(res.body.success, false, "Should reject invalid fervidex clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // BMS-iidx - .set("User-Agent", "") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // BMS-iidx + .set("User-Agent", "") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(data); - t.equal(res.body.success, false, "Should reject invalid fervidex clients"); + t.equal(res.body.success, false, "Should reject invalid fervidex clients"); - res = await mockApi - .post(url) - .set("Authorization", "Bearer mock_token") - // BMS-iidx - .set("User-Agent", "invalid") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(data); + res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // BMS-iidx + .set("User-Agent", "invalid") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(data); - t.equal(res.body.success, false, "Should reject invalid fervidex clients"); + t.equal(res.body.success, false, "Should reject invalid fervidex clients"); - t.end(); - }); + t.end(); + }); } t.beforeEach(ResetDBState); t.beforeEach(InsertFakeTokenWithAllPerms("mock_token")); t.test("POST /ir/fervidex/class/submit", (t) => { - TestHeaders("/ir/fervidex/class/submit", {}); + TestHeaders("/ir/fervidex/class/submit", {}); - t.test("Should update a users class.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: 18, play_style: 0 }); + t.test("Should update a users class.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: 18, play_style: 0 }); - t.equal(res.status, 200); - t.equal(res.body.success, true); - t.equal(res.body.description, "Dan changed!"); + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Dan changed!"); - const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "SP" }); + const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "SP" }); - t.equal(ugs?.classes.dan, 18); + t.equal(ugs?.classes.dan, 18); - t.end(); - }); + t.end(); + }); - t.test("Should update a users class for DP.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: 17, play_style: 1 }); + t.test("Should update a users class for DP.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: 17, play_style: 1 }); - t.equal(res.status, 200); - t.equal(res.body.success, true); - t.equal(res.body.description, "Dan changed!"); + t.equal(res.status, 200); + t.equal(res.body.success, true); + t.equal(res.body.description, "Dan changed!"); - const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "DP" }); + const ugs = await db["game-stats"].findOne({ userID: 1, game: "iidx", playtype: "DP" }); - t.equal(ugs?.classes.dan, 17); + t.equal(ugs?.classes.dan, 17); - t.end(); - }); + t.end(); + }); - t.test("Should ignore dans that weren't cleared.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: false, course_id: 17, play_style: 1 }); + t.test("Should ignore dans that weren't cleared.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: false, course_id: 17, play_style: 1 }); - t.equal(res.status, 200); - t.equal(res.body.description, "No Update Made."); + t.equal(res.status, 200); + t.equal(res.body.description, "No Update Made."); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid dans.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: null, play_style: 1 }); + t.test("Should reject invalid dans.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: null, play_style: 1 }); - t.equal(res.status, 400); - t.match(res.body.description, /Invalid course_id/u); + t.equal(res.status, 400); + t.match(res.body.description, /Invalid course_id/u); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid numerical dans.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: 20, play_style: 1 }); + t.test("Should reject invalid numerical dans.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: 20, play_style: 1 }); - t.equal(res.status, 400); - t.match(res.body.description, /Invalid course_id 20/u); + t.equal(res.status, 400); + t.match(res.body.description, /Invalid course_id 20/u); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid negative dans.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: -1, play_style: 1 }); + t.test("Should reject invalid negative dans.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: -1, play_style: 1 }); - t.equal(res.status, 400); - t.match(res.body.description, /Invalid course_id -1/u); + t.equal(res.status, 400); + t.match(res.body.description, /Invalid course_id -1/u); - t.end(); - }); + t.end(); + }); - t.test("Should reject invalid play_styles.", async (t) => { - const res = await mockApi - .post("/ir/fervidex/class/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send({ cleared: true, course_id: 16, play_style: null }); + t.test("Should reject invalid play_styles.", async (t) => { + const res = await mockApi + .post("/ir/fervidex/class/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send({ cleared: true, course_id: 16, play_style: null }); - t.equal(res.status, 400); - t.match(res.body.description, /Invalid play_style/u); + t.equal(res.status, 400); + t.match(res.body.description, /Invalid play_style/u); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("POST /ir/fervidex/score/submit", (t) => { - TestHeaders("/ir/fervidex/score/submit", GetKTDataJSON("./fervidex/base.json")); + TestHeaders("/ir/fervidex/score/submit", GetKTDataJSON("./fervidex/base.json")); - t.test("Should import a valid score", async (t) => { - const res = await mockApi - .post("/ir/fervidex/score/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(GetKTDataJSON("./fervidex/base.json")); + t.test("Should import a valid score", async (t) => { + const res = await mockApi + .post("/ir/fervidex/score/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(GetKTDataJSON("./fervidex/base.json")); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scores = await db.scores.count({ - service: "Fervidex", - }); + const scores = await db.scores.count({ + service: "Fervidex", + }); - t.equal(scores, 1, "Should import 1 score."); + t.equal(scores, 1, "Should import 1 score."); - t.end(); - }); + t.end(); + }); - t.test("Should import a valid score with 2dx-gsm", async (t) => { - const res = await mockApi - .post("/ir/fervidex/score/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "LDJ:J:B:A:2020092900") - .send(GetKTDataJSON("./fervidex/2dxgsm.json")); + t.test("Should import a valid score with 2dx-gsm", async (t) => { + const res = await mockApi + .post("/ir/fervidex/score/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(GetKTDataJSON("./fervidex/2dxgsm.json")); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - const scores = await db.scores.count({ - service: "Fervidex", - }); + const scores = await db.scores.count({ + service: "Fervidex", + }); - t.equal(scores, 1, "Should import 1 score."); + t.equal(scores, 1, "Should import 1 score."); - t.end(); - }); + t.end(); + }); - t.test("Should reject an invalid body", async (t) => { - const res = await mockApi - .post("/ir/fervidex/score/submit") - .set("User-Agent", "fervidex/1.3.0") - .set("Authorization", "Bearer mock_token") - .send({}); + t.test("Should reject an invalid body", async (t) => { + const res = await mockApi + .post("/ir/fervidex/score/submit") + .set("User-Agent", "fervidex/1.3.0") + .set("Authorization", "Bearer mock_token") + .send({}); - t.equal(res.body.success, false, "Should not be successful"); + t.equal(res.body.success, false, "Should not be successful"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("POST /ir/fervidex/profile/submit", (t) => { - TestHeaders("/ir/fervidex/class/submit", GetKTDataJSON("./fervidex-static/base.json")); + TestHeaders("/ir/fervidex/class/submit", GetKTDataJSON("./fervidex-static/base.json")); - const ferStaticBody = GetKTDataJSON("./fervidex-static/base.json"); + const ferStaticBody = GetKTDataJSON("./fervidex-static/base.json"); - t.test("Should accept a fervidex-static body", async (t) => { - await db.songs.iidx.remove({}); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); - await db.charts.iidx.remove({}); - await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); + t.test("Should accept a fervidex-static body", async (t) => { + await db.songs.iidx.remove({}); + await db.songs.iidx.insert(GetKTDataJSON("./tachi/ktblack-songs-iidx.json")); + await db.charts.iidx.remove({}); + await db.charts.iidx.insert(GetKTDataJSON("./tachi/ktblack-charts-iidx.json")); - const res = await mockApi - .post("/ir/fervidex/profile/submit") - .set("Authorization", "Bearer mock_token") - .set("User-Agent", "fervidex/1.3.0") - .set("X-Software-Model", "P2D:J:B:A:2020092900") - .send(ferStaticBody); + const res = await mockApi + .post("/ir/fervidex/profile/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "P2D:J:B:A:2020092900") + .send(ferStaticBody); - t.equal(res.body.success, true, "Should be successful"); + t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - t.strictSame( - res.body.body.classDeltas, - [ - { - set: "dan", - playtype: "SP", - old: null, - new: 15, - }, - ], - "Should return updated dan deltas." - ); + t.strictSame( + res.body.body.classDeltas, + [ + { + set: "dan", + playtype: "SP", + old: null, + new: 15, + }, + ], + "Should return updated dan deltas." + ); - const scores = await db.scores.count({ - service: "Fervidex Static", - }); + const scores = await db.scores.count({ + service: "Fervidex Static", + }); - t.equal(scores, 3, "Should import 3 scores."); + t.equal(scores, 3, "Should import 3 scores."); - const ugs = await db["game-stats"].findOne({ - userID: 1, - game: "iidx", - playtype: "SP", - }); + const ugs = await db["game-stats"].findOne({ + userID: 1, + game: "iidx", + playtype: "SP", + }); - t.equal(ugs!.classes.dan, 15, "Should successfully update dan to 9th."); + t.equal(ugs!.classes.dan, 15, "Should successfully update dan to 9th."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index 4746d6260..dcfa80ac1 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -17,138 +17,138 @@ const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); const ValidateFervidexHeader: RequestHandler = (req, res, next) => { - const agent = req.header("User-Agent"); + const agent = req.header("User-Agent"); - if (!agent) { - logger.debug( - `Rejected fervidex client with no agent from user ${req[SYMBOL_TachiAPIAuth].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid User-Agent.`, - }); - } + if (!agent) { + logger.debug( + `Rejected fervidex client with no agent from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid User-Agent.`, + }); + } - if (!agent.startsWith("fervidex/")) { - logger.info( - `Rejected fervidex client with invalid agent ${agent} from user ${req[ - SYMBOL_TachiAPIAuth - ].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid User-Agent ${agent} - expected fervidex client.`, - }); - } + if (!agent.startsWith("fervidex/")) { + logger.info( + `Rejected fervidex client with invalid agent ${agent} from user ${req[ + SYMBOL_TachiAPIAuth + ].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid User-Agent ${agent} - expected fervidex client.`, + }); + } - const versions = agent.split("fervidex/")[1].split(".").map(Number); + const versions = agent.split("fervidex/")[1].split(".").map(Number); - if (!versions.every((e) => !Number.isNaN(e))) { - logger.info( - `Rejected fervidex client with agent ${agent} for NaN-like versions from user ${req[ - SYMBOL_TachiAPIAuth - ].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid version ${versions.join(".")}.`, - }); - } + if (!versions.every((e) => !Number.isNaN(e))) { + logger.info( + `Rejected fervidex client with agent ${agent} for NaN-like versions from user ${req[ + SYMBOL_TachiAPIAuth + ].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid version ${versions.join(".")}.`, + }); + } - // version.minor - if (versions[1] < 3) { - logger.debug( - `Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIAuth].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Versions of fervidex < 1.3.0 are not supported.`, - }); - } + // version.minor + if (versions[1] < 3) { + logger.debug( + `Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Versions of fervidex < 1.3.0 are not supported.`, + }); + } - return next(); + return next(); }; const RequireInf2ModelHeader: RequestHandler = (req, res, next) => { - const swModel = req.header("X-Software-Model"); + const swModel = req.header("X-Software-Model"); - if (!swModel) { - logger.debug( - `Rejected empty X-Software-Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid X-Software-Model.`, - }); - } + if (!swModel) { + logger.debug( + `Rejected empty X-Software-Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); + } - try { - const softID = ParseEA3SoftID(swModel); + try { + const softID = ParseEA3SoftID(swModel); - if (softID.model !== MODEL_INFINITAS_2) { - logger.debug(`Rejected non-inf2 model from user ${req[SYMBOL_TachiAPIAuth].userID!}.`); - return res.status(400).send({ - success: false, - description: "This endpoint is only available for INF2 clients.", - }); - } - } catch (err) { - logger.debug(err); - return res.status(400).json({ - success: false, - description: `Invalid X-Software-Model.`, - }); - } + if (softID.model !== MODEL_INFINITAS_2) { + logger.debug(`Rejected non-inf2 model from user ${req[SYMBOL_TachiAPIAuth].userID!}.`); + return res.status(400).send({ + success: false, + description: "This endpoint is only available for INF2 clients.", + }); + } + } catch (err) { + logger.debug(err); + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); + } - return next(); + return next(); }; const ValidateModelHeader: RequestHandler = (req, res, next) => { - const swModel = req.header("X-Software-Model"); + const swModel = req.header("X-Software-Model"); - if (!swModel) { - logger.debug( - `Rejected empty X-Software Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid X-Software-Model.`, - }); - } + if (!swModel) { + logger.debug( + `Rejected empty X-Software Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); + } - try { - const softID = ParseEA3SoftID(swModel); + try { + const softID = ParseEA3SoftID(swModel); - if (softID.rev === REV_2DXBMS) { - return res.status(400).send({ - success: false, - description: "2DX_BMS is not supported.", - }); - } + if (softID.rev === REV_2DXBMS) { + return res.status(400).send({ + success: false, + description: "2DX_BMS is not supported.", + }); + } - if (softID.model === MODEL_INFINITAS_2) { - return next(); // allow anything for inf2. - } + if (softID.model === MODEL_INFINITAS_2) { + return next(); // allow anything for inf2. + } - if (softID.ext !== EXT_HEROIC_VERSE) { - logger.info( - `Rejected invalid Software Model ${softID.ext} from user ${req[SYMBOL_TachiAPIAuth] - .userID!}.` - ); - return res.status(400).json({ - success: false, - description: `Invalid extension ${softID.ext}`, - }); - } - } catch (err) { - logger.debug(err); - return res.status(400).json({ - success: false, - description: `Invalid X-Software-Model.`, - }); - } + if (softID.ext !== EXT_HEROIC_VERSE) { + logger.info( + `Rejected invalid Software Model ${softID.ext} from user ${req[SYMBOL_TachiAPIAuth] + .userID!}.` + ); + return res.status(400).json({ + success: false, + description: `Invalid extension ${softID.ext}`, + }); + } + } catch (err) { + logger.debug(err); + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); + } - return next(); + return next(); }; router.use(RequirePermissions("submit:score"), ValidateFervidexHeader, ValidateModelHeader); @@ -162,21 +162,21 @@ router.use(RequirePermissions("submit:score"), ValidateFervidexHeader, ValidateM * @name POST /ir/fervidex/profile/submit */ router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); - const headers = { - // guaranteed to exist because of RequireInf2ModelHeader - model: req.header("X-Software-Model")!, - }; + const headers = { + // guaranteed to exist because of RequireInf2ModelHeader + model: req.header("X-Software-Model")!, + }; - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - false, - "ir/fervidex-static", - (logger) => ParseFervidexStatic(req.body, headers, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + false, + "ir/fervidex-static", + (logger) => ParseFervidexStatic(req.body, headers, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); + return res.status(responseData.statusCode).json(responseData.body); }); /** @@ -187,29 +187,29 @@ router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => { * @name POST /ir/fervidex/score/submit */ router.post("/score/submit", ValidateModelHeader, async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); - const model = req.header("X-Software-Model"); + const model = req.header("X-Software-Model"); - if (!model) { - return res.status(400).json({ - success: false, - description: "No X-Software-Model header provided?", - }); - } + if (!model) { + return res.status(400).json({ + success: false, + description: "No X-Software-Model header provided?", + }); + } - const headers = { - model, - }; + const headers = { + model, + }; - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - true, - "ir/fervidex", - (logger) => ParseFervidexSingle(req.body, headers, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + true, + "ir/fervidex", + (logger) => ParseFervidexSingle(req.body, headers, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); + return res.status(responseData.statusCode).json(responseData.body); }); /** @@ -219,48 +219,48 @@ router.post("/score/submit", ValidateModelHeader, async (req, res) => { * @name POST /ir/fervidex/class/submit */ router.post("/class/submit", ValidateModelHeader, async (req, res) => { - if (!req.body.cleared) { - return res.status(200).json({ success: true, description: "No Update Made.", body: {} }); - } + if (!req.body.cleared) { + return res.status(200).json({ success: true, description: "No Update Made.", body: {} }); + } - if (!Number.isInteger(req.body.course_id)) { - return res.status(400).json({ - success: false, - description: `Invalid course_id ${req.body.course_id}.`, - }); - } + if (!Number.isInteger(req.body.course_id)) { + return res.status(400).json({ + success: false, + description: `Invalid course_id ${req.body.course_id}.`, + }); + } - const courseID = req.body.course_id as integer; + const courseID = req.body.course_id as integer; - if (courseID < 0 || courseID > 18) { - return res.status(400).json({ - success: false, - description: `Invalid course_id ${req.body.course_id}.`, - }); - } + if (courseID < 0 || courseID > 18) { + return res.status(400).json({ + success: false, + description: `Invalid course_id ${req.body.course_id}.`, + }); + } - if (req.body.play_style !== 0 && req.body.play_style !== 1) { - return res.status(400).json({ - success: false, - description: `Invalid play_style ${req.body.playstyle}`, - }); - } + if (req.body.play_style !== 0 && req.body.play_style !== 1) { + return res.status(400).json({ + success: false, + description: `Invalid play_style ${req.body.playstyle}`, + }); + } - // is 0 or 1. - const playtype: Playtypes["iidx"] = req.body.play_style === 0 ? "SP" : "DP"; + // is 0 or 1. + const playtype: Playtypes["iidx"] = req.body.play_style === 0 ? "SP" : "DP"; - const r = await UpdateClassIfGreater( - req[SYMBOL_TachiAPIAuth].userID!, - "iidx", - playtype, - "dan", - courseID - ); + const r = await UpdateClassIfGreater( + req[SYMBOL_TachiAPIAuth].userID!, + "iidx", + playtype, + "dan", + courseID + ); - return res.status(200).json({ - success: true, - description: r === false ? "Dan unchanged." : "Dan changed!", - }); + return res.status(200).json({ + success: true, + description: r === false ? "Dan unchanged." : "Dan changed!", + }); }); export default router; diff --git a/server/src/server/router/ir/usc/router.test.ts b/server/src/server/router/ir/usc/router.test.ts index 7191a80b1..f733311d0 100644 --- a/server/src/server/router/ir/usc/router.test.ts +++ b/server/src/server/router/ir/usc/router.test.ts @@ -9,453 +9,453 @@ import { GetKTDataBuffer } from "../../../../test-utils/test-data"; import { RetrieveCDN } from "../../../../lib/cdn/cdn"; async function InsertFakeUSCAuth() { - await db["api-tokens"].insert({ - userID: 1, - identifier: "USC Token", - permissions: { - "submit:score": true, - }, - token: "foo", - }); + await db["api-tokens"].insert({ + userID: 1, + identifier: "USC Token", + permissions: { + "submit:score": true, + }, + token: "foo", + }); } function TestAuth(url: string) { - t.test(`Authorization Check ${url}`, async (t) => { - const res = await mockApi.get(url).set("Authorization", "Bearer invalid"); + t.test(`Authorization Check ${url}`, async (t) => { + const res = await mockApi.get(url).set("Authorization", "Bearer invalid"); - t.equal(res.body.statusCode, 41, "Should return 41 for nonsense token"); + t.equal(res.body.statusCode, 41, "Should return 41 for nonsense token"); - const res2 = await mockApi.get(url).set("Authorization", "NOTBEARER invalid"); + const res2 = await mockApi.get(url).set("Authorization", "NOTBEARER invalid"); - t.equal(res2.body.statusCode, 40, "Should return 40 for nonsense authtype"); + t.equal(res2.body.statusCode, 40, "Should return 40 for nonsense authtype"); - const res3 = await mockApi.get(url); + const res3 = await mockApi.get(url); - t.equal(res3.body.statusCode, 40, "Should return 40 for no auth header."); + t.equal(res3.body.statusCode, 40, "Should return 40 for no auth header."); - const res4 = await mockApi.get(url).set("Authorization", "Bearer foo invalid"); + const res4 = await mockApi.get(url).set("Authorization", "Bearer foo invalid"); - t.equal(res4.body.statusCode, 40, "Should return 40 for nonsense header"); - }); + t.equal(res4.body.statusCode, 40, "Should return 40 for nonsense header"); + }); } t.test("GET /ir/usc", async (t) => { - await db["api-tokens"].insert({ - userID: 1, - identifier: "USC Token", - permissions: { - "submit:score": true, - }, - token: "foo", - }); + await db["api-tokens"].insert({ + userID: 1, + identifier: "USC Token", + permissions: { + "submit:score": true, + }, + token: "foo", + }); - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - TestAuth("/ir/usc"); + TestAuth("/ir/usc"); - const res = await mockApi.get("/ir/usc").set("Authorization", "Bearer foo"); + const res = await mockApi.get("/ir/usc").set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 20, "Should return 20"); - t.match( - res.body.body, - { - serverName: /tachi/iu, - irVersion: /^[0-9]\.[0-9]\.[0-9](-a)?$/iu, - }, - "Should return the right body." - ); + t.equal(res.body.statusCode, 20, "Should return 20"); + t.match( + res.body.body, + { + serverName: /tachi/iu, + irVersion: /^[0-9]\.[0-9]\.[0-9](-a)?$/iu, + }, + "Should return the right body." + ); - t.end(); + t.end(); }); t.test("GET /ir/usc/charts/:chartHash", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(InsertFakeUSCAuth); + t.beforeEach(ResetDBState); + t.beforeEach(InsertFakeUSCAuth); - t.test("Should return 20 if the chartHash matches a chart.", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH") - .set("Authorization", "Bearer foo"); + t.test("Should return 20 if the chartHash matches a chart.", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 20, "Should return 20"); + t.equal(res.body.statusCode, 20, "Should return 20"); - t.end(); - }); + t.end(); + }); - t.test("Should return 42 if the chartHash doesn't match a chart.", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/INVALID_HASH") - .set("Authorization", "Bearer foo"); + t.test("Should return 42 if the chartHash doesn't match a chart.", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/INVALID_HASH") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 42, "Should return 42"); + t.equal(res.body.statusCode, 42, "Should return 42"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); const USC_SCORE_PB: PBScoreDocument = { - chartID: "USC_CHART_ID", - comments: [], - rankingData: { - rank: 1, - outOf: 2, - }, - songID: 1, - userID: 1, - timeAchieved: 0, - playtype: "Single", - game: "usc", - highlight: false, - composedFrom: { - scorePB: "usc_score_pb", - lampPB: "bar", - }, - calculatedData: { - VF6: null, - }, - isPrimary: true, - scoreData: { - score: 9_000_000, - percent: 90, - grade: "A+", - esd: null, - lamp: "EXCESSIVE CLEAR", - lampIndex: 2, - gradeIndex: 4, // idk, lazy - hitData: { - critical: 50, - near: 30, - miss: 10, - }, - hitMeta: { - gauge: 50, - }, - }, + chartID: "USC_CHART_ID", + comments: [], + rankingData: { + rank: 1, + outOf: 2, + }, + songID: 1, + userID: 1, + timeAchieved: 0, + playtype: "Single", + game: "usc", + highlight: false, + composedFrom: { + scorePB: "usc_score_pb", + lampPB: "bar", + }, + calculatedData: { + VF6: null, + }, + isPrimary: true, + scoreData: { + score: 9_000_000, + percent: 90, + grade: "A+", + esd: null, + lamp: "EXCESSIVE CLEAR", + lampIndex: 2, + gradeIndex: 4, // idk, lazy + hitData: { + critical: 50, + near: 30, + miss: 10, + }, + hitMeta: { + gauge: 50, + }, + }, }; t.test("GET /ir/usc/:chartHash/record", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(InsertFakeUSCAuth); - TestAuth("/ir/usc/:chartHash/record"); + t.beforeEach(ResetDBState); + t.beforeEach(InsertFakeUSCAuth); + TestAuth("/ir/usc/:chartHash/record"); - t.test("Should return 42 if the chartHash doesn't match a chart.", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/INVALID_HASH/record") - .set("Authorization", "Bearer foo"); + t.test("Should return 42 if the chartHash doesn't match a chart.", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/INVALID_HASH/record") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 42, "Should return 42"); + t.equal(res.body.statusCode, 42, "Should return 42"); - t.end(); - }); + t.end(); + }); - t.test("Should return 44 if there are no scores on the chart.", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/record") - .set("Authorization", "Bearer foo"); + t.test("Should return 44 if there are no scores on the chart.", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/record") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 44, "Should return 42"); + t.equal(res.body.statusCode, 44, "Should return 42"); - t.end(); - }); + t.end(); + }); - t.test("Should return 20 and the Server Record.", async (t) => { - await db["personal-bests"].insert([ - // empty deepmerge is because monk monkey-patches _id on, - // which means this crashes if you try to re-insert this document. - deepmerge(USC_SCORE_PB, {}), - deepmerge(USC_SCORE_PB, { userID: 2, rankingData: { rank: 2 } }), - ]); + t.test("Should return 20 and the Server Record.", async (t) => { + await db["personal-bests"].insert([ + // empty deepmerge is because monk monkey-patches _id on, + // which means this crashes if you try to re-insert this document. + deepmerge(USC_SCORE_PB, {}), + deepmerge(USC_SCORE_PB, { userID: 2, rankingData: { rank: 2 } }), + ]); - // hack for referencing - await db.scores.insert({ - scoreID: "usc_score_pb", - scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, - } as any); + // hack for referencing + await db.scores.insert({ + scoreID: "usc_score_pb", + scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, + } as any); - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/record") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/record") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 20, "Should return 20"); + t.equal(res.body.statusCode, 20, "Should return 20"); - t.strictSame( - res.body.body, - { - score: 9_000_000, - timestamp: 0, - crit: 50, - near: 30, - error: 10, - ranking: 1, - lamp: 3, - username: "test_zkldi", - noteMod: "NORMAL", - gaugeMod: "HARD", - }, - "Should correctly return the right score." - ); + t.strictSame( + res.body.body, + { + score: 9_000_000, + timestamp: 0, + crit: 50, + near: 30, + error: 10, + ranking: 1, + lamp: 3, + username: "test_zkldi", + noteMod: "NORMAL", + gaugeMod: "HARD", + }, + "Should correctly return the right score." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("GET /charts/:chartHash/leaderboard", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(InsertFakeUSCAuth); - TestAuth("/ir/usc/:chartHash/leaderboard"); + t.beforeEach(ResetDBState); + t.beforeEach(InsertFakeUSCAuth); + TestAuth("/ir/usc/:chartHash/leaderboard"); - t.test("Should return 40 if mode is invalid", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard") - .set("Authorization", "Bearer foo"); + t.test("Should return 40 if mode is invalid", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 40); + t.equal(res.body.statusCode, 40); - const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=invalid") - .set("Authorization", "Bearer foo"); + const res2 = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=invalid") + .set("Authorization", "Bearer foo"); - t.equal(res2.body.statusCode, 40); + t.equal(res2.body.statusCode, 40); - t.end(); - }); + t.end(); + }); - t.test("Should return 40 if N is invalid", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best") - .set("Authorization", "Bearer foo"); + t.test("Should return 40 if N is invalid", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 40); + t.equal(res.body.statusCode, 40); - const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=foo") - .set("Authorization", "Bearer foo"); + const res2 = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=foo") + .set("Authorization", "Bearer foo"); - t.equal(res2.body.statusCode, 40); + t.equal(res2.body.statusCode, 40); - t.end(); - }); + t.end(); + }); - t.test("Should return empty arr for mode = best if no scores", async (t) => { - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=5") - .set("Authorization", "Bearer foo"); + t.test("Should return empty arr for mode = best if no scores", async (t) => { + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=5") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 20); + t.equal(res.body.statusCode, 20); - t.end(); - }); + t.end(); + }); - t.test("Should return scorePBs for mode = best", async (t) => { - await db["personal-bests"].insert([ - deepmerge(USC_SCORE_PB, {}), - deepmerge(USC_SCORE_PB, { - userID: 2, - scoreData: { - score: 8_000_000, - percent: 80, - }, - rankingData: { rank: 2 }, - composedFrom: { scorePB: "other_usc_score_pb" }, - }), - ]); + t.test("Should return scorePBs for mode = best", async (t) => { + await db["personal-bests"].insert([ + deepmerge(USC_SCORE_PB, {}), + deepmerge(USC_SCORE_PB, { + userID: 2, + scoreData: { + score: 8_000_000, + percent: 80, + }, + rankingData: { rank: 2 }, + composedFrom: { scorePB: "other_usc_score_pb" }, + }), + ]); - await db.users.insert({ - id: 2, - username: "not_zkldi", - } as any); + await db.users.insert({ + id: 2, + username: "not_zkldi", + } as any); - await db.scores.insert([ - { - scoreID: "usc_score_pb", - scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, - }, - { - scoreID: "other_usc_score_pb", - scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, - }, - ] as any); + await db.scores.insert([ + { + scoreID: "usc_score_pb", + scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, + }, + { + scoreID: "other_usc_score_pb", + scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, + }, + ] as any); - const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=2") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=2") + .set("Authorization", "Bearer foo"); - t.equal(res.body.statusCode, 20); - t.strictSame( - res.body.body, - [ - { - score: 9000000, - timestamp: 0, - crit: 50, - near: 30, - error: 10, - ranking: 1, - lamp: 3, - username: "test_zkldi", - noteMod: "NORMAL", - gaugeMod: "HARD", - }, - { - score: 8000000, - timestamp: 0, - crit: 50, - near: 30, - error: 10, - ranking: 2, - lamp: 3, - username: "not_zkldi", - noteMod: "NORMAL", - gaugeMod: "HARD", - }, - ], - "Should return the scores." - ); + t.equal(res.body.statusCode, 20); + t.strictSame( + res.body.body, + [ + { + score: 9000000, + timestamp: 0, + crit: 50, + near: 30, + error: 10, + ranking: 1, + lamp: 3, + username: "test_zkldi", + noteMod: "NORMAL", + gaugeMod: "HARD", + }, + { + score: 8000000, + timestamp: 0, + crit: 50, + near: 30, + error: 10, + ranking: 2, + lamp: 3, + username: "not_zkldi", + noteMod: "NORMAL", + gaugeMod: "HARD", + }, + ], + "Should return the scores." + ); - const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=1") - .set("Authorization", "Bearer foo"); + const res2 = await mockApi + .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=1") + .set("Authorization", "Bearer foo"); - t.equal(res2.body.statusCode, 20); - t.strictSame( - res2.body.body, - [ - { - score: 9000000, - timestamp: 0, - crit: 50, - near: 30, - error: 10, - ranking: 1, - lamp: 3, - username: "test_zkldi", - noteMod: "NORMAL", - gaugeMod: "HARD", - }, - ], - "Should return the scores dependent on N." - ); + t.equal(res2.body.statusCode, 20); + t.strictSame( + res2.body.body, + [ + { + score: 9000000, + timestamp: 0, + crit: 50, + near: 30, + error: 10, + ranking: 1, + lamp: 3, + username: "test_zkldi", + noteMod: "NORMAL", + gaugeMod: "HARD", + }, + ], + "Should return the scores dependent on N." + ); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("POST /replays", (t) => { - t.beforeEach(ResetDBState); - t.beforeEach(ResetCDN); - t.beforeEach(InsertFakeUSCAuth); + t.beforeEach(ResetDBState); + t.beforeEach(ResetCDN); + t.beforeEach(InsertFakeUSCAuth); - t.test("Should successfully upload a file where an identifier matches", async (t) => { - await db.scores.insert({ - game: "usc", - userID: 1, - scoreID: "MOCK_IDENTIFIER", - } as ScoreDocument); + t.test("Should successfully upload a file where an identifier matches", async (t) => { + await db.scores.insert({ + game: "usc", + userID: 1, + scoreID: "MOCK_IDENTIFIER", + } as ScoreDocument); - const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); + const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); - const res = await mockApi - .post("/ir/usc/replays") - .field("identifier", "MOCK_IDENTIFIER") - .attach("replay", replayFile, "replay.urf") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .post("/ir/usc/replays") + .field("identifier", "MOCK_IDENTIFIER") + .attach("replay", replayFile, "replay.urf") + .set("Authorization", "Bearer foo"); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.strictSame(res.body, { - statusCode: 20, - description: "Saved replay.", - body: null, - }); + t.strictSame(res.body, { + statusCode: 20, + description: "Saved replay.", + body: null, + }); - const stored = await RetrieveCDN("/uscir/replays/MOCK_IDENTIFIER"); + const stored = await RetrieveCDN("/uscir/replays/MOCK_IDENTIFIER"); - t.strictSame(stored, replayFile, "Should store the same file exactly."); + t.strictSame(stored, replayFile, "Should store the same file exactly."); - t.end(); - }); + t.end(); + }); - t.test("Should reject a request with no identifier", async (t) => { - await db.scores.insert({ - game: "usc", - userID: 1, - scoreID: "MOCK_IDENTIFIER", - } as ScoreDocument); + t.test("Should reject a request with no identifier", async (t) => { + await db.scores.insert({ + game: "usc", + userID: 1, + scoreID: "MOCK_IDENTIFIER", + } as ScoreDocument); - const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); + const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); - const res = await mockApi - .post("/ir/usc/replays") - .attach("replay", replayFile, "replay.urf") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .post("/ir/usc/replays") + .attach("replay", replayFile, "replay.urf") + .set("Authorization", "Bearer foo"); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.strictSame(res.body, { - statusCode: 40, - description: "No Identifier Provided.", - }); + t.strictSame(res.body, { + statusCode: 40, + description: "No Identifier Provided.", + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject a request with no file", async (t) => { - await db.scores.insert({ - game: "usc", - userID: 1, - scoreID: "MOCK_IDENTIFIER", - } as ScoreDocument); + t.test("Should reject a request with no file", async (t) => { + await db.scores.insert({ + game: "usc", + userID: 1, + scoreID: "MOCK_IDENTIFIER", + } as ScoreDocument); - // const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); + // const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); - const res = await mockApi - .post("/ir/usc/replays") - .field("identifier", "MOCK_IDENTIFIER") - // .attach("replay", replayFile, "replay.urf") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .post("/ir/usc/replays") + .field("identifier", "MOCK_IDENTIFIER") + // .attach("replay", replayFile, "replay.urf") + .set("Authorization", "Bearer foo"); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.strictSame(res.body, { - statusCode: 40, - description: "No File Provided.", - }); + t.strictSame(res.body, { + statusCode: 40, + description: "No File Provided.", + }); - t.end(); - }); + t.end(); + }); - t.test("Should reject a request with an invalid identifier", async (t) => { - await db.scores.insert({ - game: "usc", - userID: 1, - scoreID: "MOCK_IDENTIFIER", - } as ScoreDocument); + t.test("Should reject a request with an invalid identifier", async (t) => { + await db.scores.insert({ + game: "usc", + userID: 1, + scoreID: "MOCK_IDENTIFIER", + } as ScoreDocument); - const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); + const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); - const res = await mockApi - .post("/ir/usc/replays") - .field("identifier", "INVALID_IDENTIFIER") - .attach("replay", replayFile, "replay.urf") - .set("Authorization", "Bearer foo"); + const res = await mockApi + .post("/ir/usc/replays") + .field("identifier", "INVALID_IDENTIFIER") + .attach("replay", replayFile, "replay.urf") + .set("Authorization", "Bearer foo"); - t.equal(res.status, 200); + t.equal(res.status, 200); - t.strictSame(res.body, { - statusCode: 44, - description: "No score corresponds to this identifier.", - }); + t.strictSame(res.body, { + statusCode: 44, + description: "No score corresponds to this identifier.", + }); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/usc/router.ts b/server/src/server/router/ir/usc/router.ts index 985c21bb0..0a6a2ff62 100644 --- a/server/src/server/router/ir/usc/router.ts +++ b/server/src/server/router/ir/usc/router.ts @@ -3,10 +3,10 @@ import { FindChartOnSHA256 } from "../../../../utils/queries/charts"; import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "../../../../lib/constants/tachi"; import db from "../../../../external/mongo/db"; import { - ChartDocument, - PBScoreDocument, - SuccessfulAPIResponse, - ImportDocument, + ChartDocument, + PBScoreDocument, + SuccessfulAPIResponse, + ImportDocument, } from "tachi-common"; import { AssertStrAsPositiveNonZeroInt } from "../../../../lib/score-import/framework/common/string-asserts"; import CreateLogCtx, { KtLogger } from "../../../../lib/logger/logger"; @@ -26,48 +26,48 @@ const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); const STATUS_CODES = { - UNAUTH: 41, - CHART_REFUSE: 42, - FORBIDDEN: 43, - NOT_FOUND: 44, - SERVER_ERROR: 50, - SUCCESS: 20, - BAD_REQ: 40, + UNAUTH: 41, + CHART_REFUSE: 42, + FORBIDDEN: 43, + NOT_FOUND: 44, + SERVER_ERROR: 50, + SUCCESS: 20, + BAD_REQ: 40, }; const ValidateUSCRequest: RequestHandler = async (req, res, next) => { - const token = req.header("Authorization"); + const token = req.header("Authorization"); - if (!token) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "No auth token provided.", - }); - } + if (!token) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "No auth token provided.", + }); + } - const splitToken = token.split(" "); + const splitToken = token.split(" "); - if (splitToken.length !== 2 || splitToken[0] !== "Bearer") { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "Invalid Authorization Header. Expected Bearer ", - }); - } + if (splitToken.length !== 2 || splitToken[0] !== "Bearer") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "Invalid Authorization Header. Expected Bearer ", + }); + } - const uscAuthDoc = await db["api-tokens"].findOne({ - token: splitToken[1], - }); + const uscAuthDoc = await db["api-tokens"].findOne({ + token: splitToken[1], + }); - if (!uscAuthDoc) { - return res.status(200).json({ - statusCode: STATUS_CODES.UNAUTH, - description: "Unauthorized.", - }); - } + if (!uscAuthDoc) { + return res.status(200).json({ + statusCode: STATUS_CODES.UNAUTH, + description: "Unauthorized.", + }); + } - req[SYMBOL_TachiAPIAuth] = uscAuthDoc; + req[SYMBOL_TachiAPIAuth] = uscAuthDoc; - return next(); + return next(); }; router.use(ValidateUSCRequest); @@ -82,25 +82,25 @@ router.use(ValidateUSCRequest); * @name GET /ir/usc */ router.get("/", (req, res) => - res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: "IR Request Successful.", - body: { - serverTime: Math.floor(Date.now() / 1000), - serverName: "Bokutachi", - irVersion: "0.3.1-a", - }, - }) + res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "IR Request Successful.", + body: { + serverTime: Math.floor(Date.now() / 1000), + serverName: "Bokutachi", + irVersion: "0.3.1-a", + }, + }) ); const RetrieveChart: RequestHandler = async (req, res, next) => { - const chart = await FindChartOnSHA256("usc", req.params.chartHash); + const chart = await FindChartOnSHA256("usc", req.params.chartHash); - AssignToReqTachiData(req, { - uscChartDoc: (chart ?? undefined) as ChartDocument<"usc:Single"> | undefined, - }); + AssignToReqTachiData(req, { + uscChartDoc: (chart ?? undefined) as ChartDocument<"usc:Single"> | undefined, + }); - return next(); + return next(); }; /** @@ -109,19 +109,19 @@ const RetrieveChart: RequestHandler = async (req, res, next) => { * @name GET /ir/usc/charts/:chartHash */ router.get("/charts/:chartHash", RetrieveChart, (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc; + const chart = req[SYMBOL_TachiData]!.uscChartDoc; - if (!chart) { - return res.status(200).json({ - statusCode: STATUS_CODES.CHART_REFUSE, - description: "This chart is not tracked, and will not be accepted.", - }); - } + if (!chart) { + return res.status(200).json({ + statusCode: STATUS_CODES.CHART_REFUSE, + description: "This chart is not tracked, and will not be accepted.", + }); + } - return res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: "This chart is tracked by the IR.", - }); + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "This chart is tracked by the IR.", + }); }); /** @@ -130,36 +130,36 @@ router.get("/charts/:chartHash", RetrieveChart, (req, res) => { * @name GET /ir/usc/charts/:chartHash/record */ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc; + const chart = req[SYMBOL_TachiData]!.uscChartDoc; - // spec ambigious here + // spec ambigious here - if (!chart) { - return res.status(200).json({ - statusCode: STATUS_CODES.CHART_REFUSE, - description: "This IR is not currently tracking this chart.", - }); - } + if (!chart) { + return res.status(200).json({ + statusCode: STATUS_CODES.CHART_REFUSE, + description: "This IR is not currently tracking this chart.", + }); + } - const serverRecord = (await db["personal-bests"].findOne({ - chartID: chart.chartID, - "rankingData.rank": 1, - })) as PBScoreDocument<"usc:Single"> | null; + const serverRecord = (await db["personal-bests"].findOne({ + chartID: chart.chartID, + "rankingData.rank": 1, + })) as PBScoreDocument<"usc:Single"> | null; - if (!serverRecord) { - return res.status(200).json({ - statusCode: STATUS_CODES.NOT_FOUND, - description: "No server record found.", - }); - } + if (!serverRecord) { + return res.status(200).json({ + statusCode: STATUS_CODES.NOT_FOUND, + description: "No server record found.", + }); + } - const serverScore = await TachiScoreToServerScore(serverRecord); + const serverScore = await TachiScoreToServerScore(serverRecord); - return res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: "Retrieved score.", - body: serverScore, - }); + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "Retrieved score.", + body: serverScore, + }); }); /** @@ -168,65 +168,65 @@ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { * @name GET /ir/usc/charts/:chartHash/leaderboard */ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc!; + const chart = req[SYMBOL_TachiData]!.uscChartDoc!; - if (!(typeof req.query.mode === "string" && ["best", "rivals"].includes(req.query.mode))) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: `Invalid 'mode' param - expected 'best' or 'rivals'.`, - }); - } + if (!(typeof req.query.mode === "string" && ["best", "rivals"].includes(req.query.mode))) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'mode' param - expected 'best' or 'rivals'.`, + }); + } - if (typeof req.query.n !== "string") { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${USCIR_MAX_LEADERBOARD_N}.`, - }); - } + if (typeof req.query.n !== "string") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${USCIR_MAX_LEADERBOARD_N}.`, + }); + } - let n; + let n; - try { - n = AssertStrAsPositiveNonZeroInt(req.query.n, "Invalid 'N' param."); - } catch (err) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${USCIR_MAX_LEADERBOARD_N}.`, - }); - } + try { + n = AssertStrAsPositiveNonZeroInt(req.query.n, "Invalid 'N' param."); + } catch (err) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${USCIR_MAX_LEADERBOARD_N}.`, + }); + } - if (n >= USCIR_MAX_LEADERBOARD_N) { - n = USCIR_MAX_LEADERBOARD_N; - } + if (n >= USCIR_MAX_LEADERBOARD_N) { + n = USCIR_MAX_LEADERBOARD_N; + } - const mode = req.query.mode as "best" | "rivals"; + const mode = req.query.mode as "best" | "rivals"; - if (mode === "rivals") { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "This is currently unsupported.", - }); - } + if (mode === "rivals") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "This is currently unsupported.", + }); + } - const bestScores = (await db["personal-bests"].find( - { - chartID: chart.chartID, - }, - { - sort: { - "scoreData.perecent": -1, - }, - limit: n, - } - )) as PBScoreDocument<"usc:Single">[]; + const bestScores = (await db["personal-bests"].find( + { + chartID: chart.chartID, + }, + { + sort: { + "scoreData.perecent": -1, + }, + limit: n, + } + )) as PBScoreDocument<"usc:Single">[]; - const serverScores = await Promise.all(bestScores.map(TachiScoreToServerScore)); + const serverScores = await Promise.all(bestScores.map(TachiScoreToServerScore)); - return res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: `Returned ${serverScores.length} scores.`, - body: serverScores, - }); + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: `Returned ${serverScores.length} scores.`, + body: serverScores, + }); }); /** @@ -235,78 +235,78 @@ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => * @name POST /ir/usc/scores */ router.post("/scores", RequirePermissions("submit:score"), async (req, res) => { - if (typeof req.body.chart !== "object" || req.body.chart === null) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "Invalid chart provided.", - }); - } + if (typeof req.body.chart !== "object" || req.body.chart === null) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "Invalid chart provided.", + }); + } - if (typeof req.body.chart.chartHash !== "string") { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "Invalid chart provided.", - }); - } + if (typeof req.body.chart.chartHash !== "string") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "Invalid chart provided.", + }); + } - const chartDoc = (await FindChartOnSHA256( - "usc", - req.body.chart.chartHash - )) as ChartDocument<"usc:Single"> | null; + const chartDoc = (await FindChartOnSHA256( + "usc", + req.body.chart.chartHash + )) as ChartDocument<"usc:Single"> | null; - if (!chartDoc) { - return res.status(200).json({ - statusCode: STATUS_CODES.CHART_REFUSE, - description: "This chart is not supported.", - }); - } + if (!chartDoc) { + return res.status(200).json({ + statusCode: STATUS_CODES.CHART_REFUSE, + description: "This chart is not supported.", + }); + } - const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIAuth]!.userID!); + const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIAuth]!.userID!); - if (!userDoc) { - logger.severe(`User ${req[SYMBOL_TachiAPIAuth]!.userID!} as no parent userDoc?`); - return res.status(200).json({ - statusCode: STATUS_CODES.SERVER_ERROR, - description: "An internal server error has occured.", - }); - } + if (!userDoc) { + logger.severe(`User ${req[SYMBOL_TachiAPIAuth]!.userID!} as no parent userDoc?`); + return res.status(200).json({ + statusCode: STATUS_CODES.SERVER_ERROR, + description: "An internal server error has occured.", + }); + } - const importParser = (logger: KtLogger) => ParseIRUSC(req.body, chartDoc, logger); + const importParser = (logger: KtLogger) => ParseIRUSC(req.body, chartDoc, logger); - const importRes = await ExpressWrappedScoreImportMain(userDoc, false, "ir/usc", importParser); + const importRes = await ExpressWrappedScoreImportMain(userDoc, false, "ir/usc", importParser); - if (importRes.statusCode === 500) { - return res.status(200).json({ - statusCode: STATUS_CODES.SERVER_ERROR, - description: importRes.body.description, - }); - } else if (importRes.statusCode !== 200) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: importRes.body.description, - }); - } + if (importRes.statusCode === 500) { + return res.status(200).json({ + statusCode: STATUS_CODES.SERVER_ERROR, + description: importRes.body.description, + }); + } else if (importRes.statusCode !== 200) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: importRes.body.description, + }); + } - const importDoc = (importRes.body as SuccessfulAPIResponse).body as ImportDocument; + const importDoc = (importRes.body as SuccessfulAPIResponse).body as ImportDocument; - try { - const body = await CreatePOSTScoresResponseBody( - userDoc.id, - chartDoc, - importDoc.scoreIDs[0] - ); + try { + const body = await CreatePOSTScoresResponseBody( + userDoc.id, + chartDoc, + importDoc.scoreIDs[0] + ); - return res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: "Successfully imported score.", - body, - }); - } catch (err) { - return res.status(200).json({ - statusCode: STATUS_CODES.SERVER_ERROR, - description: "An internal server error has occured.", - }); - } + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "Successfully imported score.", + body, + }); + } catch (err) { + return res.status(200).json({ + statusCode: STATUS_CODES.SERVER_ERROR, + description: "An internal server error has occured.", + }); + } }); /** @@ -315,56 +315,56 @@ router.post("/scores", RequirePermissions("submit:score"), async (req, res) => { * @name POST /ir/usc/replays */ router.post( - "/replays", - RequirePermissions("submit:score"), - CreateMulterSingleUploadMiddleware("replay", ONE_MEGABYTE, logger), - async (req, res) => { - if (typeof req.body.identifier !== "string") { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "No Identifier Provided.", - }); - } + "/replays", + RequirePermissions("submit:score"), + CreateMulterSingleUploadMiddleware("replay", ONE_MEGABYTE, logger), + async (req, res) => { + if (typeof req.body.identifier !== "string") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "No Identifier Provided.", + }); + } - if (!req.file) { - return res.status(200).json({ - statusCode: STATUS_CODES.BAD_REQ, - description: "No File Provided.", - }); - } + if (!req.file) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "No File Provided.", + }); + } - const correspondingScore = await db.scores.findOne({ - userID: req[SYMBOL_TachiAPIAuth]!.userID!, - game: "usc", - scoreID: req.body.identifier, - }); + const correspondingScore = await db.scores.findOne({ + userID: req[SYMBOL_TachiAPIAuth]!.userID!, + game: "usc", + scoreID: req.body.identifier, + }); - if (!correspondingScore) { - return res.status(200).json({ - statusCode: STATUS_CODES.NOT_FOUND, - description: "No score corresponds to this identifier.", - }); - } + if (!correspondingScore) { + return res.status(200).json({ + statusCode: STATUS_CODES.NOT_FOUND, + description: "No score corresponds to this identifier.", + }); + } - try { - await StoreCDN(`/uscir/replays/${correspondingScore.scoreID}`, req.file.buffer); + try { + await StoreCDN(`/uscir/replays/${correspondingScore.scoreID}`, req.file.buffer); - return res.status(200).json({ - statusCode: STATUS_CODES.SUCCESS, - description: "Saved replay.", - body: null, - }); - } catch (err) { - // impossible to test pretty much. - /* istanbul ignore next */ - logger.error(`USCIR Replay Store error.`, { err }); - /* istanbul ignore next */ - return res.status(200).json({ - statusCode: STATUS_CODES.SERVER_ERROR, - description: "An error has occured in storing the replay.", - }); - } - } + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "Saved replay.", + body: null, + }); + } catch (err) { + // impossible to test pretty much. + /* istanbul ignore next */ + logger.error(`USCIR Replay Store error.`, { err }); + /* istanbul ignore next */ + return res.status(200).json({ + statusCode: STATUS_CODES.SERVER_ERROR, + description: "An error has occured in storing the replay.", + }); + } + } ); export default router; diff --git a/server/src/server/router/ir/usc/types.ts b/server/src/server/router/ir/usc/types.ts index b2106c5da..8558f48dc 100644 --- a/server/src/server/router/ir/usc/types.ts +++ b/server/src/server/router/ir/usc/types.ts @@ -1,48 +1,48 @@ import { integer } from "tachi-common"; export interface USCServerScore { - score: integer; - lamp: 0 | 1 | 2 | 3 | 4 | 5; - timestamp: integer; - crit: integer; - near: integer; - error: integer; - ranking: integer; - gaugeMod: "NORMAL" | "HARD"; - noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN"; - username: string; + score: integer; + lamp: 0 | 1 | 2 | 3 | 4 | 5; + timestamp: integer; + crit: integer; + near: integer; + error: integer; + ranking: integer; + gaugeMod: "NORMAL" | "HARD"; + noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN"; + username: string; } export interface USCClientScore { - score: integer; - gauge: number; - timestamp: integer; - crit: integer; - near: integer; - error: integer; - options: { - gaugeType: 0 | 1; - gaugeOpt: integer; - mirror: boolean; - random: boolean; - autoFlags: integer; //??? - }; - windows: { - perfect: number; - good: number; - hold: number; - miss: number; - slam: number; - }; + score: integer; + gauge: number; + timestamp: integer; + crit: integer; + near: integer; + error: integer; + options: { + gaugeType: 0 | 1; + gaugeOpt: integer; + mirror: boolean; + random: boolean; + autoFlags: integer; //??? + }; + windows: { + perfect: number; + good: number; + hold: number; + miss: number; + slam: number; + }; } export interface USCClientChart { - chartHash: string; - artist: string; - title: string; - level: integer; - difficulty: 0 | 1 | 2 | 3; - effector: string; - illustrator: string; - bpm: string; + chartHash: string; + artist: string; + title: string; + level: integer; + difficulty: 0 | 1 | 2 | 3; + effector: string; + illustrator: string; + bpm: string; } diff --git a/server/src/server/router/ir/usc/usc.test.ts b/server/src/server/router/ir/usc/usc.test.ts index d2a4b7f30..8adc5cfa8 100644 --- a/server/src/server/router/ir/usc/usc.test.ts +++ b/server/src/server/router/ir/usc/usc.test.ts @@ -7,406 +7,406 @@ import deepmerge from "deepmerge"; import { CloseAllConnections } from "../../../../test-utils/close-connections"; const mockScorePB: PBScoreDocument<"usc:Single"> = { - chartID: "USC_CHART_ID", - comments: [], - calculatedData: { - VF6: 0, - }, - composedFrom: { - scorePB: "USC_EXAMPLE_SCORE_PB_ID", - lampPB: "USC_EXAMPLE_LAMP_PB_ID", - }, - game: "usc", - highlight: false, - isPrimary: true, - playtype: "Single", - rankingData: { - outOf: 2, - rank: 1, - }, - scoreData: { - esd: null, - grade: "AAA+", - gradeIndex: 7, - hitData: { - critical: 100, - miss: 15, - }, - hitMeta: {}, - lamp: "EXCESSIVE CLEAR", - lampIndex: 3, - percent: 95, - score: 9_500_000, - }, - songID: 1, - timeAchieved: null, - userID: 1, + chartID: "USC_CHART_ID", + comments: [], + calculatedData: { + VF6: 0, + }, + composedFrom: { + scorePB: "USC_EXAMPLE_SCORE_PB_ID", + lampPB: "USC_EXAMPLE_LAMP_PB_ID", + }, + game: "usc", + highlight: false, + isPrimary: true, + playtype: "Single", + rankingData: { + outOf: 2, + rank: 1, + }, + scoreData: { + esd: null, + grade: "AAA+", + gradeIndex: 7, + hitData: { + critical: 100, + miss: 15, + }, + hitMeta: {}, + lamp: "EXCESSIVE CLEAR", + lampIndex: 3, + percent: 95, + score: 9_500_000, + }, + songID: 1, + timeAchieved: null, + userID: 1, }; const mockScoreDocument = { - scoreID: "USC_EXAMPLE_SCORE_PB_ID", - scoreMeta: { - noteMod: "MIRROR", - gaugeMod: "HARD", - }, + scoreID: "USC_EXAMPLE_SCORE_PB_ID", + scoreMeta: { + noteMod: "MIRROR", + gaugeMod: "HARD", + }, } as ScoreDocument; t.test("#TachiScoreToServerScore", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should correctly convert a tachiScore to a serverScore", async (t) => { - await db.scores.insert(mockScoreDocument as ScoreDocument); + t.test("Should correctly convert a tachiScore to a serverScore", async (t) => { + await db.scores.insert(mockScoreDocument as ScoreDocument); - const res = await TachiScoreToServerScore(mockScorePB); + const res = await TachiScoreToServerScore(mockScorePB); - t.strictSame( - res, - { - score: 9_500_000, - timestamp: 0, - crit: 100, - near: 0, - error: 15, - ranking: 1, - lamp: 3, - username: "test_zkldi", - noteMod: "MIRROR", - gaugeMod: "HARD", - }, - "Should return the right ServerScore." - ); + t.strictSame( + res, + { + score: 9_500_000, + timestamp: 0, + crit: 100, + near: 0, + error: 15, + ranking: 1, + lamp: 3, + username: "test_zkldi", + noteMod: "MIRROR", + gaugeMod: "HARD", + }, + "Should return the right ServerScore." + ); - t.end(); - }); + t.end(); + }); - t.test("Should work for timestamped scores", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should work for timestamped scores", async (t) => { + await db.scores.insert(mockScoreDocument); - const res = await TachiScoreToServerScore( - deepmerge(mockScorePB, { timeAchieved: 1_621_844_762_995 }) - ); + const res = await TachiScoreToServerScore( + deepmerge(mockScorePB, { timeAchieved: 1_621_844_762_995 }) + ); - t.strictSame( - res, - { - score: 9_500_000, - timestamp: 1_621_844_762, - crit: 100, - near: 0, - error: 15, - ranking: 1, - lamp: 3, - username: "test_zkldi", - noteMod: "MIRROR", - gaugeMod: "HARD", - }, - "Should return the right ServerScore." - ); + t.strictSame( + res, + { + score: 9_500_000, + timestamp: 1_621_844_762, + crit: 100, + near: 0, + error: 15, + ranking: 1, + lamp: 3, + username: "test_zkldi", + noteMod: "MIRROR", + gaugeMod: "HARD", + }, + "Should return the right ServerScore." + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw if user document does not exist.", (t) => { - t.rejects(() => TachiScoreToServerScore(deepmerge(mockScorePB, { userID: 2 } as any)), { - message: /User 2 from PB on chart.*has no user document\?/u, - } as any); + t.test("Should throw if user document does not exist.", (t) => { + t.rejects(() => TachiScoreToServerScore(deepmerge(mockScorePB, { userID: 2 } as any)), { + message: /User 2 from PB on chart.*has no user document\?/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw if scorePB document does not exist.", (t) => { - t.rejects(() => TachiScoreToServerScore(mockScorePB), { - message: - /Score USC_EXAMPLE_SCORE_PB_ID does not exist, but is referenced in 1's PBDoc on/u, - } as any); + t.test("Should throw if scorePB document does not exist.", (t) => { + t.rejects(() => TachiScoreToServerScore(mockScorePB), { + message: + /Score USC_EXAMPLE_SCORE_PB_ID does not exist, but is referenced in 1's PBDoc on/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); // yeah const uscScorePBsSet = [ - { - userID: 1, - scoreData: { - score: 9_000_000, - }, - rankingData: { - rank: 5, - outOf: 10, - }, - }, - { - userID: 2, - scoreData: { - score: 9_100_000, - }, - rankingData: { - rank: 4, - outOf: 10, - }, - }, - { - userID: 3, - scoreData: { - score: 9_200_000, - }, - rankingData: { - rank: 3, - outOf: 10, - }, - }, - { - userID: 4, - scoreData: { - score: 9_300_000, - }, - rankingData: { - rank: 2, - outOf: 10, - }, - }, - { - userID: 5, - scoreData: { - score: 9_400_000, - }, - rankingData: { - rank: 1, - outOf: 10, - }, - }, - { - userID: 6, - scoreData: { - score: 8_900_000, - }, - rankingData: { - rank: 6, - outOf: 10, - }, - }, - { - userID: 7, - scoreData: { - score: 8_800_000, - }, - rankingData: { - rank: 7, - outOf: 10, - }, - }, - { - userID: 8, - scoreData: { - score: 8_700_000, - }, - rankingData: { - rank: 8, - outOf: 10, - }, - }, - { - userID: 9, - scoreData: { - score: 8_600_000, - }, - rankingData: { - rank: 9, - outOf: 10, - }, - }, - { - userID: 10, - scoreData: { - score: 8_500_000, - }, - rankingData: { - rank: 10, - outOf: 10, - }, - }, + { + userID: 1, + scoreData: { + score: 9_000_000, + }, + rankingData: { + rank: 5, + outOf: 10, + }, + }, + { + userID: 2, + scoreData: { + score: 9_100_000, + }, + rankingData: { + rank: 4, + outOf: 10, + }, + }, + { + userID: 3, + scoreData: { + score: 9_200_000, + }, + rankingData: { + rank: 3, + outOf: 10, + }, + }, + { + userID: 4, + scoreData: { + score: 9_300_000, + }, + rankingData: { + rank: 2, + outOf: 10, + }, + }, + { + userID: 5, + scoreData: { + score: 9_400_000, + }, + rankingData: { + rank: 1, + outOf: 10, + }, + }, + { + userID: 6, + scoreData: { + score: 8_900_000, + }, + rankingData: { + rank: 6, + outOf: 10, + }, + }, + { + userID: 7, + scoreData: { + score: 8_800_000, + }, + rankingData: { + rank: 7, + outOf: 10, + }, + }, + { + userID: 8, + scoreData: { + score: 8_700_000, + }, + rankingData: { + rank: 8, + outOf: 10, + }, + }, + { + userID: 9, + scoreData: { + score: 8_600_000, + }, + rankingData: { + rank: 9, + outOf: 10, + }, + }, + { + userID: 10, + scoreData: { + score: 8_500_000, + }, + rankingData: { + rank: 10, + outOf: 10, + }, + }, ].map((e) => deepmerge(mockScorePB, e) as any); const mockUserDocs = [2, 3, 4, 5, 6, 7, 8, 9, 10].map((e) => ({ - id: e, - username: e.toString(), + id: e, + username: e.toString(), })) as any; t.test("#CreatePOSTScoresResponseBody", async (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - const chartDoc = (await db.charts.usc.findOne()) as ChartDocument<"usc:Single">; + const chartDoc = (await db.charts.usc.findOne()) as ChartDocument<"usc:Single">; - t.test("Should correctly return POSTScoresResponseBody", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should correctly return POSTScoresResponseBody", async (t) => { + await db.scores.insert(mockScoreDocument); - await db["personal-bests"].insert(uscScorePBsSet); - await db.users.insert(mockUserDocs); + await db["personal-bests"].insert(uscScorePBsSet); + await db.users.insert(mockUserDocs); - await db.scores.insert({ - scoreID: "USER_1_SCORE_PB", - scoreMeta: { - replayID: "foo_bar", - }, - } as any); + await db.scores.insert({ + scoreID: "USER_1_SCORE_PB", + scoreMeta: { + replayID: "foo_bar", + }, + } as any); - const res = await CreatePOSTScoresResponseBody( - 1, - chartDoc as ChartDocument<"usc:Single">, - "USER_1_SCORE_PB" - ); + const res = await CreatePOSTScoresResponseBody( + 1, + chartDoc as ChartDocument<"usc:Single">, + "USER_1_SCORE_PB" + ); - t.hasStrict(res, { - score: { - score: 9_000_000, - username: "test_zkldi", - ranking: 5, - }, - serverRecord: { - score: 9_400_000, - username: "5", - ranking: 1, - }, - isServerRecord: false, - isPB: false, - sendReplay: "USER_1_SCORE_PB", - adjacentAbove: [ - { score: 9_300_000, username: "4", ranking: 2 }, - { score: 9_200_000, username: "3", ranking: 3 }, - { score: 9_100_000, username: "2", ranking: 4 }, - ], - adjacentBelow: [ - { score: 8_900_000, username: "6", ranking: 6 }, - { score: 8_800_000, username: "7", ranking: 7 }, - { score: 8_700_000, username: "8", ranking: 8 }, - ], - }); + t.hasStrict(res, { + score: { + score: 9_000_000, + username: "test_zkldi", + ranking: 5, + }, + serverRecord: { + score: 9_400_000, + username: "5", + ranking: 1, + }, + isServerRecord: false, + isPB: false, + sendReplay: "USER_1_SCORE_PB", + adjacentAbove: [ + { score: 9_300_000, username: "4", ranking: 2 }, + { score: 9_200_000, username: "3", ranking: 3 }, + { score: 9_100_000, username: "2", ranking: 4 }, + ], + adjacentBelow: [ + { score: 8_900_000, username: "6", ranking: 6 }, + { score: 8_800_000, username: "7", ranking: 7 }, + { score: 8_700_000, username: "8", ranking: 8 }, + ], + }); - t.end(); - }); + t.end(); + }); - t.test("Should not return serverRecord in adjacentAbove", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should not return serverRecord in adjacentAbove", async (t) => { + await db.scores.insert(mockScoreDocument); - await db["personal-bests"].insert(uscScorePBsSet); - await db.users.insert(mockUserDocs); + await db["personal-bests"].insert(uscScorePBsSet); + await db.users.insert(mockUserDocs); - await db.scores.insert({ - scoreID: "USER_4_SCORE_PB", - } as any); + await db.scores.insert({ + scoreID: "USER_4_SCORE_PB", + } as any); - const res = await CreatePOSTScoresResponseBody(4, chartDoc, "USER_4_SCORE_PB"); + const res = await CreatePOSTScoresResponseBody(4, chartDoc, "USER_4_SCORE_PB"); - t.hasStrict(res, { - score: { - score: 9_300_000, - username: "4", - ranking: 2, - }, - serverRecord: { - ranking: 1, - }, - isServerRecord: false, - isPB: false, - sendReplay: "USER_4_SCORE_PB", - }); + t.hasStrict(res, { + score: { + score: 9_300_000, + username: "4", + ranking: 2, + }, + serverRecord: { + ranking: 1, + }, + isServerRecord: false, + isPB: false, + sendReplay: "USER_4_SCORE_PB", + }); - t.strictSame( - res.adjacentAbove.map((e) => e.ranking), - [] - ); + t.strictSame( + res.adjacentAbove.map((e) => e.ranking), + [] + ); - t.strictSame( - res.adjacentBelow.map((e) => e.ranking), - [3, 4, 5] - ); + t.strictSame( + res.adjacentBelow.map((e) => e.ranking), + [3, 4, 5] + ); - t.end(); - }); + t.end(); + }); - t.test("Should trim serverRecord in adjacentAbove", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should trim serverRecord in adjacentAbove", async (t) => { + await db.scores.insert(mockScoreDocument); - await db["personal-bests"].insert(uscScorePBsSet); - await db.users.insert(mockUserDocs); + await db["personal-bests"].insert(uscScorePBsSet); + await db.users.insert(mockUserDocs); - await db.scores.insert({ - scoreID: "USER_3_SCORE_PB", - } as any); + await db.scores.insert({ + scoreID: "USER_3_SCORE_PB", + } as any); - const res = await CreatePOSTScoresResponseBody(3, chartDoc, "USER_3_SCORE_PB"); + const res = await CreatePOSTScoresResponseBody(3, chartDoc, "USER_3_SCORE_PB"); - t.hasStrict(res, { - score: { - score: 9_200_000, - username: "3", - ranking: 3, - }, - serverRecord: { - ranking: 1, - }, - isServerRecord: false, - isPB: false, - sendReplay: "USER_3_SCORE_PB", - }); + t.hasStrict(res, { + score: { + score: 9_200_000, + username: "3", + ranking: 3, + }, + serverRecord: { + ranking: 1, + }, + isServerRecord: false, + isPB: false, + sendReplay: "USER_3_SCORE_PB", + }); - t.strictSame( - res.adjacentAbove.map((e) => e.ranking), - [2] - ); + t.strictSame( + res.adjacentAbove.map((e) => e.ranking), + [2] + ); - t.strictSame( - res.adjacentBelow.map((e) => e.ranking), - [4, 5, 6] - ); + t.strictSame( + res.adjacentBelow.map((e) => e.ranking), + [4, 5, 6] + ); - t.end(); - }); + t.end(); + }); - t.test("Should throw on no ScorePB", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should throw on no ScorePB", async (t) => { + await db.scores.insert(mockScoreDocument); - // await db["personal-bests"].insert(uscScorePBsSet); - await db.users.insert(mockUserDocs); + // await db["personal-bests"].insert(uscScorePBsSet); + await db.users.insert(mockUserDocs); - await db.scores.insert({ - scoreID: "USER_1_SCORE_PB", - scoreMeta: { - replayID: "foo_bar", - }, - } as any); + await db.scores.insert({ + scoreID: "USER_1_SCORE_PB", + scoreMeta: { + replayID: "foo_bar", + }, + } as any); - t.rejects(() => CreatePOSTScoresResponseBody(1, chartDoc, "USER_1_SCORE_PB"), { - message: /Score was imported for chart, but no ScorePB was available on this chart/u, - } as any); + t.rejects(() => CreatePOSTScoresResponseBody(1, chartDoc, "USER_1_SCORE_PB"), { + message: /Score was imported for chart, but no ScorePB was available on this chart/u, + } as any); - t.end(); - }); + t.end(); + }); - t.test("Should throw on no original score", async (t) => { - await db.scores.insert(mockScoreDocument); + t.test("Should throw on no original score", async (t) => { + await db.scores.insert(mockScoreDocument); - await db["personal-bests"].insert(uscScorePBsSet); - await db.users.insert(mockUserDocs); + await db["personal-bests"].insert(uscScorePBsSet); + await db.users.insert(mockUserDocs); - // await db.scores.insert({ - // scoreID: "USER_1_SCORE_PB", - // scoreMeta: { - // replayID: "foo_bar", - // }, - // } as any); + // await db.scores.insert({ + // scoreID: "USER_1_SCORE_PB", + // scoreMeta: { + // replayID: "foo_bar", + // }, + // } as any); - t.rejects(() => CreatePOSTScoresResponseBody(1, chartDoc, "USER_1_SCORE_PB"), { - message: /Score with ID USER_1_SCORE_PB is not in the database/u, - } as any); + t.rejects(() => CreatePOSTScoresResponseBody(1, chartDoc, "USER_1_SCORE_PB"), { + message: /Score with ID USER_1_SCORE_PB is not in the database/u, + } as any); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/server/router/ir/usc/usc.ts b/server/src/server/router/ir/usc/usc.ts index d8d8f3e07..b3eda4903 100644 --- a/server/src/server/router/ir/usc/usc.ts +++ b/server/src/server/router/ir/usc/usc.ts @@ -9,15 +9,15 @@ import { USCServerScore } from "./types"; const logger = CreateLogCtx(__filename); export const TACHI_LAMP_TO_USC: Record< - PBScoreDocument<"usc:Single">["scoreData"]["lamp"], - USCServerScore["lamp"] + PBScoreDocument<"usc:Single">["scoreData"]["lamp"], + USCServerScore["lamp"] > = { - // we don't do NO PLAY, so its not handled. - FAILED: 1, - CLEAR: 2, - "EXCESSIVE CLEAR": 3, - "ULTIMATE CHAIN": 4, - "PERFECT ULTIMATE CHAIN": 5, + // we don't do NO PLAY, so its not handled. + FAILED: 1, + CLEAR: 2, + "EXCESSIVE CLEAR": 3, + "ULTIMATE CHAIN": 4, + "PERFECT ULTIMATE CHAIN": 5, }; /** @@ -27,177 +27,177 @@ export const TACHI_LAMP_TO_USC: Record< * fields are null. */ export async function TachiScoreToServerScore( - tachiScore: PBScoreDocument<"usc:Single"> + tachiScore: PBScoreDocument<"usc:Single"> ): Promise { - // @optimisable - // Repeated calls to this may pre-emptively provide usernames - // and score PBs. - const userDoc = await db.users.findOne( - { - id: tachiScore.userID, - }, - { - projection: { - username: 1, - }, - } - ); + // @optimisable + // Repeated calls to this may pre-emptively provide usernames + // and score PBs. + const userDoc = await db.users.findOne( + { + id: tachiScore.userID, + }, + { + projection: { + username: 1, + }, + } + ); - if (!userDoc) { - logger.severe( - `User ${tachiScore.userID} from PB on chart ${tachiScore.chartID} has no user document?` - ); - throw new Error( - `User ${tachiScore.userID} from PB on chart ${tachiScore.chartID} has no user document?` - ); - } + if (!userDoc) { + logger.severe( + `User ${tachiScore.userID} from PB on chart ${tachiScore.chartID} has no user document?` + ); + throw new Error( + `User ${tachiScore.userID} from PB on chart ${tachiScore.chartID} has no user document?` + ); + } - const scorePB = (await db.scores.findOne({ - scoreID: tachiScore.composedFrom.scorePB, - })) as ScoreDocument<"usc:Single"> | null; + const scorePB = (await db.scores.findOne({ + scoreID: tachiScore.composedFrom.scorePB, + })) as ScoreDocument<"usc:Single"> | null; - if (!scorePB) { - logger.severe( - `Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?` - ); + if (!scorePB) { + logger.severe( + `Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?` + ); - throw new Error( - `Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?` - ); - } + throw new Error( + `Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?` + ); + } - return { - score: tachiScore.scoreData.score, - timestamp: MStoS(tachiScore.timeAchieved ?? 0), - crit: tachiScore.scoreData.hitData.critical ?? 0, - near: tachiScore.scoreData.hitData.near ?? 0, - error: tachiScore.scoreData.hitData.miss ?? 0, - ranking: tachiScore.rankingData.rank, - lamp: TACHI_LAMP_TO_USC[tachiScore.scoreData.lamp], - username: userDoc.username, - noteMod: scorePB.scoreMeta.noteMod ?? "NORMAL", - gaugeMod: scorePB.scoreMeta.gaugeMod ?? "NORMAL", - }; + return { + score: tachiScore.scoreData.score, + timestamp: MStoS(tachiScore.timeAchieved ?? 0), + crit: tachiScore.scoreData.hitData.critical ?? 0, + near: tachiScore.scoreData.hitData.near ?? 0, + error: tachiScore.scoreData.hitData.miss ?? 0, + ranking: tachiScore.rankingData.rank, + lamp: TACHI_LAMP_TO_USC[tachiScore.scoreData.lamp], + username: userDoc.username, + noteMod: scorePB.scoreMeta.noteMod ?? "NORMAL", + gaugeMod: scorePB.scoreMeta.gaugeMod ?? "NORMAL", + }; } export async function CreatePOSTScoresResponseBody( - userID: integer, - chartDoc: ChartDocument<"usc:Single">, - scoreID: string + userID: integer, + chartDoc: ChartDocument<"usc:Single">, + scoreID: string ): Promise { - const scorePB = (await GetPBOnChart( - userID, - chartDoc.chartID - )) as PBScoreDocument<"usc:Single"> | null; + const scorePB = (await GetPBOnChart( + userID, + chartDoc.chartID + )) as PBScoreDocument<"usc:Single"> | null; - if (!scorePB) { - logger.severe(`Score was imported for chart, but no ScorePB was available on this chart?`, { - chartDoc, - scoreID, - }); - throw new Error( - `Score was imported for chart, but no ScorePB was available on this chart?` - ); - } + if (!scorePB) { + logger.severe(`Score was imported for chart, but no ScorePB was available on this chart?`, { + chartDoc, + scoreID, + }); + throw new Error( + `Score was imported for chart, but no ScorePB was available on this chart?` + ); + } - const ktServerRecord = (await GetServerRecordOnChart( - chartDoc.chartID - )) as PBScoreDocument<"usc:Single"> | null; + const ktServerRecord = (await GetServerRecordOnChart( + chartDoc.chartID + )) as PBScoreDocument<"usc:Single"> | null; - // this is impossible to trigger without making a race-condition. - /* istanbul ignore next */ - if (!ktServerRecord) { - logger.severe( - `Score was imported for chart, but no Server Record was available on this chart?`, - { - chartDoc, - scoreID, - } - ); - throw new Error( - `Score was imported for chart, but no Server Record was available on this chart?` - ); - } + // this is impossible to trigger without making a race-condition. + /* istanbul ignore next */ + if (!ktServerRecord) { + logger.severe( + `Score was imported for chart, but no Server Record was available on this chart?`, + { + chartDoc, + scoreID, + } + ); + throw new Error( + `Score was imported for chart, but no Server Record was available on this chart?` + ); + } - const usersRanking = scorePB.rankingData.rank; + const usersRanking = scorePB.rankingData.rank; - // This returns immediately ranked higher - // than the current user. + // This returns immediately ranked higher + // than the current user. - const adjAbove = (await db["personal-bests"].find( - { - chartID: chartDoc.chartID, - "rankingData.rank": { $lt: usersRanking }, - }, - { - limit: USCIR_ADJACENT_SCORE_N, - sort: { "rankingData.rank": -1 }, - } - )) as PBScoreDocument<"usc:Single">[]; + const adjAbove = (await db["personal-bests"].find( + { + chartID: chartDoc.chartID, + "rankingData.rank": { $lt: usersRanking }, + }, + { + limit: USCIR_ADJACENT_SCORE_N, + sort: { "rankingData.rank": -1 }, + } + )) as PBScoreDocument<"usc:Single">[]; - // The specification enforces that we return them in - // ascending order, though, so we reverse this after - // the query. - adjAbove.reverse(); + // The specification enforces that we return them in + // ascending order, though, so we reverse this after + // the query. + adjAbove.reverse(); - // if the users ranking implies that the above query - // returned the server record (i.e. they are ranked - // between #1 and #1 + N) - // delete the server record from adjAbove. - if (usersRanking - USCIR_ADJACENT_SCORE_N <= 1) { - adjAbove.shift(); - } + // if the users ranking implies that the above query + // returned the server record (i.e. they are ranked + // between #1 and #1 + N) + // delete the server record from adjAbove. + if (usersRanking - USCIR_ADJACENT_SCORE_N <= 1) { + adjAbove.shift(); + } - // Similar to above, this returns the N most immediate - // scores below the given user. - const adjBelow = (await db["personal-bests"].find( - { - chartID: chartDoc.chartID, - "rankingData.rank": { $gt: usersRanking }, - }, - { - limit: USCIR_ADJACENT_SCORE_N, - sort: { "rankingData.rank": 1 }, - } - )) as PBScoreDocument<"usc:Single">[]; + // Similar to above, this returns the N most immediate + // scores below the given user. + const adjBelow = (await db["personal-bests"].find( + { + chartID: chartDoc.chartID, + "rankingData.rank": { $gt: usersRanking }, + }, + { + limit: USCIR_ADJACENT_SCORE_N, + sort: { "rankingData.rank": 1 }, + } + )) as PBScoreDocument<"usc:Single">[]; - const [score, serverRecord, adjacentAbove, adjacentBelow] = await Promise.all([ - TachiScoreToServerScore(scorePB), - TachiScoreToServerScore(ktServerRecord), - Promise.all(adjAbove.map(TachiScoreToServerScore)), - Promise.all(adjBelow.map(TachiScoreToServerScore)), - ]); + const [score, serverRecord, adjacentAbove, adjacentBelow] = await Promise.all([ + TachiScoreToServerScore(scorePB), + TachiScoreToServerScore(ktServerRecord), + Promise.all(adjAbove.map(TachiScoreToServerScore)), + Promise.all(adjBelow.map(TachiScoreToServerScore)), + ]); - const originalScore = (await db.scores.findOne({ - scoreID, - })) as ScoreDocument<"usc:Single">; + const originalScore = (await db.scores.findOne({ + scoreID, + })) as ScoreDocument<"usc:Single">; - if (!originalScore) { - logger.severe( - `Score with ID ${scoreID} is not in the database, but was claimed to be inserted?` - ); - throw new Error( - `Score with ID ${scoreID} is not in the database, but was claimed to be inserted?` - ); - } + if (!originalScore) { + logger.severe( + `Score with ID ${scoreID} is not in the database, but was claimed to be inserted?` + ); + throw new Error( + `Score with ID ${scoreID} is not in the database, but was claimed to be inserted?` + ); + } - return { - score, - serverRecord, - isServerRecord: scorePB.userID === ktServerRecord?.userID, - isPB: scorePB.composedFrom.scorePB === scoreID, - sendReplay: originalScore.scoreID, - adjacentAbove, - adjacentBelow, - }; + return { + score, + serverRecord, + isServerRecord: scorePB.userID === ktServerRecord?.userID, + isPB: scorePB.composedFrom.scorePB === scoreID, + sendReplay: originalScore.scoreID, + adjacentAbove, + adjacentBelow, + }; } export interface POSTScoresResponseBody { - score: USCServerScore; - serverRecord: USCServerScore; - adjacentAbove: USCServerScore[]; - adjacentBelow: USCServerScore[]; - isPB: boolean; - isServerRecord: boolean; - sendReplay: string; + score: USCServerScore; + serverRecord: USCServerScore; + adjacentAbove: USCServerScore[]; + adjacentBelow: USCServerScore[]; + isPB: boolean; + isServerRecord: boolean; + sendReplay: string; } diff --git a/server/src/server/server.ts b/server/src/server/server.ts index 02e18d539..71c243edb 100644 --- a/server/src/server/server.ts +++ b/server/src/server/server.ts @@ -12,26 +12,26 @@ const logger = CreateLogCtx(__filename); let store; if (process.env.NODE_ENV !== "test") { - const RedisStore = connectRedis(expressSession); - store = new RedisStore({ - host: "localhost", - port: 6379, - client: RedisClient, - }); + const RedisStore = connectRedis(expressSession); + store = new RedisStore({ + host: "localhost", + port: 6379, + client: RedisClient, + }); } const userSessionMiddleware = expressSession({ - // append node_env onto the end of the session name - // so we can separate tokens under the same URL. - // say for staging.kamaitachi.xyz - name: `${CONFIG.TYPE}_${process.env.NODE_ENV}_session`, - secret: SESSION_SECRET, - store, - resave: true, - saveUninitialized: false, - cookie: { - secure: process.env.NODE_ENV === "production", - }, + // append node_env onto the end of the session name + // so we can separate tokens under the same URL. + // say for staging.kamaitachi.xyz + name: `${CONFIG.TYPE}_${process.env.NODE_ENV}_session`, + secret: SESSION_SECRET, + store, + resave: true, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === "production", + }, }); const app: Express = express(); @@ -49,9 +49,9 @@ app.set("query parser", "simple"); // taken from https://nodejs.org/api/process.html#process_event_unhandledrejection // to avoid future deprecation. process.on("unhandledRejection", (reason, promise) => { - // @ts-expect-error reason is an error, and the logger can handle errors - // it just refuses. - logger.error(reason, { promise }); + // @ts-expect-error reason is an error, and the logger can handle errors + // it just refuses. + logger.error(reason, { promise }); }); // enable reading json bodies @@ -68,32 +68,32 @@ app.use("/", mainRouter); */ app.get("*", (req, res) => res.status(200).send("todo")); app.all("*", (req, res) => - res.status(404).json({ success: false, description: "404: This URL does not exist." }) + res.status(404).json({ success: false, description: "404: This URL does not exist." }) ); // completely stolen from ktapi error handler interface ExpressJSONErr extends SyntaxError { - status: integer; - message: string; + status: integer; + message: string; } // eslint-disable-next-line @typescript-eslint/no-unused-vars const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res) => { - logger.info("foo"); - if (err instanceof SyntaxError) { - const expErr: ExpressJSONErr = err as ExpressJSONErr; - if (expErr.status === 400 && "body" in expErr) { - return res.status(400).send({ success: false, description: err.message }); - } + logger.info("foo"); + if (err instanceof SyntaxError) { + const expErr: ExpressJSONErr = err as ExpressJSONErr; + if (expErr.status === 400 && "body" in expErr) { + return res.status(400).send({ success: false, description: err.message }); + } - // else, this isn't a JSON parsing error - } + // else, this isn't a JSON parsing error + } - logger.error(err, req.route); - return res.status(500).json({ - success: false, - description: "A fatal internal server error has occured.", - }); + logger.error(err, req.route); + return res.status(500).json({ + success: false, + description: "A fatal internal server error has occured.", + }); }; app.use(MAIN_ERR_HANDLER); diff --git a/server/src/test-utils/api-common.ts b/server/src/test-utils/api-common.ts index 403067317..4dd1ed093 100644 --- a/server/src/test-utils/api-common.ts +++ b/server/src/test-utils/api-common.ts @@ -2,24 +2,24 @@ import t from "tap"; import mockApi from "./mock-api"; export function RequireNeutralAuthentication(url: string, method: "GET" | "POST" = "GET") { - t.test(`Testing authentication for ${method} ${url}.`, async (t) => { - let res; + t.test(`Testing authentication for ${method} ${url}.`, async (t) => { + let res; - if (method === "GET") { - res = await mockApi.get(url); - } else { - res = await mockApi.post(url); - } + if (method === "GET") { + res = await mockApi.get(url); + } else { + res = await mockApi.post(url); + } - t.equal(res.status, 401, "Should return 401 immediately."); - t.equal( - res.body.description, - "You are not authorised to perform this action.", - "Should return an appropriate error message." - ); + t.equal(res.status, 401, "Should return 401 immediately."); + t.equal( + res.body.description, + "You are not authorised to perform this action.", + "Should return an appropriate error message." + ); - // CloseAllConnections(); + // CloseAllConnections(); - t.end(); - }); + t.end(); + }); } diff --git a/server/src/test-utils/asserts.ts b/server/src/test-utils/asserts.ts index 9082a45b0..f18f23c2c 100644 --- a/server/src/test-utils/asserts.ts +++ b/server/src/test-utils/asserts.ts @@ -2,25 +2,25 @@ import Prudence, { PrudenceSchema } from "prudence"; import t from "tap"; export function isApproximately(number: number, target: number, message: string, lenience = 0.01) { - const result = t.ok(Math.abs(number - target) < lenience, message); + const result = t.ok(Math.abs(number - target) < lenience, message); - if (!result) { - throw new Error(`${number} was not close enough to ${target}`); - } + if (!result) { + throw new Error(`${number} was not close enough to ${target}`); + } - return true; + return true; } export function prAssert( - obj: Record | unknown, - schema: PrudenceSchema, - message = "Unnamed Prudence Assertion" + obj: Record | unknown, + schema: PrudenceSchema, + message = "Unnamed Prudence Assertion" ) { - const err = Prudence(obj, schema); + const err = Prudence(obj, schema); - if (err) { - return t.fail(message, err); - } + if (err) { + return t.fail(message, err); + } - return t.pass(message); + return t.pass(message); } diff --git a/server/src/test-utils/close-connections.ts b/server/src/test-utils/close-connections.ts index 7b582007f..e7aa3cfb2 100644 --- a/server/src/test-utils/close-connections.ts +++ b/server/src/test-utils/close-connections.ts @@ -4,8 +4,8 @@ import { CloseRedisConnection } from "../external/redis/redis"; import { CloseServerConnection } from "./mock-api"; export async function CloseAllConnections() { - await CloseMongoConnection(); - await CloseServerConnection(); - await CloseRedisConnection(); - await CloseRedisPubSub(); + await CloseMongoConnection(); + await CloseServerConnection(); + await CloseRedisConnection(); + await CloseRedisPubSub(); } diff --git a/server/src/test-utils/fake-auth.ts b/server/src/test-utils/fake-auth.ts index 8c2adf235..63caa4d3e 100644 --- a/server/src/test-utils/fake-auth.ts +++ b/server/src/test-utils/fake-auth.ts @@ -7,32 +7,32 @@ import db from "../external/mongo/db"; const logger = CreateLogCtx(__filename); export async function CreateFakeAuthCookie(mockApi: supertest.SuperTest) { - await ResetDBState(); + await ResetDBState(); - // possible security issue, ask hazel - const res = await mockApi.post("/api/v1/auth/login").send({ - username: "test_zkldi", - password: "password", - captcha: "asdf", - }); + // possible security issue, ask hazel + const res = await mockApi.post("/api/v1/auth/login").send({ + username: "test_zkldi", + password: "password", + captcha: "asdf", + }); - if (res.status !== 200) { - logger.crit("Failed to login. Cannot generate auth cookie."); - throw res.body; - } + if (res.status !== 200) { + logger.crit("Failed to login. Cannot generate auth cookie."); + throw res.body; + } - return res.headers["set-cookie"] as string[]; + return res.headers["set-cookie"] as string[]; } // my local dev env hates this part because of pnpm // eslint-disable-next-line @typescript-eslint/no-explicit-any export function InsertFakeTokenWithAllPerms(token: string): () => any { - return () => { - db["api-tokens"].insert({ - userID: 1, - identifier: "Mock API Token", - permissions: AllPermissions, - token, - }); - }; + return () => { + db["api-tokens"].insert({ + userID: 1, + identifier: "Mock API Token", + permissions: AllPermissions, + token, + }); + }; } diff --git a/server/src/test-utils/misc.ts b/server/src/test-utils/misc.ts index bfc927995..490779b55 100644 --- a/server/src/test-utils/misc.ts +++ b/server/src/test-utils/misc.ts @@ -2,10 +2,10 @@ * Async Generator To Array */ export async function agta(ag: AsyncIterable | Iterable) { - const a = []; - for await (const el of ag) { - a.push(el); - } + const a = []; + for await (const el of ag) { + a.push(el); + } - return a; + return a; } diff --git a/server/src/test-utils/mock-api.ts b/server/src/test-utils/mock-api.ts index dab17c609..e0ac34ea9 100644 --- a/server/src/test-utils/mock-api.ts +++ b/server/src/test-utils/mock-api.ts @@ -12,7 +12,7 @@ logger.verbose("Connecting to Supertest..."); const mockApi = supertest(connection); export function CloseServerConnection() { - connection.close(); + connection.close(); } export default mockApi; diff --git a/server/src/test-utils/mock-fetch.ts b/server/src/test-utils/mock-fetch.ts index b40a67889..bd5744b2b 100644 --- a/server/src/test-utils/mock-fetch.ts +++ b/server/src/test-utils/mock-fetch.ts @@ -7,7 +7,7 @@ import { NodeFetch } from "../utils/fetch"; * Creates a basic Fetch function used for statusCode checks. */ export function MockBasicFetch(data: Partial) { - return (async () => data) as unknown as NodeFetch; + return (async () => data) as unknown as NodeFetch; } /** @@ -15,13 +15,13 @@ export function MockBasicFetch(data: Partial) { * a fake JSON -> data function at that key. */ export function MockJSONFetch(urlDataMap: Record) { - return (async (url: string) => { - if (urlDataMap[url]) { - return { - json: async () => urlDataMap[url], - }; - } + return (async (url: string) => { + if (urlDataMap[url]) { + return { + json: async () => urlDataMap[url], + }; + } - throw new Error(`Unexpected url ${url} - No Data Present?`); - }) as NodeFetch; + throw new Error(`Unexpected url ${url} - No Data Present?`); + }) as NodeFetch; } diff --git a/server/src/test-utils/mock-multer.ts b/server/src/test-utils/mock-multer.ts index 2b713f68e..bb464ff56 100644 --- a/server/src/test-utils/mock-multer.ts +++ b/server/src/test-utils/mock-multer.ts @@ -1,6 +1,6 @@ export function MockMulterFile(buffer: Buffer, originalname: string) { - return { - originalname, - buffer, - } as Express.Multer.File; + return { + originalname, + buffer, + } as Express.Multer.File; } diff --git a/server/src/test-utils/resets.ts b/server/src/test-utils/resets.ts index 3e0ee0998..d96e14dda 100644 --- a/server/src/test-utils/resets.ts +++ b/server/src/test-utils/resets.ts @@ -17,84 +17,84 @@ const DATA_DIR = path.join(__dirname, "./mock-db"); const CACHE: Record = {}; async function ResetState(data: any[], collection: any) { - await collection.remove({}); + await collection.remove({}); - await collection.insert(data); + await collection.insert(data); } function GetAndCache(filename: string, fileLoc: string) { - let collection; - if (filename.startsWith("songs-")) { - // @ts-expect-error it's right, but we know what we're doing! - collection = db.songs[filename.split("-")[1]]; - } else if (filename.startsWith("charts-")) { - // @ts-expect-error see above - collection = db.charts[filename.split("-")[1]]; - } else { - // @ts-expect-error see above - collection = db[filename]; - } + let collection; + if (filename.startsWith("songs-")) { + // @ts-expect-error it's right, but we know what we're doing! + collection = db.songs[filename.split("-")[1]]; + } else if (filename.startsWith("charts-")) { + // @ts-expect-error see above + collection = db.charts[filename.split("-")[1]]; + } else { + // @ts-expect-error see above + collection = db[filename]; + } - if (CACHE[filename]) { - return { data: CACHE[filename], collection }; - } + if (CACHE[filename]) { + return { data: CACHE[filename], collection }; + } - const data = JSON.parse(fs.readFileSync(fileLoc, "utf-8")); + const data = JSON.parse(fs.readFileSync(fileLoc, "utf-8")); - if (!Array.isArray(data)) { - throw new Error(`Panic, ${filename} not JSONArray?`); - } + if (!Array.isArray(data)) { + throw new Error(`Panic, ${filename} not JSONArray?`); + } - CACHE[filename] = data; + CACHE[filename] = data; - return { data, collection }; + return { data, collection }; } let CACHE_FILENAMES: string[]; export default async function ResetDBState() { - let files; - if (CACHE_FILENAMES) { - files = CACHE_FILENAMES; - } else { - files = fs.readdirSync(DATA_DIR); - CACHE_FILENAMES = files; - } + let files; + if (CACHE_FILENAMES) { + files = CACHE_FILENAMES; + } else { + files = fs.readdirSync(DATA_DIR); + CACHE_FILENAMES = files; + } - const promises = []; + const promises = []; - for (const file of files) { - const filename = path.basename(file, ".json"); - const fileLoc = path.join(DATA_DIR, file); + for (const file of files) { + const filename = path.basename(file, ".json"); + const fileLoc = path.join(DATA_DIR, file); - const { data, collection } = GetAndCache(filename, fileLoc); + const { data, collection } = GetAndCache(filename, fileLoc); - promises.push(ResetState(data, collection)); - } + promises.push(ResetState(data, collection)); + } - await Promise.all(promises); + await Promise.all(promises); } export function ResetCDN() { - return new Promise((resolve, reject) => - rimraf(KTCDN_ROOT, (err) => { - if (err) { - reject(err); - } - resolve(); - }) - ); + return new Promise((resolve, reject) => + rimraf(KTCDN_ROOT, (err) => { + if (err) { + reject(err); + } + resolve(); + }) + ); } export async function SetIndexesForDB() { - const url = `${MONGO_CONNECTION_URL}/${ - process.env.TACHI_PARALLEL_TESTS ? `test-ephemeral-${process.pid.toString()}` : "testingdb" - }`; + const url = `${MONGO_CONNECTION_URL}/${ + process.env.TACHI_PARALLEL_TESTS ? `test-ephemeral-${process.pid.toString()}` : "testingdb" + }`; - logger.info(`Setting indexes for ${url}`); + logger.info(`Setting indexes for ${url}`); - await SetIndexes(url, true); + await SetIndexes(url, true); - logger.info(`Done.`); - return true; + logger.info(`Done.`); + return true; } diff --git a/server/src/test-utils/setup.ts b/server/src/test-utils/setup.ts index 98073e34d..e7a45bd5b 100644 --- a/server/src/test-utils/setup.ts +++ b/server/src/test-utils/setup.ts @@ -7,8 +7,8 @@ assert(process.env.NODE_ENV === "test", "NODE_ENV WAS NOT TEST, BAILING"); // empty the logs. try { - fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests-error.log")); - fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests.log")); + fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests-error.log")); + fs.rmSync(path.join(__dirname, "../../logs/ktblack-tests.log")); } catch (err) { - // no logs to remove, who cares! + // no logs to remove, who cares! } diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index db5201c5b..cdc2b2f9c 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -1,22 +1,22 @@ import fs from "fs"; import { - ChartDocument, - PBScoreDocument, - ScoreDocument, - GoalDocument, - UserGoalDocument, - FolderDocument, - MilestoneDocument, - SongDocument, + ChartDocument, + PBScoreDocument, + ScoreDocument, + GoalDocument, + UserGoalDocument, + FolderDocument, + MilestoneDocument, + SongDocument, } from "tachi-common"; import path from "path"; import db from "../external/mongo/db"; import { - USC_DEFAULT_HOLD, - USC_DEFAULT_MISS, - USC_DEFAULT_NEAR, - USC_DEFAULT_PERFECT, - USC_DEFAULT_SLAM, + USC_DEFAULT_HOLD, + USC_DEFAULT_MISS, + USC_DEFAULT_NEAR, + USC_DEFAULT_PERFECT, + USC_DEFAULT_SLAM, } from "../lib/constants/usc-ir"; import { DryScore } from "../lib/score-import/framework/common/types"; import { BarbatosScore } from "../lib/score-import/import-types/ir/barbatos/types"; @@ -28,14 +28,14 @@ export const GetKTDataJSON = (name: string) => JSON.parse(fs.readFileSync(file(n export const GetKTDataBuffer = (name: string) => fs.readFileSync(file(name)); export const TestingIIDXSPDryScore = GetKTDataJSON( - "./tachi/iidx-dryscore.json" + "./tachi/iidx-dryscore.json" ) as DryScore<"iidx:SP">; export const TestingIIDXSPScorePB = GetKTDataJSON( - "./tachi/iidx-scorepb.json" + "./tachi/iidx-scorepb.json" ) as PBScoreDocument<"iidx:SP">; export const TestingIIDXSPScore = GetKTDataJSON( - "./tachi/iidx-score.json" + "./tachi/iidx-score.json" ) as ScoreDocument<"iidx:SP">; export const Testing511SPA = GetKTDataJSON("./tachi/iidx-511spa.json") as ChartDocument<"iidx:SP">; @@ -43,197 +43,197 @@ export const Testing511SPA = GetKTDataJSON("./tachi/iidx-511spa.json") as ChartD export const Testing511Song = GetKTDataJSON("./tachi/iidx-511-song.json") as SongDocument<"iidx">; export const TestingAlbidaADV = GetKTDataJSON( - "./tachi/sdvx-albida-chart.json" + "./tachi/sdvx-albida-chart.json" ) as ChartDocument<"sdvx:Single">; export const TestingSDVXAlbidaSong = GetKTDataJSON( - "./tachi/sdvx-albida-song.json" + "./tachi/sdvx-albida-song.json" ) as SongDocument<"sdvx">; export const TestingDoraChart = GetKTDataJSON( - "./tachi/gitadora-ithinkabtyou.json" + "./tachi/gitadora-ithinkabtyou.json" ) as ChartDocument<"gitadora:Dora">; export const TestingSDVXSingleDryScore = GetKTDataJSON( - "./tachi/sdvx-dryscore.json" + "./tachi/sdvx-dryscore.json" ) as DryScore<"sdvx:Single">; export const TestingGITADORADoraDryScore = GetKTDataJSON( - "./tachi/gitadora-dryscore.json" + "./tachi/gitadora-dryscore.json" ) as DryScore<"gitadora:Dora">; export const TestingIIDXEamusementCSV26 = GetKTDataBuffer( - "./eamusement-iidx-csv/pre-leggendaria.csv" + "./eamusement-iidx-csv/pre-leggendaria.csv" ); export const TestingIIDXEamusementCSV27 = GetKTDataBuffer( - "./eamusement-iidx-csv/post-leggendaria.csv" + "./eamusement-iidx-csv/post-leggendaria.csv" ); export const TestingBarbatosScore: BarbatosScore = GetKTDataJSON("./barbatos/base.json"); export const HC511Goal: GoalDocument = { - charts: { - type: "single", - data: Testing511SPA.chartID, - }, - createdBy: 1, - game: "iidx", - goalID: "mock_goalID", - playtype: "SP", - timeAdded: 0, - title: "HC 5.1.1. SPA", - criteria: { - mode: "single", - value: 5, - key: "scoreData.lampIndex", - }, + charts: { + type: "single", + data: Testing511SPA.chartID, + }, + createdBy: 1, + game: "iidx", + goalID: "mock_goalID", + playtype: "SP", + timeAdded: 0, + title: "HC 5.1.1. SPA", + criteria: { + mode: "single", + value: 5, + key: "scoreData.lampIndex", + }, }; export const HC511UserGoal: UserGoalDocument = { - achieved: false, - timeAchieved: null, - game: "iidx", - playtype: "SP", - goalID: "mock_goalID", - lastInteraction: null, - outOf: 5, - outOfHuman: "HARD CLEAR", - progress: null, - progressHuman: "NO DATA", - timeSet: 0, - userID: 1, + achieved: false, + timeAchieved: null, + game: "iidx", + playtype: "SP", + goalID: "mock_goalID", + lastInteraction: null, + outOf: 5, + outOfHuman: "HARD CLEAR", + progress: null, + progressHuman: "NO DATA", + timeSet: 0, + userID: 1, }; export const TestingIIDXFolderSP10: FolderDocument = { - title: "Level 10", - game: "iidx", - playtype: "SP", - type: "charts", - folderID: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", - table: "Levels", - tableIndex: 10, - data: { - level: "10", - "flags¬IN BASE GAME": true, - }, + title: "Level 10", + game: "iidx", + playtype: "SP", + type: "charts", + folderID: "ed9d8c734447ce67d7135c0067441a98cc81aeaf", + table: "Levels", + tableIndex: 10, + data: { + level: "10", + "flags¬IN BASE GAME": true, + }, }; export const TestingIIDXSPMilestone: MilestoneDocument = { - createdBy: 1, - criteria: { - type: "all", - }, - desc: "testing milestone", - game: "iidx", - playtype: "SP", - group: "ExampleGroup", - groupIndex: 1, - milestoneID: "example_milestone_id", - name: "Example Milestone", - milestoneData: [ - { - title: "Group1", - desc: "Foo", - goals: [ - { - goalID: "eg_goal_1", - }, - { - goalID: "eg_goal_2", - }, - ], - }, - { - title: "Group2", - desc: "Bar", - goals: [ - { - goalID: "eg_goal_3", - }, - { - goalID: "eg_goal_4", - }, - ], - }, - ], + createdBy: 1, + criteria: { + type: "all", + }, + desc: "testing milestone", + game: "iidx", + playtype: "SP", + group: "ExampleGroup", + groupIndex: 1, + milestoneID: "example_milestone_id", + name: "Example Milestone", + milestoneData: [ + { + title: "Group1", + desc: "Foo", + goals: [ + { + goalID: "eg_goal_1", + }, + { + goalID: "eg_goal_2", + }, + ], + }, + { + title: "Group2", + desc: "Bar", + goals: [ + { + goalID: "eg_goal_3", + }, + { + goalID: "eg_goal_4", + }, + ], + }, + ], }; let KTDATA_CACHE: { songs: unknown[]; charts: unknown[] } | undefined; export async function LoadKTBlackIIDXData() { - let songs; - let charts; + let songs; + let charts; - if (KTDATA_CACHE) { - songs = KTDATA_CACHE.songs; - charts = KTDATA_CACHE.charts; - } else { - songs = GetKTDataJSON("./tachi/ktblack-songs-iidx.json"); - charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); - KTDATA_CACHE = { songs, charts }; - } + if (KTDATA_CACHE) { + songs = KTDATA_CACHE.songs; + charts = KTDATA_CACHE.charts; + } else { + songs = GetKTDataJSON("./tachi/ktblack-songs-iidx.json"); + charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json"); + KTDATA_CACHE = { songs, charts }; + } - await db.songs.iidx.remove({}); - await db.songs.iidx.insert(songs); - await db.charts.iidx.remove({}); - await db.charts.iidx.insert(charts); + await db.songs.iidx.remove({}); + await db.songs.iidx.insert(songs); + await db.charts.iidx.remove({}); + await db.charts.iidx.insert(charts); } export const barbScore: BarbatosScore = { - clear_type: 2, - did_fail: false, - difficulty: 1, - critical: 100, - error: 5, - near_total: 50, - near_fast: 40, - near_slow: 10, - gauge_type: 1, - is_skill_analyser: false, - level: 10, - max_chain: 100, - percent: 90, - score: 9_000_000, - song_id: 1, + clear_type: 2, + did_fail: false, + difficulty: 1, + critical: 100, + error: 5, + near_total: 50, + near_fast: 40, + near_slow: 10, + gauge_type: 1, + is_skill_analyser: false, + level: 10, + max_chain: 100, + percent: 90, + score: 9_000_000, + song_id: 1, }; export const uscChart: ChartDocument<"usc:Single"> = { - rgcID: null, - chartID: "USC_CHART_ID", - difficulty: "NOV", - songID: 1, - playtype: "Single", - levelNum: 1, - level: "1", - flags: { - CUSTOM: true, - }, - data: { - hashSHA1: "USC_CHART_HASH", - }, - isPrimary: true, - versions: [], + rgcID: null, + chartID: "USC_CHART_ID", + difficulty: "NOV", + songID: 1, + playtype: "Single", + levelNum: 1, + level: "1", + flags: { + CUSTOM: true, + }, + data: { + hashSHA1: "USC_CHART_HASH", + }, + isPrimary: true, + versions: [], }; export const uscScore: USCClientScore = { - crit: 100, - error: 5, - near: 50, - gauge: 80, - options: { - autoFlags: 0, - gaugeOpt: 0, - gaugeType: 1, - mirror: true, - random: false, - }, - score: 9_000_000, - timestamp: 0, - windows: { - good: USC_DEFAULT_NEAR, - perfect: USC_DEFAULT_PERFECT, - hold: USC_DEFAULT_HOLD, - miss: USC_DEFAULT_MISS, - slam: USC_DEFAULT_SLAM, - }, + crit: 100, + error: 5, + near: 50, + gauge: 80, + options: { + autoFlags: 0, + gaugeOpt: 0, + gaugeType: 1, + mirror: true, + random: false, + }, + score: 9_000_000, + timestamp: 0, + windows: { + good: USC_DEFAULT_NEAR, + perfect: USC_DEFAULT_PERFECT, + hold: USC_DEFAULT_HOLD, + miss: USC_DEFAULT_MISS, + slam: USC_DEFAULT_SLAM, + }, }; diff --git a/server/src/utils/class.ts b/server/src/utils/class.ts index 044915f87..446a57092 100644 --- a/server/src/utils/class.ts +++ b/server/src/utils/class.ts @@ -12,15 +12,15 @@ const logger = CreateLogCtx(__filename); * to compare to, and FALSE if it is worse or equal. */ export function ReturnClassIfGreater( - classSet: GameClassSets[IDStrings], - classVal: integer, - userGameStats?: UserGameStats | null + classSet: GameClassSets[IDStrings], + classVal: integer, + userGameStats?: UserGameStats | null ) { - if (!userGameStats || userGameStats.classes[classSet] === undefined) { - return null; - } + if (!userGameStats || userGameStats.classes[classSet] === undefined) { + return null; + } - return classVal > userGameStats.classes[classSet]!; + return classVal > userGameStats.classes[classSet]!; } /** @@ -32,51 +32,51 @@ export function ReturnClassIfGreater( * True if it was updated because it was better than UserGameStats. */ export async function UpdateClassIfGreater( - userID: integer, - game: Game, - playtype: Playtypes[Game], - classSet: GameClassSets[IDStrings], - classVal: integer + userID: integer, + game: Game, + playtype: Playtypes[Game], + classSet: GameClassSets[IDStrings], + classVal: integer ) { - const userGameStats = await db["game-stats"].findOne({ userID, game, playtype }); - const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); + const userGameStats = await db["game-stats"].findOne({ userID, game, playtype }); + const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); - if (isGreater === false) { - return false; - } + if (isGreater === false) { + return false; + } - if (userGameStats) { - await db["game-stats"].update( - { userID, game, playtype }, - { $set: { [`classes.${classSet}`]: classVal } } - ); - } else { - // insert new game stats for this user - this is an awkward place - // to call this - maybe we should call it elsewhere. - await db["game-stats"].insert({ - userID, - game, - playtype, - ratings: {}, - classes: { - [classSet]: classVal, - }, - }); - logger.info(`Created new player gamestats for ${userID} ${game} (${playtype})`); - } + if (userGameStats) { + await db["game-stats"].update( + { userID, game, playtype }, + { $set: { [`classes.${classSet}`]: classVal } } + ); + } else { + // insert new game stats for this user - this is an awkward place + // to call this - maybe we should call it elsewhere. + await db["game-stats"].insert({ + userID, + game, + playtype, + ratings: {}, + classes: { + [classSet]: classVal, + }, + }); + logger.info(`Created new player gamestats for ${userID} ${game} (${playtype})`); + } - if (isGreater === null) { - RedisPub("class-update", { userID, new: classVal, old: null, set: classSet }); + if (isGreater === null) { + RedisPub("class-update", { userID, new: classVal, old: null, set: classSet }); - return null; - } else { - RedisPub("class-update", { - userID, - new: classVal, - old: userGameStats!.classes[classSet]!, - set: classSet, - }); + return null; + } else { + RedisPub("class-update", { + userID, + new: classVal, + old: userGameStats!.classes[classSet]!, + set: classSet, + }); - return true; - } + return true; + } } diff --git a/server/src/utils/db.test.ts b/server/src/utils/db.test.ts index b10fbc038..8d9b1807e 100644 --- a/server/src/utils/db.test.ts +++ b/server/src/utils/db.test.ts @@ -5,27 +5,27 @@ import ResetDBState from "../test-utils/resets"; import { CloseAllConnections } from "../test-utils/close-connections"; t.test("#GetNextCounterValue", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Increments on valid counter hit", async (t) => { - const response = await GetNextCounterValue("real-counter"); + t.test("Increments on valid counter hit", async (t) => { + const response = await GetNextCounterValue("real-counter"); - // database starts with this at one - t.equal(response, 2, "Counter should return the current number stored"); + // database starts with this at one + t.equal(response, 2, "Counter should return the current number stored"); - const dbData = await db.counters.findOne({ - counterName: "real-counter", - }); + const dbData = await db.counters.findOne({ + counterName: "real-counter", + }); - t.equal(dbData!.value, 3, "Counter should increment after being hit"); - }); + t.equal(dbData!.value, 3, "Counter should increment after being hit"); + }); - t.rejects( - () => GetNextCounterValue("fake-counter"), - "Could not find sequence document for fake-counter." - ); + t.rejects( + () => GetNextCounterValue("fake-counter"), + "Could not find sequence document for fake-counter." + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/utils/db.ts b/server/src/utils/db.ts index cb1762a01..b594435f3 100644 --- a/server/src/utils/db.ts +++ b/server/src/utils/db.ts @@ -4,42 +4,42 @@ const logger = CreateLogCtx(__filename); import { integer, Game, ScoreDocument, PBScoreDocument } from "tachi-common"; export async function GetNextCounterValue(counterName: string): Promise { - const sequenceDoc = await db.counters.findOneAndUpdate( - { - counterName, - }, - { - $inc: { - value: 1, - }, - }, - { - // this is marked as deprecated, but it shouldn't be, as returnDocument: "before" - // does nothing. - returnOriginal: true, - } - ); + const sequenceDoc = await db.counters.findOneAndUpdate( + { + counterName, + }, + { + $inc: { + value: 1, + }, + }, + { + // this is marked as deprecated, but it shouldn't be, as returnDocument: "before" + // does nothing. + returnOriginal: true, + } + ); - if (!sequenceDoc) { - logger.error(`Could not find sequence document for ${counterName}`); - throw new Error(`Could not find sequence document for ${counterName}.`); - } + if (!sequenceDoc) { + logger.error(`Could not find sequence document for ${counterName}`); + throw new Error(`Could not find sequence document for ${counterName}.`); + } - return sequenceDoc.value; + return sequenceDoc.value; } export async function GetRelevantSongsAndCharts( - scores: (ScoreDocument | PBScoreDocument)[], - game: Game + scores: (ScoreDocument | PBScoreDocument)[], + game: Game ) { - const [songs, charts] = await Promise.all([ - db.songs[game].find({ - id: { $in: scores.map((e) => e.songID) }, - }), - db.charts[game].find({ - chartID: { $in: scores.map((e) => e.chartID) }, - }), - ]); + const [songs, charts] = await Promise.all([ + db.songs[game].find({ + id: { $in: scores.map((e) => e.songID) }, + }), + db.charts[game].find({ + chartID: { $in: scores.map((e) => e.chartID) }, + }), + ]); - return { songs, charts }; + return { songs, charts }; } diff --git a/server/src/utils/ea3id.ts b/server/src/utils/ea3id.ts index 54476f48a..e3bb794cb 100644 --- a/server/src/utils/ea3id.ts +++ b/server/src/utils/ea3id.ts @@ -1,19 +1,19 @@ export function ParseEA3SoftID(ver: string) { - const a = ver.split(":"); + const a = ver.split(":"); - if (a.length !== 5) { - throw new Error(`Invalid Version Code. Had ${a.length} components.`); - } + if (a.length !== 5) { + throw new Error(`Invalid Version Code. Had ${a.length} components.`); + } - if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) { - throw new Error(`Invalid Version Code.`); - } + if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) { + throw new Error(`Invalid Version Code.`); + } - return { - model: a[0], - dest: a[1], // region - spec: a[2], - rev: a[3], - ext: a[4], - }; + return { + model: a[0], + dest: a[1], // region + spec: a[2], + rev: a[3], + ext: a[4], + }; } diff --git a/server/src/utils/fetch.ts b/server/src/utils/fetch.ts index e0eb94cb8..8aa83bf3b 100644 --- a/server/src/utils/fetch.ts +++ b/server/src/utils/fetch.ts @@ -1,11 +1,11 @@ import nodeFetch from "node-fetch"; const fetch = - process.env.NODE_ENV === "test" - ? () => { - throw new Error("Cannot use real fetch inside testing env!"); - } - : nodeFetch; + process.env.NODE_ENV === "test" + ? () => { + throw new Error("Cannot use real fetch inside testing env!"); + } + : nodeFetch; export type NodeFetch = (url: RequestInfo, init?: RequestInit | undefined) => Promise; diff --git a/server/src/utils/folder.test.ts b/server/src/utils/folder.test.ts index de01cd2fd..5c1d6be00 100644 --- a/server/src/utils/folder.test.ts +++ b/server/src/utils/folder.test.ts @@ -3,10 +3,10 @@ import CreateLogCtx from "../lib/logger/logger"; import { Testing511SPA } from "../test-utils/test-data"; import { ChartDocument, FolderDocument } from "tachi-common"; import { - CreateFolderChartLookup, - GetFolderChartIDs, - GetFolderCharts, - ResolveFolderToCharts, + CreateFolderChartLookup, + GetFolderChartIDs, + GetFolderCharts, + ResolveFolderToCharts, } from "./folder"; import db from "../external/mongo/db"; import ResetDBState from "../test-utils/resets"; @@ -19,190 +19,190 @@ t.todo("#CreateFolderChartLookup"); t.todo("#GetFolderCharts"); t.test("#ResolveFolderToCharts", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Static Folder Tests", (t) => { - t.test("Basic Static", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "static", - data: [Testing511SPA.chartID], - }; + t.test("Static Folder Tests", (t) => { + t.test("Basic Static", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "static", + data: [Testing511SPA.chartID], + }; - const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); - t.equal(charts.length, 1, "Should return exactly 1 chart."); - t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); + t.equal(charts.length, 1, "Should return exactly 1 chart."); + t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); - t.end(); - }); + t.end(); + }); - t.test("Empty Static", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "static", - data: [], - }; + t.test("Empty Static", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "static", + data: [], + }; - const { charts } = await ResolveFolderToCharts(mockFolder as unknown as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as unknown as FolderDocument); - t.equal(charts.length, 0, "Should return exactly 0 charts."); + t.equal(charts.length, 0, "Should return exactly 0 charts."); - t.end(); - }); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Songs Folder Tests", (t) => { - t.test("Basic Song Query", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "songs", - data: { - title: { $regex: /5\.1\.1/u }, - }, - }; + t.test("Songs Folder Tests", (t) => { + t.test("Basic Song Query", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "songs", + data: { + title: { $regex: /5\.1\.1/u }, + }, + }; - const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); - t.equal(charts.length, 1, "Should return exactly 1 chart."); - t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); - t.end(); - }); + t.equal(charts.length, 1, "Should return exactly 1 chart."); + t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); + t.end(); + }); - t.test("Invalid Song Query", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "songs", - data: { - FieldThatDoesntExist: { $regex: /5\.1\.1/u }, - }, - }; + t.test("Invalid Song Query", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "songs", + data: { + FieldThatDoesntExist: { $regex: /5\.1\.1/u }, + }, + }; - const { charts } = await ResolveFolderToCharts(mockFolder as unknown as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as unknown as FolderDocument); - t.equal(charts.length, 0, "Should return exactly 0 charts."); - t.end(); - }); + t.equal(charts.length, 0, "Should return exactly 0 charts."); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Charts Folder Tests", (t) => { - t.test("Basic Song Query", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "charts", - data: { - songID: Testing511SPA.songID, // lol - }, - }; + t.test("Charts Folder Tests", (t) => { + t.test("Basic Song Query", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "charts", + data: { + songID: Testing511SPA.songID, // lol + }, + }; - const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); - t.equal(charts.length, 1, "Should return exactly 1 chart."); - t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); - t.end(); - }); + t.equal(charts.length, 1, "Should return exactly 1 chart."); + t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); + t.end(); + }); - t.end(); - }); + t.end(); + }); - t.test("Filter", async (t) => { - // add fake "NORMAL" 511 chart - await db.charts.iidx.insert( - deepmerge(Testing511SPA, { - difficulty: "NORMAL", - chartID: "FAKE_511_SPN", - }) as ChartDocument<"iidx:SP"> - ); + t.test("Filter", async (t) => { + // add fake "NORMAL" 511 chart + await db.charts.iidx.insert( + deepmerge(Testing511SPA, { + difficulty: "NORMAL", + chartID: "FAKE_511_SPN", + }) as ChartDocument<"iidx:SP"> + ); - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "charts", - data: { - songID: Testing511SPA.songID, // lol - }, - }; + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "charts", + data: { + songID: Testing511SPA.songID, // lol + }, + }; - const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); + const { charts } = await ResolveFolderToCharts(mockFolder as FolderDocument); - t.equal(charts.length, 2, "Should return exactly 2 charts."); - t.strictSame( - charts.map((e) => e.chartID), - [Testing511SPA.chartID, "FAKE_511_SPN"], - "Should return all 511 charts." - ); + t.equal(charts.length, 2, "Should return exactly 2 charts."); + t.strictSame( + charts.map((e) => e.chartID), + [Testing511SPA.chartID, "FAKE_511_SPN"], + "Should return all 511 charts." + ); - const { charts: charts2 } = await ResolveFolderToCharts(mockFolder as FolderDocument, { - difficulty: "NORMAL", - }); + const { charts: charts2 } = await ResolveFolderToCharts(mockFolder as FolderDocument, { + difficulty: "NORMAL", + }); - t.equal(charts2.length, 1, "Should return exactly 1 chart."); - t.equal(charts2[0].chartID, "FAKE_511_SPN", "Should only return 511 SPN"); + t.equal(charts2.length, 1, "Should return exactly 1 chart."); + t.equal(charts2[0].chartID, "FAKE_511_SPN", "Should only return 511 SPN"); - t.end(); - }); + t.end(); + }); - t.test("Songs", async (t) => { - const mockFolder = { - game: "iidx", - playtype: "SP", - type: "charts", - data: { - songID: Testing511SPA.songID, // lol - }, - }; + t.test("Songs", async (t) => { + const mockFolder = { + game: "iidx", + playtype: "SP", + type: "charts", + data: { + songID: Testing511SPA.songID, // lol + }, + }; - const { charts, songs } = await ResolveFolderToCharts( - mockFolder as FolderDocument, - {}, - true - ); + const { charts, songs } = await ResolveFolderToCharts( + mockFolder as FolderDocument, + {}, + true + ); - t.equal(charts.length, 1, "Should return exactly 1 chart."); - t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); + t.equal(charts.length, 1, "Should return exactly 1 chart."); + t.equal(charts[0].chartID, Testing511SPA.chartID, "Should return 511 [SPA]"); - t.equal(songs.length, 1, "Should return exactly 1 song."); - t.equal(songs[0].id, Testing511SPA.songID, "Should return 511's song."); + t.equal(songs.length, 1, "Should return exactly 1 song."); + t.equal(songs[0].id, Testing511SPA.songID, "Should return 511's song."); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.test("#GetFolderChartIDs", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should use the folder-cache", async (t) => { - await db["folder-chart-lookup"].insert( - ["a", "b", "c", "d"].map((e) => ({ chartID: e, folderID: "folder1" })) - ); + t.test("Should use the folder-cache", async (t) => { + await db["folder-chart-lookup"].insert( + ["a", "b", "c", "d"].map((e) => ({ chartID: e, folderID: "folder1" })) + ); - await db["folder-chart-lookup"].insert( - ["e", "f", "g", "h"].map((e) => ({ chartID: e, folderID: "folder2" })) - ); + await db["folder-chart-lookup"].insert( + ["e", "f", "g", "h"].map((e) => ({ chartID: e, folderID: "folder2" })) + ); - let fcIDs = await GetFolderChartIDs("folder1"); + let fcIDs = await GetFolderChartIDs("folder1"); - t.strictSame(fcIDs, ["a", "b", "c", "d"], "Should return the right chartIDs for folder1"); + t.strictSame(fcIDs, ["a", "b", "c", "d"], "Should return the right chartIDs for folder1"); - fcIDs = await GetFolderChartIDs("folder2"); + fcIDs = await GetFolderChartIDs("folder2"); - t.strictSame(fcIDs, ["e", "f", "g", "h"], "Should return the right chartIDs for folder2"); + t.strictSame(fcIDs, ["e", "f", "g", "h"], "Should return the right chartIDs for folder2"); - t.end(); - }); + t.end(); + }); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/utils/folder.ts b/server/src/utils/folder.ts index 4690a657f..a951f06fe 100644 --- a/server/src/utils/folder.ts +++ b/server/src/utils/folder.ts @@ -9,127 +9,127 @@ const logger = CreateLogCtx(__filename); // overloads! export async function ResolveFolderToCharts( - folder: FolderDocument, - filter: FilterQuery, - getSongs: true + folder: FolderDocument, + filter: FilterQuery, + getSongs: true ): Promise<{ songs: AnySongDocument[]; charts: AnyChartDocument[] }>; export async function ResolveFolderToCharts( - folder: FolderDocument, - filter?: FilterQuery, - getSongs?: false + folder: FolderDocument, + filter?: FilterQuery, + getSongs?: false ): Promise<{ charts: AnyChartDocument[] }>; export async function ResolveFolderToCharts( - folder: FolderDocument, - filter: FilterQuery = {}, - getSongs = false + folder: FolderDocument, + filter: FilterQuery = {}, + getSongs = false ): Promise<{ songs?: AnySongDocument[]; charts: AnyChartDocument[] }> { - let songs: AnySongDocument[] | null = null; - let charts: AnyChartDocument[]; + let songs: AnySongDocument[] | null = null; + let charts: AnyChartDocument[]; - if (folder.type === "static") { - charts = await db.charts[folder.game].find( - deepmerge(filter, { - playtype: folder.playtype, // mandatory - chartID: { $in: folder.data }, // @todo - }) - ); - } else if (folder.type === "songs") { - songs = await db.songs[folder.game].find(folder.data); + if (folder.type === "static") { + charts = await db.charts[folder.game].find( + deepmerge(filter, { + playtype: folder.playtype, // mandatory + chartID: { $in: folder.data }, // @todo + }) + ); + } else if (folder.type === "songs") { + songs = await db.songs[folder.game].find(folder.data); - charts = await db.charts[folder.game].find( - deepmerge(filter, { - playtype: folder.playtype, - songID: { $in: songs.map((e) => e.id) }, - }) - ); - } else if (folder.type === "charts") { - const folderDataTransposed: Record = {}; + charts = await db.charts[folder.game].find( + deepmerge(filter, { + playtype: folder.playtype, + songID: { $in: songs.map((e) => e.id) }, + }) + ); + } else if (folder.type === "charts") { + const folderDataTransposed: Record = {}; - for (const key in folder.data) { - folderDataTransposed[key.replace(/¬/gu, ".")] = folder.data[key]; - } + for (const key in folder.data) { + folderDataTransposed[key.replace(/¬/gu, ".")] = folder.data[key]; + } - const fx = deepmerge.all([filter, { playtype: folder.playtype }, folderDataTransposed]); + const fx = deepmerge.all([filter, { playtype: folder.playtype }, folderDataTransposed]); - charts = await db.charts[folder.game].find(fx); - } else { - // @ts-expect-error This is already a weird scenario. Shouldn't fail, though. - logger.error(`Invalid folder at ${folder.folderID}. Cannot resolve.`, { folder }); + charts = await db.charts[folder.game].find(fx); + } else { + // @ts-expect-error This is already a weird scenario. Shouldn't fail, though. + logger.error(`Invalid folder at ${folder.folderID}. Cannot resolve.`, { folder }); - // @ts-expect-error See above - throw new Error(`Invalid folder ${folder.folderID}. Cannot resolve.`); - } + // @ts-expect-error See above + throw new Error(`Invalid folder ${folder.folderID}. Cannot resolve.`); + } - if (getSongs) { - if (songs) { - return { songs, charts }; - } + if (getSongs) { + if (songs) { + return { songs, charts }; + } - songs = await db.songs[folder.game].find({ - id: { $in: charts.map((e) => e.songID) }, - }); + songs = await db.songs[folder.game].find({ + id: { $in: charts.map((e) => e.songID) }, + }); - return { songs, charts }; - } + return { songs, charts }; + } - return { charts }; + return { charts }; } export async function GetFolderCharts( - folder: FolderDocument, - filter: FilterQuery, - getSongs: true + folder: FolderDocument, + filter: FilterQuery, + getSongs: true ): Promise<{ songs: AnySongDocument[]; charts: AnyChartDocument[] }>; export async function GetFolderCharts( - folder: FolderDocument, - filter: FilterQuery, - getSongs?: false + folder: FolderDocument, + filter: FilterQuery, + getSongs?: false ): Promise<{ charts: AnyChartDocument[] }>; export async function GetFolderCharts( - folder: FolderDocument, - filter: FilterQuery = {}, - getSongs = false + folder: FolderDocument, + filter: FilterQuery = {}, + getSongs = false ): Promise<{ songs?: AnySongDocument[]; charts: AnyChartDocument[] }> { - const chartIDs = await GetFolderChartIDs(folder.folderID); + const chartIDs = await GetFolderChartIDs(folder.folderID); - const charts = await db.charts[folder.game].find( - deepmerge.all([{ playtype: folder.playtype }, { chartID: { $in: chartIDs } }, filter]) - ); + const charts = await db.charts[folder.game].find( + deepmerge.all([{ playtype: folder.playtype }, { chartID: { $in: chartIDs } }, filter]) + ); - if (getSongs) { - const songs = await db.songs[folder.game].find({ - id: { $in: charts.map((e) => e.songID) }, - }); + if (getSongs) { + const songs = await db.songs[folder.game].find({ + id: { $in: charts.map((e) => e.songID) }, + }); - return { songs, charts }; - } + return { songs, charts }; + } - return { charts }; + return { charts }; } export async function GetFolderChartIDs(folderID: string) { - const chartIDs = await db["folder-chart-lookup"].find( - { - folderID, - }, - { - projection: { - chartID: 1, - }, - } - ); + const chartIDs = await db["folder-chart-lookup"].find( + { + folderID, + }, + { + projection: { + chartID: 1, + }, + } + ); - return chartIDs.map((e) => e.chartID); + return chartIDs.map((e) => e.chartID); } export async function CreateFolderChartLookup(folder: FolderDocument) { - const { charts } = await ResolveFolderToCharts(folder, {}, false); + const { charts } = await ResolveFolderToCharts(folder, {}, false); - await db["folder-chart-lookup"].insert( - charts.map((c) => ({ - folderID: folder.folderID, - chartID: c.chartID, - })) - ); + await db["folder-chart-lookup"].insert( + charts.map((c) => ({ + folderID: folder.folderID, + chartID: c.chartID, + })) + ); } /** @@ -137,14 +137,14 @@ export async function CreateFolderChartLookup(folder: FolderDocument) { * common use cases, such as retrieving chartIDs from a folder. */ export async function InitaliseFolderChartLookup() { - logger.info(`Started InitialiseFolderChartLookup`); - await db["folder-chart-lookup"].remove({}); - logger.info(`Flushed Cache.`); + logger.info(`Started InitialiseFolderChartLookup`); + await db["folder-chart-lookup"].remove({}); + logger.info(`Flushed Cache.`); - const folders = await db.folders.find({}); - logger.info(`Reloading ${folders.length} folders.`); + const folders = await db.folders.find({}); + logger.info(`Reloading ${folders.length} folders.`); - await Promise.all(folders.map(CreateFolderChartLookup)); + await Promise.all(folders.map(CreateFolderChartLookup)); - logger.info(`Completed InitialiseFolderChartLookup.`); + logger.info(`Completed InitialiseFolderChartLookup.`); } diff --git a/server/src/utils/milestone.ts b/server/src/utils/milestone.ts index 660f6f542..70391eb09 100644 --- a/server/src/utils/milestone.ts +++ b/server/src/utils/milestone.ts @@ -5,9 +5,9 @@ import { MilestoneDocument, integer } from "tachi-common"; * nested structure of milestones. */ export function GetGoalIDsFromMilestone(milestone: MilestoneDocument) { - // this sucks - maybe a nicer way to do this, because nested - // maps are just ugly - return milestone.milestoneData.map((e) => e.goals.map((e) => e.goalID)).flat(1); + // this sucks - maybe a nicer way to do this, because nested + // maps are just ugly + return milestone.milestoneData.map((e) => e.goals.map((e) => e.goalID)).flat(1); } /** @@ -15,27 +15,27 @@ export function GetGoalIDsFromMilestone(milestone: MilestoneDocument) { * milestone to be considered completed. */ export function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: string[]): integer { - if (milestone.criteria.type === "all") { - return goalIDs.length; - } else if (milestone.criteria.type === "abs") { - if (milestone.criteria.value === null) { - throw new Error( - `Invalid milestone ${milestone.milestoneID} - abs and null are not compatible.` - ); - } + if (milestone.criteria.type === "all") { + return goalIDs.length; + } else if (milestone.criteria.type === "abs") { + if (milestone.criteria.value === null) { + throw new Error( + `Invalid milestone ${milestone.milestoneID} - abs and null are not compatible.` + ); + } - return milestone.criteria.value!; - } else if (milestone.criteria.type === "proportion") { - if (milestone.criteria.value === null) { - throw new Error( - `Invalid milestone ${milestone.milestoneID} - proportion and null are not compatible.` - ); - } + return milestone.criteria.value!; + } else if (milestone.criteria.type === "proportion") { + if (milestone.criteria.value === null) { + throw new Error( + `Invalid milestone ${milestone.milestoneID} - proportion and null are not compatible.` + ); + } - return Math.floor(milestone.criteria.value * goalIDs.length); - } + return Math.floor(milestone.criteria.value * goalIDs.length); + } - throw new Error( - `Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}` - ); + throw new Error( + `Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}` + ); } diff --git a/server/src/utils/misc.test.ts b/server/src/utils/misc.test.ts index d1fe5323c..0c1caa531 100644 --- a/server/src/utils/misc.test.ts +++ b/server/src/utils/misc.test.ts @@ -2,8 +2,8 @@ import t from "tap"; import { GetMilisecondsSince } from "./misc"; t.test("#GetMilisecondsSince", (t) => { - const time = GetMilisecondsSince(10n); - t.ok(typeof time === "number" && time > 0, "Should return a number greater than 0."); + const time = GetMilisecondsSince(10n); + t.ok(typeof time === "number" && time > 0, "Should return a number greater than 0."); - t.end(); + t.end(); }); diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 0b612c42e..091ca3db9 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -9,13 +9,13 @@ import { validPlaytypes } from "tachi-common/js/config"; // shim. export function EscapeStringRegexp(string: string) { - if (typeof string !== "string") { - throw new TypeError("Expected a string"); - } + if (typeof string !== "string") { + throw new TypeError("Expected a string"); + } - // Escape characters with special meaning either inside or outside character sets. - // Use a simple backslash escape when it's always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns' stricter grammar. - return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d"); + // Escape characters with special meaning either inside or outside character sets. + // Use a simple backslash escape when it's always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns' stricter grammar. + return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d"); } /** @@ -23,31 +23,31 @@ export function EscapeStringRegexp(string: string) { * This function will not work if more than 100(ish) days have passed since the first reference. */ export function GetMilisecondsSince(ref: bigint) { - return Number(process.hrtime.bigint() - ref) / 1e6; + return Number(process.hrtime.bigint() - ref) / 1e6; } export function Random20Hex() { - return crypto.randomBytes(20).toString("hex"); + return crypto.randomBytes(20).toString("hex"); } export function MStoS(ms: number) { - return Math.floor(ms / 1000); + return Math.floor(ms / 1000); } /** * Random From Array - Selects a random value from an array. */ export function RFA(arr: unknown[]) { - return arr[Math.floor(Math.random() * arr.length)]; + return arr[Math.floor(Math.random() * arr.length)]; } /** * Splits an Authorization header into the token and the type. */ export function SplitAuthorizationHeader(authHeader: string) { - const parts = authHeader.split(" "); + const parts = authHeader.split(" "); - return { type: parts[0], token: parts.slice(1).join(" ") }; + return { type: parts[0], token: parts.slice(1).join(" ") }; } /** @@ -59,13 +59,13 @@ export function SplitAuthorizationHeader(authHeader: string) { * False if the object doesn't. */ export function HasOwnProperty(obj: T, key: string | number | symbol): key is keyof T { - return Object.prototype.hasOwnProperty.call(obj, key); + return Object.prototype.hasOwnProperty.call(obj, key); } export function IsValidGame(str: string): str is Game { - return !!CONF_INFO.SUPPORTED_GAMES.includes(str as Game); + return !!CONF_INFO.SUPPORTED_GAMES.includes(str as Game); } export function IsValidPlaytype(game: Game, str: string): str is Playtypes[Game] { - return validPlaytypes[game].includes(str as Playtypes[Game]); + return validPlaytypes[game].includes(str as Playtypes[Game]); } diff --git a/server/src/utils/prudence.ts b/server/src/utils/prudence.ts index aa7e7fcfb..53876c00b 100644 --- a/server/src/utils/prudence.ts +++ b/server/src/utils/prudence.ts @@ -2,12 +2,12 @@ import p, { PrudenceError, ValidSchemaValue } from "prudence"; export function FormatPrError(err: PrudenceError, foreword = "Error") { - const receivedText = - typeof err.userVal === "object" && err.userVal !== null - ? "" - : ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`; + const receivedText = + typeof err.userVal === "object" && err.userVal !== null + ? "" + : ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`; - return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`; + return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`; } export const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v)); diff --git a/server/src/utils/queries/charts.ts b/server/src/utils/queries/charts.ts index 8a34b1b5f..cb3a5d993 100644 --- a/server/src/utils/queries/charts.ts +++ b/server/src/utils/queries/charts.ts @@ -2,7 +2,7 @@ import { Difficulties, Game, integer, Playtypes, IDStrings } from "tachi-common" import db from "../../external/mongo/db"; export function FindChartWithChartID(game: Game, chartID: string) { - return db.charts[game].findOne({ chartID }); + return db.charts[game].findOne({ chartID }); } /** @@ -11,16 +11,16 @@ export function FindChartWithChartID(game: Game, chartID: string) { * @see FindChartWithPTDFVersion */ export function FindChartWithPTDF< - G extends Game = Game, - P extends Playtypes[G] = Playtypes[G], - I extends IDStrings = IDStrings + G extends Game = Game, + P extends Playtypes[G] = Playtypes[G], + I extends IDStrings = IDStrings >(game: G, songID: integer, playtype: P, difficulty: Difficulties[I]) { - return db.charts[game].findOne({ - songID: songID, - playtype: playtype, - difficulty: difficulty, - isPrimary: true, - }); + return db.charts[game].findOne({ + songID: songID, + playtype: playtype, + difficulty: difficulty, + isPrimary: true, + }); } /** @@ -28,16 +28,16 @@ export function FindChartWithPTDF< * `isPrimary` set. */ export function FindChartWithPTDFVersion< - G extends Game = Game, - P extends Playtypes[G] = Playtypes[G], - I extends IDStrings = IDStrings + G extends Game = Game, + P extends Playtypes[G] = Playtypes[G], + I extends IDStrings = IDStrings >(game: G, songID: integer, playtype: P, difficulty: Difficulties[I], version: string) { - return db.charts[game].findOne({ - songID: songID, - playtype: playtype, - difficulty: difficulty, - versions: version, - }); + return db.charts[game].findOne({ + songID: songID, + playtype: playtype, + difficulty: difficulty, + versions: version, + }); } /** @@ -52,20 +52,20 @@ export function FindChartWithPTDFVersion< * @param difficulty The difficulty for the chart. */ export function FindDDRChartOnSongHash( - songHash: string, - // Technically both of these should be "ddr" instead of Game, but it proves hard to work with. - playtype: Playtypes[Game], - difficulty: Difficulties[IDStrings] + songHash: string, + // Technically both of these should be "ddr" instead of Game, but it proves hard to work with. + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings] ) { - // note: this only works on accident because monk - // allows any strings like "foo.bar". - // We *should* normally cast this to ChartDocument<"ddr:SP" | "ddr:DP"> - return db.charts.ddr.findOne({ - "data.songHash": songHash, - playtype, - difficulty, - isPrimary: true, - }); + // note: this only works on accident because monk + // allows any strings like "foo.bar". + // We *should* normally cast this to ChartDocument<"ddr:SP" | "ddr:DP"> + return db.charts.ddr.findOne({ + "data.songHash": songHash, + playtype, + difficulty, + isPrimary: true, + }); } /** @@ -73,29 +73,29 @@ export function FindDDRChartOnSongHash( * @param hash The md5 or sha256 hash to look for. */ export function FindBMSChartOnHash(hash: string) { - return db.charts.bms.findOne({ - $or: [{ "data.hashMD5": hash }, { "data.hashSHA256": hash }], - }); + return db.charts.bms.findOne({ + $or: [{ "data.hashMD5": hash }, { "data.hashSHA256": hash }], + }); } /** * Find a chart on its in-game-ID, playtype and difficulty. */ export function FindChartOnInGameID( - game: Game, - inGameID: number, - playtype: Playtypes[Game], - difficulty: Difficulties[IDStrings] + game: Game, + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings] ) { - if (game === "bms" || game === "usc") { - throw new Error(`Cannot call FindChartOnInGameID for game ${game}.`); - } + if (game === "bms" || game === "usc") { + throw new Error(`Cannot call FindChartOnInGameID for game ${game}.`); + } - return db.charts[game].findOne({ - "data.inGameID": inGameID, - playtype, - difficulty, - }); + return db.charts[game].findOne({ + "data.inGameID": inGameID, + playtype, + difficulty, + }); } /** @@ -103,17 +103,17 @@ export function FindChartOnInGameID( * This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate. */ export function FindIIDXChartOnInGameID( - inGameID: number, - playtype: Playtypes[Game], - difficulty: Difficulties[IDStrings] + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings] ) { - return db.charts.iidx.findOne({ - "data.inGameID": inGameID, - "flags.2dxtra": false, - isPrimary: true, - playtype, - difficulty, - }); + return db.charts.iidx.findOne({ + "data.inGameID": inGameID, + "flags.2dxtra": false, + isPrimary: true, + playtype, + difficulty, + }); } /** @@ -121,45 +121,45 @@ export function FindIIDXChartOnInGameID( * This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate. */ export function FindIIDXChartOnInGameIDVersion( - inGameID: number, - playtype: Playtypes[Game], - difficulty: Difficulties[IDStrings], - version: string + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings], + version: string ) { - return db.charts.iidx.findOne({ - "data.inGameID": inGameID, - "flags.2dxtra": false, - playtype, - difficulty, - versions: version, - }); + return db.charts.iidx.findOne({ + "data.inGameID": inGameID, + "flags.2dxtra": false, + playtype, + difficulty, + versions: version, + }); } /** * Find a chart on its in-game-ID, playtype, difficulty and version. */ export function FindChartOnInGameIDVersion( - game: Game, - inGameID: number, - playtype: Playtypes[Game], - difficulty: Difficulties[IDStrings], - version: string + game: Game, + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings], + version: string ) { - return db.charts[game].findOne({ - "data.inGameID": inGameID, - versions: version, - playtype, - difficulty, - }); + return db.charts[game].findOne({ + "data.inGameID": inGameID, + versions: version, + playtype, + difficulty, + }); } /** * Finds an IIDX chart on its 2dxtra hash, which is the sha256 of the .1 buffer. */ export function FindIIDXChartWith2DXtraHash(hash: string) { - return db.charts.iidx.findOne({ - "data.hashSHA256": hash, - }); + return db.charts.iidx.findOne({ + "data.hashSHA256": hash, + }); } /** @@ -169,50 +169,50 @@ export function FindIIDXChartWith2DXtraHash(hash: string) { * all of those as the same difficulty, but we do not. */ export function FindSDVXChartOnInGameID( - inGameID: number, - difficulty: Difficulties["sdvx:Single"] | "ANY_INF" + inGameID: number, + difficulty: Difficulties["sdvx:Single"] | "ANY_INF" ) { - const diffQuery = - difficulty === "ANY_INF" - ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } - : difficulty; + const diffQuery = + difficulty === "ANY_INF" + ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } + : difficulty; - return db.charts.sdvx.findOne({ - "data.inGameID": inGameID, - difficulty: diffQuery, - isPrimary: true, - }); + return db.charts.sdvx.findOne({ + "data.inGameID": inGameID, + difficulty: diffQuery, + isPrimary: true, + }); } export function FindSDVXChartOnInGameIDVersion( - inGameID: number, - difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF", - version: string + inGameID: number, + difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF", + version: string ) { - const diffQuery = - difficulty === "ANY_INF" - ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } - : difficulty; + const diffQuery = + difficulty === "ANY_INF" + ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } + : difficulty; - return db.charts.sdvx.findOne({ - "data.inGameID": inGameID, - difficulty: diffQuery, - versions: version, - }); + return db.charts.sdvx.findOne({ + "data.inGameID": inGameID, + difficulty: diffQuery, + versions: version, + }); } export function FindChartOnSHA256(game: Game, hash: string) { - if (game !== "bms" && game !== "usc" && game !== "iidx") { - throw new Error(`Cannot call FindChartOnSHA256 for game ${game}.`); - } + if (game !== "bms" && game !== "usc" && game !== "iidx") { + throw new Error(`Cannot call FindChartOnSHA256 for game ${game}.`); + } - return db.charts[game].findOne({ - "data.hashSHA256": hash, - }); + return db.charts[game].findOne({ + "data.hashSHA256": hash, + }); } export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID: string) { - return db.charts[game].findOne({ - "data.arcChartID": arcID, - }); + return db.charts[game].findOne({ + "data.arcChartID": arcID, + }); } diff --git a/server/src/utils/queries/songs.ts b/server/src/utils/queries/songs.ts index 09248491b..552d030d7 100644 --- a/server/src/utils/queries/songs.ts +++ b/server/src/utils/queries/songs.ts @@ -14,20 +14,20 @@ import { EscapeStringRegexp } from "../misc"; * @returns AnySongDocument */ export function FindSongOnTitle( - game: Game, - title: string + game: Game, + title: string ): Promise> { - // @optimisable: Performance should be tested here by having a utility field for all-titles. - return db.songs[game].findOne({ - $or: [ - { - title: title, - }, - { - "alt-titles": title, - }, - ], - }); + // @optimisable: Performance should be tested here by having a utility field for all-titles. + return db.songs[game].findOne({ + $or: [ + { + title: title, + }, + { + "alt-titles": title, + }, + ], + }); } /** @@ -35,22 +35,22 @@ export function FindSongOnTitle( * This is needed for services that provide horrifically mutated string titles. */ export function FindSongOnTitleInsensitive( - game: Game, - title: string + game: Game, + title: string ): Promise> { - // @optimisable: Performance should be tested here by having a utility field for all-titles. + // @optimisable: Performance should be tested here by having a utility field for all-titles. - const regex = new RegExp(`^${EscapeStringRegexp(title)}$`, "iu"); - return db.songs[game].findOne({ - $or: [ - { - title: { $regex: regex }, - }, - { - "alt-titles": { $regex: regex }, - }, - ], - }); + const regex = new RegExp(`^${EscapeStringRegexp(title)}$`, "iu"); + return db.songs[game].findOne({ + $or: [ + { + title: { $regex: regex }, + }, + { + "alt-titles": { $regex: regex }, + }, + ], + }); } /** @@ -61,18 +61,18 @@ export function FindSongOnTitleInsensitive( * @returns AnySongDocument */ export function FindSongOnID(game: Game, songID: integer): Promise> { - return db.songs[game].findOne({ - id: songID, - }); + return db.songs[game].findOne({ + id: songID, + }); } export async function FindSongOnIDGuaranteed(game: Game, songID: integer, logger: KtLogger) { - const song = await FindSongOnID(game, songID); + const song = await FindSongOnID(game, songID); - if (!song) { - logger.severe(`Song-Chart desync for ${songID}. Has charts, but no song.`); - throw new InternalFailure(`Song-Chart desync for ${songID}. Has charts, but no song.`); - } + if (!song) { + logger.severe(`Song-Chart desync for ${songID}. Has charts, but no song.`); + throw new InternalFailure(`Song-Chart desync for ${songID}. Has charts, but no song.`); + } - return song; + return song; } diff --git a/server/src/utils/req-tachi-data.ts b/server/src/utils/req-tachi-data.ts index 276f7c7ad..92dae8dd7 100644 --- a/server/src/utils/req-tachi-data.ts +++ b/server/src/utils/req-tachi-data.ts @@ -4,9 +4,9 @@ import { TachiRequestData } from "./types"; import deepmerge from "deepmerge"; export function AssignToReqTachiData(req: Request, data: Partial) { - if (!req[SYMBOL_TachiData]) { - req[SYMBOL_TachiData] = data; - } else { - req[SYMBOL_TachiData] = deepmerge(req[SYMBOL_TachiData]!, data); - } + if (!req[SYMBOL_TachiData]) { + req[SYMBOL_TachiData] = data; + } else { + req[SYMBOL_TachiData] = deepmerge(req[SYMBOL_TachiData]!, data); + } } diff --git a/server/src/utils/scores.ts b/server/src/utils/scores.ts index 929a06047..3c717a6b8 100644 --- a/server/src/utils/scores.ts +++ b/server/src/utils/scores.ts @@ -1,42 +1,42 @@ import db from "../external/mongo/db"; import { - integer, - PBScoreDocument, - ScoreDocument, - AnyChartDocument, - AnySongDocument, + integer, + PBScoreDocument, + ScoreDocument, + AnyChartDocument, + AnySongDocument, } from "tachi-common"; export function GetPBOnChart(userID: integer, chartID: string) { - return db["personal-bests"].findOne({ - userID, - chartID, - }); + return db["personal-bests"].findOne({ + userID, + chartID, + }); } export function GetServerRecordOnChart(chartID: string) { - return db["personal-bests"].findOne({ - chartID, - "rankingData.rank": 1, - }); + return db["personal-bests"].findOne({ + chartID, + "rankingData.rank": 1, + }); } export function FilterChartsAndSongs( - scores: (PBScoreDocument | ScoreDocument)[], - charts: AnyChartDocument[], - songs: AnySongDocument[] + scores: (PBScoreDocument | ScoreDocument)[], + charts: AnyChartDocument[], + songs: AnySongDocument[] ) { - const chartIDs = new Set(); - const songIDs = new Set(); + const chartIDs = new Set(); + const songIDs = new Set(); - for (const score of scores) { - chartIDs.add(score.chartID); - songIDs.add(score.songID); - } + for (const score of scores) { + chartIDs.add(score.chartID); + songIDs.add(score.songID); + } - // filter out irrelevant songs and charts - return { - songs: songs.filter((e) => songIDs.has(e.id)), - charts: charts.filter((e) => chartIDs.has(e.chartID)), - }; + // filter out irrelevant songs and charts + return { + songs: songs.filter((e) => songIDs.has(e.id)), + charts: charts.filter((e) => chartIDs.has(e.chartID)), + }; } diff --git a/server/src/utils/session.test.ts b/server/src/utils/session.test.ts index 551a53f72..344847e75 100644 --- a/server/src/utils/session.test.ts +++ b/server/src/utils/session.test.ts @@ -5,29 +5,29 @@ import ResetDBState from "../test-utils/resets"; import { GetScoresFromSession } from "./session"; t.test("#GetScoresFromSession", async (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - await ResetDBState(); + await ResetDBState(); - const exampleSession = await db.sessions.findOne(); + const exampleSession = await db.sessions.findOne(); - const scores = await GetScoresFromSession(exampleSession!); + const scores = await GetScoresFromSession(exampleSession!); - t.equal( - scores.length, - exampleSession!.scoreInfo.length, - "Should return the same amount of scores as the session." - ); + t.equal( + scores.length, + exampleSession!.scoreInfo.length, + "Should return the same amount of scores as the session." + ); - for (let i = 0; i < exampleSession!.scoreInfo.length; i++) { - t.equal( - scores[i].scoreID, - exampleSession!.scoreInfo[i].scoreID, - "Should return the scores requested." - ); - } + for (let i = 0; i < exampleSession!.scoreInfo.length; i++) { + t.equal( + scores[i].scoreID, + exampleSession!.scoreInfo[i].scoreID, + "Should return the scores requested." + ); + } - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/utils/session.ts b/server/src/utils/session.ts index ca5940631..361d27044 100644 --- a/server/src/utils/session.ts +++ b/server/src/utils/session.ts @@ -7,9 +7,9 @@ import db from "../external/mongo/db"; * @param session The session to retrieve the score documents of. */ export function GetScoresFromSession(session: SessionDocument) { - return db.scores.find({ - scoreID: { $in: session.scoreInfo.map((e) => e.scoreID) }, - }); + return db.scores.find({ + scoreID: { $in: session.scoreInfo.map((e) => e.scoreID) }, + }); } /** @@ -17,8 +17,8 @@ export function GetScoresFromSession(session: SessionDocument) { * @param score The score to return the associated session of. */ export function GetSessionFromScore(score: ScoreDocument) { - return db.sessions.findOne({ - // ??? another bug in monks types i think - scoreInfo: { scoreID: score.scoreID } as Condition, - }); + return db.sessions.findOne({ + // ??? another bug in monks types i think + scoreInfo: { scoreID: score.scoreID } as Condition, + }); } diff --git a/server/src/utils/tierlist.ts b/server/src/utils/tierlist.ts index db5411e6d..d3944f96c 100644 --- a/server/src/utils/tierlist.ts +++ b/server/src/utils/tierlist.ts @@ -6,70 +6,70 @@ import CreateLogCtx from "../lib/logger/logger"; const logger = CreateLogCtx(__filename); export function CalculateTierlistDataID( - chartID: string, - type: "score" | "lamp" | "grade", - key: string | null, - tierlistID: string + chartID: string, + type: "score" | "lamp" | "grade", + key: string | null, + tierlistID: string ) { - return crypto - .createHash("sha256") - .update(`${chartID}|${type}|${key}|${tierlistID}`) - .digest("hex"); + return crypto + .createHash("sha256") + .update(`${chartID}|${type}|${key}|${tierlistID}`) + .digest("hex"); } export function GetDefaultTierlist(game: Game, playtype: Playtypes[Game]) { - return db.tierlists.findOne({ - game, - playtype, - isDefault: true, - }); + return db.tierlists.findOne({ + game, + playtype, + isDefault: true, + }); } async function GetDefaultTierlistID(game: Game, playtype: Playtypes[Game]) { - const tierlistData = await GetDefaultTierlist(game, playtype); + const tierlistData = await GetDefaultTierlist(game, playtype); - if (!tierlistData) { - logger.warn( - `Attempted to GetTierlistData for ${game} ${playtype}, but no default tierlist is set.` - ); - return undefined; - } + if (!tierlistData) { + logger.warn( + `Attempted to GetTierlistData for ${game} ${playtype}, but no default tierlist is set.` + ); + return undefined; + } - return tierlistData.tierlistID; + return tierlistData.tierlistID; } export async function GetOneTierlistData( - game: Game, - chart: AnyChartDocument, - type: "score" | "lamp" | "grade", - key: string | null, - tierlistID?: string + game: Game, + chart: AnyChartDocument, + type: "score" | "lamp" | "grade", + key: string | null, + tierlistID?: string ) { - const tlID = tierlistID ?? (await GetDefaultTierlistID(game, chart.playtype)); + const tlID = tierlistID ?? (await GetDefaultTierlistID(game, chart.playtype)); - const query: Record = { - tierlistID: tlID, - chartID: chart.chartID, - type, - key, - }; + const query: Record = { + tierlistID: tlID, + chartID: chart.chartID, + type, + key, + }; - return db["tierlist-data"].findOne(query); + return db["tierlist-data"].findOne(query); } export async function GetAllTierlistDataOfType( - game: Game, - chart: AnyChartDocument, - type: "score" | "lamp" | "grade", - tierlistID?: string + game: Game, + chart: AnyChartDocument, + type: "score" | "lamp" | "grade", + tierlistID?: string ) { - const tlID = tierlistID ?? (await GetDefaultTierlistID(game, chart.playtype)); + const tlID = tierlistID ?? (await GetDefaultTierlistID(game, chart.playtype)); - const query: Record = { - tierlistID: tlID, - chartID: chart.chartID, - type, - }; + const query: Record = { + tierlistID: tlID, + chartID: chart.chartID, + type, + }; - return db["tierlist-data"].find(query); + return db["tierlist-data"].find(query); } diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index 952f2b68f..d005cf160 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -1,32 +1,32 @@ import { - integer, - ChartDocument, - PublicUserDocument, - UserGameStats, - Game, - Playtypes, + integer, + ChartDocument, + PublicUserDocument, + UserGameStats, + Game, + Playtypes, } from "tachi-common"; declare module "express-session" { - // Inject additional properties on express-session - interface SessionData { - tachi: TachiSessionData; - } + // Inject additional properties on express-session + interface SessionData { + tachi: TachiSessionData; + } } export interface TachiSessionData { - userID: integer; + userID: integer; } export interface TachiAPIFailResponse { - success: false; - description: string; + success: false; + description: string; } export interface TachiAPISuccessResponse { - success: true; - description: string; - body: Record; + success: true; + description: string; + body: Record; } export type TachiAPIReponse = TachiAPIFailResponse | TachiAPISuccessResponse; @@ -40,12 +40,12 @@ export type EmptyObject = Record; * Data that may be monkey-patched onto req.tachi. This holds things such as middleware results. */ export interface TachiRequestData { - uscChartDoc?: ChartDocument<"usc:Single">; + uscChartDoc?: ChartDocument<"usc:Single">; - beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K">; + beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K">; - requestedUser?: PublicUserDocument; - requestedUserGameStats?: UserGameStats; - game?: Game; - playtype?: Playtypes[Game]; + requestedUser?: PublicUserDocument; + requestedUserGameStats?: UserGameStats; + game?: Game; + playtype?: Playtypes[Game]; } diff --git a/server/src/utils/user.test.ts b/server/src/utils/user.test.ts index c34f99d1c..90e9a49a3 100644 --- a/server/src/utils/user.test.ts +++ b/server/src/utils/user.test.ts @@ -7,87 +7,87 @@ import { CloseAllConnections } from "../test-utils/close-connections"; import ResetDBState from "../test-utils/resets"; t.test("#GetUserCaseInsensitive", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return the user for an exact username", async (t) => { - const result = await GetUserCaseInsensitive("test_zkldi"); + t.test("Should return the user for an exact username", async (t) => { + const result = await GetUserCaseInsensitive("test_zkldi"); - t.not(result, null, "Should not return null"); + t.not(result, null, "Should not return null"); - t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); + t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); - prAssert(result, PRUDENCE_PUBLIC_USER, "Should return a conforming PublicUserDocument"); + prAssert(result, PRUDENCE_PUBLIC_USER, "Should return a conforming PublicUserDocument"); - // @ts-expect-error yeah - t.equal(result.password, undefined, "Should not return password"); - // @ts-expect-error yeah - t.equal(result.email, undefined, "Should not return email"); - }); + // @ts-expect-error yeah + t.equal(result.password, undefined, "Should not return password"); + // @ts-expect-error yeah + t.equal(result.email, undefined, "Should not return email"); + }); - t.test("Should return the user for an incorrectly cased username", async (t) => { - const result = await GetUserCaseInsensitive("tesT_ZkLdi"); + t.test("Should return the user for an incorrectly cased username", async (t) => { + const result = await GetUserCaseInsensitive("tesT_ZkLdi"); - t.not(result, null, "Should not return null"); + t.not(result, null, "Should not return null"); - t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); + t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); - prAssert(result, PRUDENCE_PUBLIC_USER, "Should return a conforming PublicUserDocument"); + prAssert(result, PRUDENCE_PUBLIC_USER, "Should return a conforming PublicUserDocument"); - // @ts-expect-error yeah - t.equal(result.password, undefined, "Should not return password"); - // @ts-expect-error yeah - t.equal(result.email, undefined, "Should not return email"); - }); + // @ts-expect-error yeah + t.equal(result.password, undefined, "Should not return password"); + // @ts-expect-error yeah + t.equal(result.email, undefined, "Should not return email"); + }); - t.test("Should not return the user for a username that does not exist", async (t) => { - const result = await GetUserCaseInsensitive("foobar"); + t.test("Should not return the user for a username that does not exist", async (t) => { + const result = await GetUserCaseInsensitive("foobar"); - t.equal(result, null, "Should return null"); - }); + t.equal(result, null, "Should return null"); + }); - t.end(); + t.end(); }); t.test("#PRIVATEINFO_GetUserCaseInsensitive", (t) => { - t.beforeEach(ResetDBState); + t.beforeEach(ResetDBState); - t.test("Should return the user for an exact username", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("test_zkldi"); + t.test("Should return the user for an exact username", async (t) => { + const result = await PRIVATEINFO_GetUserCaseInsensitive("test_zkldi"); - t.not(result, null, "Should not return null"); + t.not(result, null, "Should not return null"); - t.equal(result!.username, "test_zkldi", "Should return test_rzkldi"); + t.equal(result!.username, "test_zkldi", "Should return test_rzkldi"); - prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); - }); + prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); + }); - t.test("Should return the user for an incorrectly cased username", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("tesT_ZkLdi"); + t.test("Should return the user for an incorrectly cased username", async (t) => { + const result = await PRIVATEINFO_GetUserCaseInsensitive("tesT_ZkLdi"); - t.not(result, null, "Should not return null"); + t.not(result, null, "Should not return null"); - t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); + t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); - prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); - }); + prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); + }); - t.test("Should not return the user for a username that does not exist", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("foobar"); + t.test("Should not return the user for a username that does not exist", async (t) => { + const result = await PRIVATEINFO_GetUserCaseInsensitive("foobar"); - t.equal(result, null, "Should return null"); - }); + t.equal(result, null, "Should return null"); + }); - t.end(); + t.end(); }); t.test("#FormatUserDoc", (t) => { - t.equal( - FormatUserDoc({ username: "zkldi", id: 123 } as PublicUserDocument), - "zkldi (#123)", - "Should format a user document into username #id format." - ); + t.equal( + FormatUserDoc({ username: "zkldi", id: 123 } as PublicUserDocument), + "zkldi (#123)", + "Should format a user document into username #id format." + ); - t.end(); + t.end(); }); t.teardown(CloseAllConnections); diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index e93544fd3..49ab1e03c 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -3,40 +3,40 @@ import { FindOneResult } from "monk"; import db from "../external/mongo/db"; import CreateLogCtx from "../lib/logger/logger"; import { - defaultRatingAlgorithm, - defaultScoreRatingAlgorithm, - defaultSessionRatingAlgorithm, + defaultRatingAlgorithm, + defaultScoreRatingAlgorithm, + defaultSessionRatingAlgorithm, } from "tachi-common/js/config"; const logger = CreateLogCtx(__filename); const OMIT_PRIVATE_USER_RETURNS = { - password: 0, - email: 0, - // legacy protection - this field does not exist on user documents anymore, but it did, and it *did* hold personal information. - integrations: 0, + password: 0, + email: 0, + // legacy protection - this field does not exist on user documents anymore, but it did, and it *did* hold personal information. + integrations: 0, }; /** * Returns a user's username from their ID. Throws if no user with that ID exists. */ export async function GetUsernameFromUserID(userID: integer): Promise { - const partialDoc = await db.users.findOne( - { - id: userID, - }, - { - projection: { - username: 1, - }, - } - ); + const partialDoc = await db.users.findOne( + { + id: userID, + }, + { + projection: { + username: 1, + }, + } + ); - if (!partialDoc) { - throw new Error(`Could not find username for userID ${userID}.`); - } + if (!partialDoc) { + throw new Error(`Could not find username for userID ${userID}.`); + } - return partialDoc.username; + return partialDoc.username; } /** @@ -45,16 +45,16 @@ export async function GetUsernameFromUserID(userID: integer): Promise { * @returns PublicUserDocument | null */ export function GetUserCaseInsensitive( - username: string + username: string ): Promise> { - return db.users.findOne( - { - usernameLowercase: username.toLowerCase(), - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ) as Promise>; + return db.users.findOne( + { + usernameLowercase: username.toLowerCase(), + }, + { + projection: OMIT_PRIVATE_USER_RETURNS, + } + ) as Promise>; } /** @@ -64,9 +64,9 @@ export function GetUserCaseInsensitive( * @returns PrivateUserDocument | null */ export function PRIVATEINFO_GetUserCaseInsensitive(username: string) { - return db.users.findOne({ - usernameLowercase: username.toLowerCase(), - }); + return db.users.findOne({ + usernameLowercase: username.toLowerCase(), + }); } /** @@ -75,14 +75,14 @@ export function PRIVATEINFO_GetUserCaseInsensitive(username: string) { * @returns PublicUserDocument | null */ export function GetUserWithID(userID: integer): Promise> { - return db.users.findOne( - { - id: userID, - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ) as Promise>; + return db.users.findOne( + { + id: userID, + }, + { + projection: OMIT_PRIVATE_USER_RETURNS, + } + ) as Promise>; } /** @@ -94,18 +94,18 @@ export function GetUserWithID(userID: integer): Promise { - const userDoc = await GetUserWithID(userID); + const userDoc = await GetUserWithID(userID); - if (!userDoc) { - logger.severe( - `User ${userID} does not have an associated user document, but one was expected.` - ); - throw new Error( - `User ${userID} does not have an associated user document, but one was expected.` - ); - } + if (!userDoc) { + logger.severe( + `User ${userID} does not have an associated user document, but one was expected.` + ); + throw new Error( + `User ${userID} does not have an associated user document, but one was expected.` + ); + } - return userDoc; + return userDoc; } /** @@ -115,9 +115,9 @@ export async function GetUserWithIDGuaranteed(userID: integer): Promise