From ce3af7c682a75e4a650b6dd74ea689c0bf330f48 Mon Sep 17 00:00:00 2001 From: Yi-Ting Shih Date: Thu, 16 Oct 2025 05:07:56 +0800 Subject: [PATCH] Feat: works on my machine --- README.md | 11 + auth.go | 18 + cmd.go | 30 + dict | 2331 +++++++++++++++++++++++++++++++++ go.mod | 68 + go.sum | 173 +++ go.work | 3 + go.work.sum | 22 + handlers/auth/handlers.go | 11 + handlers/auth/postLogin.go | 59 + handlers/auth/postLogout.go | 39 + handlers/auth/postRegister.go | 66 + handlers/wordle/getState.go | 55 + handlers/wordle/handlers.go | 98 ++ handlers/wordle/postGuess.go | 54 + logs/wordle-stderr.log | 0 logs/wordle-stdout.log | 0 middlewares/auth.go | 60 + middlewares/errorHandler.go | 63 + middlewares/handlers.go | 11 + models/user.go | 13 + player.go | 37 + stages/base.go | 23 + stages/landing.go | 241 ++++ stages/lobby.go | 283 ++++ stages/udp.go | 1 + stages/wordle.go | 179 +++ test.go | 38 + types/auth.go | 7 + types/types.go | 5 + types/udp.go | 10 + types/wordle.go | 209 +++ utils/initDB.go | 13 + utils/success.go | 14 + utils/udp.go | 143 ++ workflows/authServer.go | 63 + workflows/wordleServer.go | 86 ++ 37 files changed, 4537 insertions(+) create mode 100644 README.md create mode 100644 auth.go create mode 100644 cmd.go create mode 100644 dict create mode 100644 go.mod create mode 100644 go.sum create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 handlers/auth/handlers.go create mode 100644 handlers/auth/postLogin.go create mode 100644 handlers/auth/postLogout.go create mode 100644 handlers/auth/postRegister.go create mode 100644 handlers/wordle/getState.go create mode 100644 handlers/wordle/handlers.go create mode 100644 handlers/wordle/postGuess.go create mode 100644 logs/wordle-stderr.log create mode 100644 logs/wordle-stdout.log create mode 100644 middlewares/auth.go create mode 100644 middlewares/errorHandler.go create mode 100644 middlewares/handlers.go create mode 100644 models/user.go create mode 100644 player.go create mode 100644 stages/base.go create mode 100644 stages/landing.go create mode 100644 stages/lobby.go create mode 100644 stages/udp.go create mode 100644 stages/wordle.go create mode 100644 test.go create mode 100644 types/auth.go create mode 100644 types/types.go create mode 100644 types/udp.go create mode 100644 types/wordle.go create mode 100644 utils/initDB.go create mode 100644 utils/success.go create mode 100644 utils/udp.go create mode 100644 workflows/authServer.go create mode 100644 workflows/wordleServer.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2a6a28 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# NMSL + +## TODO + +- Lobby + - Login count + - Logout and duplicate login +- Connection + - Scan port +- Game + - Game end reason diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..6f2f839 --- /dev/null +++ b/auth.go @@ -0,0 +1,18 @@ +package main + +import ( + "gitea.konchin.com/ytshih/inp2025/workflows" + "github.com/spf13/cobra" +) + +var authCmd = &cobra.Command{ + Use: "auth", + Run: func(cmd *cobra.Command, args []string) { + workflows.AuthServer() + }, +} + +func init() { + authCmd.Flags(). + String("listen-addr", "localhost:8888", "") +} diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..c2a8aaa --- /dev/null +++ b/cmd.go @@ -0,0 +1,30 @@ +package main + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var RootCmd = &cobra.Command{ + Use: "game", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) + viper.BindPFlags(cmd.PersistentFlags()) + viper.BindPFlags(cmd.Flags()) + }, +} + +func main() { + RootCmd.Execute() +} + +func init() { + cobra.EnableTraverseRunHooks = true + + RootCmd.AddCommand(authCmd) + RootCmd.AddCommand(playerCmd) + RootCmd.AddCommand(testCmd) +} diff --git a/dict b/dict new file mode 100644 index 0000000..b4d88d6 --- /dev/null +++ b/dict @@ -0,0 +1,2331 @@ +ABOUT +OTHER +WHICH +THEIR +THERE +FIRST +WOULD +THESE +CLICK +PRICE +STATE +EMAIL +WORLD +MUSIC +AFTER +VIDEO +WHERE +ORDER +GROUP +UNDER +COULD +GREAT +HOTEL +STORE +RIGHT +LOCAL +THOSE +USING +PHONE +FORUM +BLACK +CHECK +INDEX +BEING +WOMEN +TODAY +SOUTH +FOUND +HOUSE +PHOTO +POWER +WHILE +THREE +TOTAL +PLACE +THINK +NORTH +MEDIA +WATER +SINCE +GUIDE +BOARD +WHITE +SMALL +LEVEL +IMAGE +TITLE +SHALL +CLASS +STILL +MONEY +EVERY +VISIT +REPLY +VALUE +PRESS +LEARN +PRINT +STOCK +POINT +LARGE +TABLE +START +MODEL +HUMAN +MOVIE +MARCH +GOING +STUDY +STAFF +AGAIN +NEVER +TOPIC +BELOW +PARTY +LOGIN +LEGAL +ABOVE +QUOTE +STORY +YOUNG +FIELD +PAPER +NIGHT +POKER +ISSUE +RANGE +COURT +AUDIO +LIGHT +WRITE +OFFER +GIVEN +EVENT +CHINA +MIGHT +MONTH +MAJOR +SPACE +CHILD +ENTER +SHARE +RADIO +UNTIL +COLOR +TRACK +LEAST +TRADE +GREEN +CLOSE +DRIVE +SHORT +DAILY +BEACH +STYLE +FRONT +EARLY +SOUND +FINAL +ADULT +THING +CHEAP +THIRD +COVER +OFTEN +WATCH +HEART +ERROR +CLEAR +TAKEN +KNOWN +QUICK +WHOLE +LATER +BASIC +ALONG +AMONG +DEATH +SPEED +BRAND +STUFF +DOING +ENTRY +FORCE +RIVER +ALBUM +BUILD +APPLY +CROSS +LOWER +UNION +LEAVE +WOMAN +CABLE +SCORE +SHOWN +FLASH +ALLOW +SUPER +CAUSE +FOCUS +VOICE +BROWN +GLASS +HAPPY +SMITH +THANK +PRIOR +SPORT +READY +ROUND +BUILT +BLOOD +EARTH +BASIS +AWARD +EXTRA +QUITE +HORSE +OWNER +BRING +INPUT +AGENT +VALID +GRAND +TRIAL +WROTE +METAL +GUEST +TRUST +GRADE +PANEL +FLOOR +MATCH +PLANT +SENSE +STAGE +MAYBE +YOUTH +BREAK +DANCE +APPLE +ENJOY +BLOCK +CIVIL +STEEL +WRONG +FULLY +WORTH +PEACE +COAST +GRANT +AGREE +SCALE +STAND +FRAME +CHIEF +HEARD +BEGIN +ROYAL +CLEAN +BIBLE +SUITE +PIECE +SHEET +SEVEN +OLDER +WHOSE +STONE +BUYER +LABEL +CANON +WASTE +CHAIR +PHASE +MOTOR +SHIRT +CRIME +COUNT +CLAIM +PATCH +ALONE +SAINT +JOINT +FRESH +UPPER +PRIME +LIMIT +BEGAN +CREEK +URBAN +LABOR +ADMIN +HEAVY +SOLID +THEME +TOUCH +SERVE +MAGIC +MOUNT +SMART +AVOID +BIRTH +VIRUS +ABUSE +FAITH +CHAIN +REACH +SORRY +GAMMA +TRUTH +DRAFT +CHART +EQUAL +FUNNY +TRIED +LASER +HARRY +MOUSE +BRAIN +DREAM +FALSE +CARRY +HELLO +BRIEF +EIGHT +ALERT +QUEEN +SWEET +TRUCK +OCEAN +DEPTH +TRAIN +ROUTE +FRANK +ANIME +SPEAK +QUERY +RURAL +JUDGE +FIGHT +MINOR +SPENT +CYCLE +SLEEP +SCENE +DRINK +GUESS +AHEAD +DELTA +ALPHA +BONUS +ADOBE +DRESS +REFER +LAYER +SPEND +CLOCK +RATIO +PROOF +EMPTY +IDEAL +CREAM +AWARE +SHAPE +USAGE +EXIST +WHEEL +ANGEL +WIDTH +NOISE +ARRAY +SHARP +OCCUR +COACH +PLATE +LOGIC +PLAIN +TRAIL +BUDDY +SETUP +SCOPE +CRAZY +MOUTH +METER +FRUIT +SUGAR +STICK +GENRE +SLIDE +EXACT +BOUND +STORM +MICRO +PAINT +DELAY +PILOT +NOVEL +ULTRA +TRULY +LODGE +BROAD +GUARD +NEWLY +RAISE +DRAMA +LUNCH +AUDIT +TOWER +SHELL +SOLAR +CATCH +DOUBT +FORTH +SPLIT +TWICE +SHIFT +TREAT +PIANO +TEACH +RAPID +HAIRY +DUTCH +PULSE +METRO +STRIP +PEARL +OPERA +ASSET +BLANK +HUMOR +TIGHT +MEANT +PLANE +GRACE +VILLA +INNER +TASTE +CACHE +LEASE +PROUD +GIANT +ALARM +USUAL +ANGLE +VINYL +WORST +HONOR +EAGLE +NURSE +QUIET +COMIC +CROWN +MAKER +CRACK +SMOKE +CRAFT +APART +BLIND +GROSS +ACTOR +FIFTH +PRIZE +DIRTY +ALIVE +PROVE +RIDGE +MODEM +SKILL +THROW +TREND +WORSE +FIBER +GRAPH +FRAUD +ROGER +CRASH +INTER +GROVE +SPRAY +MAYOR +YIELD +HENCE +RADAR +DIARY +BAKER +SHOCK +EBONY +DRAWN +BEAST +DODGE +PIZZA +GLOBE +GHOST +PRIDE +BRASS +PLAZA +QUEST +BOOTY +VENUE +VITAL +EXCEL +ENEMY +LUCKY +THICK +VISTA +FLOOD +ARENA +GROWN +SMILE +CANDY +TIGER +BOOST +MORAL +POUND +BREAD +TOUGH +CHEST +BILLY +SOLVE +SIGHT +WORRY +GLORY +FAULT +RUGBY +FLUID +DEVIL +GRASS +MANGA +THEFT +SWING +SHOOT +ELITE +ROBOT +GNOME +NOBLE +SHORE +LOOSE +HORNY +RALPH +LIVER +DECOR +AGING +INTRO +CLERK +FAVOR +SIGMA +ASIDE +ESSAY +TRACE +SPOKE +ARROW +ROUGH +WEIRD +BLADE +ROBIN +STRAP +CROWD +CLOUD +VALVE +KNIFE +SHELF +ADOPT +OUTER +STEAM +ACUTE +STOOD +CAROL +STACK +CURVE +AMBER +TRUNK +CAMEL +JUICE +CHASE +SAUCE +FEWER +PROXY +SLAVE +HAVEN +CHARM +BASIN +RANCH +DRUNK +ALIEN +BROKE +NYLON +ROCKY +FLEET +BUNCH +OMEGA +CIVIC +GRILL +GRAIN +SALON +TURBO +RESET +BRUSH +SPARE +SKIRT +HONEY +GAUGE +SIXTH +CHEAT +SANDY +MACRO +LAUGH +PITCH +DOZEN +TEETH +CLOTH +STAMP +CARGO +MAPLE +DEPOT +BLEND +PROBE +DEBUG +CHUCK +BINGO +SUNNY +CEDAR +MASON +CHOSE +BLAST +BRAKE +OLIVE +CYBER +CLONE +RELAY +ANGRY +LOVER +DADDY +FERRY +MOTEL +RALLY +DYING +STUCK +VOCAL +ORGAN +LEMON +TOXIC +BENCH +RIDER +BOBBY +SHEEP +SALAD +PASTE +RELAX +SWORD +CORAL +PIXEL +FLOAT +DAIRY +ADMIT +FANCY +SQUAD +CHAOS +WHEAT +UNITY +BRIDE +BEGUN +FEVER +ROVER +FLAME +SPELL +ANNEX +ARGUE +ARISE +CHESS +CANAL +LYING +DRILL +HOBBY +TRICK +WIDER +SCREW +BLAME +FIFTY +UNCLE +RANDY +BRICK +NAVAL +CABIN +RETRO +ANGER +HANDY +GUILD +TRIBE +BATCH +ALTER +AMEND +CHICK +THONG +MEDAL +BOOTH +INDIE +BREED +POLAR +PATIO +SNAKE +BERRY +OUGHT +TIMER +VERSE +NASTY +TUMOR +FORTY +QUEUE +WELSH +BELLY +ELDER +SONIC +THUMB +TWIST +DEBUT +PENNY +IVORY +NEWER +SPICE +DONOR +TRASH +MANOR +DISCO +MINUS +SHADE +DIGIT +LYRIC +GRAVE +LOBBY +PUNCH +KARMA +SHAKE +HOLLY +SILLY +MERCY +FENCE +SHAME +FATAL +FLESH +SHEER +WITCH +PUPPY +SMELL +SATIN +NERVE +RENEW +REBEL +SLOPE +REHAB +MERIT +CONDO +FAIRY +SHAFT +KITTY +DRAIN +PANIC +ONION +MERRY +SCUBA +DRIED +DERBY +STEAL +ALIKE +SCOUT +DEALT +BADGE +WRIST +HEATH +REALM +ROUGE +YEAST +BROOK +ARMOR +VIRAL +LADEN +MERGE +SPERM +FROST +SALLY +YACHT +WHALE +SHARK +CLIFF +TRACT +SHINE +OZONE +PASTA +SERUM +SWIFT +INBOX +FOCAL +WOUND +BELLE +CUBIC +ELECT +BUNNY +FLYER +CLIMB +TOKEN +FLUSH +JEWEL +TEDDY +DRYER +FUNKY +SCARY +TOOTH +DROVE +UPSET +LANCE +COLON +PURSE +ALIGN +BLESS +CREST +ALLOY +BLOOM +SURGE +SPANK +VAULT +ORBIT +BACON +SPINE +TROUT +FATTY +OXIDE +BADLY +SCOOP +BLINK +FUZZY +FORGE +DENSE +BRAVE +AWFUL +WAGON +KNOCK +QUILT +MAMBO +FLOUR +CHOIR +BLOND +BURST +FIBRE +DAISY +CRUDE +SAFER +MARSH +THETA +STAKE +ARBOR +RIFLE +WAIST +SEWER +RESIN +LINEN +DECAY +USHER +SKATE +LYNCH +VOTER +URINE +TOWEL +CRANE +HABIT +COUPE +SIXTY +SPARK +SPIKE +TONGA +SEDAN +FLORA +HARDY +DENIM +GLOVE +PLUSH +ADAPT +STERN +TUTOR +IDIOT +DEBIT +RAVEN +SLICE +PAUSE +DEMON +COUCH +ROGUE +OPTIC +CHILI +GRIEF +SWEAT +QUAKE +ALLEY +LOYAL +RENAL +SPITE +IMPLY +CHILL +LINER +VIVID +SKULL +NINJA +STEAK +COBRA +THREW +NINTH +MARRY +DRAKE +FRIED +WOODY +CRIED +RIVAL +HOMER +WOVEN +RIGID +SALSA +BLOWN +BATON +ABBEY +SAUNA +CRUEL +EAGER +PUPIL +FEAST +ANKLE +BLUNT +REACT +FLUTE +HARSH +CEASE +EQUIP +HEDGE +CURRY +POUCH +SPOON +NICHE +CIGAR +CURSE +TITAN +SHOUT +STRAW +REUSE +PEACH +UNCUT +STOVE +FREAK +BLUFF +SADLY +AVAIL +HATCH +STEIN +SPILL +DRIFT +CRISP +ONSET +ASSAY +SNACK +MAXIM +SLATE +PAGAN +WIDOW +CANOE +JUICY +MOODY +PEDAL +SCRAP +TERRA +VAPOR +ALOUD +GOOSE +HYDRO +NOISY +ABIDE +BLISS +PARSE +JELLY +MANIA +CHEER +CLAMP +GRAPE +RACER +GUILT +SWEEP +LUNAR +BOXER +WEIGH +RODEO +MOOSE +CRUSH +LEVER +TASTY +TAROT +COCOA +HURRY +CLASH +STAIN +REIGN +BARON +STIFF +RABBI +SUSHI +PUFFY +ELBOW +STARK +CIRCA +RAZOR +COUGH +INLET +GLOSS +PORCH +EATEN +STEEP +CREED +CARAT +PLUMP +MIDST +BORNE +TEMPO +TORCH +ATTIC +PIPER +TENTH +CUTIE +NOTCH +SCENT +GRASP +OUNCE +TOAST +KINKY +QUOTA +JUMBO +FLINT +DUMMY +AWAKE +BURNT +ROAST +PETTY +SHINY +SMASH +AMPLE +SCARF +SPICY +BEARD +WEDGE +HYPER +GAMER +SAVVY +FETAL +CHORD +COMET +SYRUP +ERASE +PROSE +SWEAR +CLOWN +TABOO +DWARF +DOUGH +STOOL +WELCH +HORDE +MOMMY +NANNY +ROACH +NATAL +LOCUS +PRONE +SCARE +THIEF +MOTIF +SPEAR +BIRCH +SLASH +HELIX +SHOOK +MATTE +ZEBRA +FETCH +UNITE +SHEAR +TRUMP +AVIAN +CHAMP +RECAP +CRAWL +HAZEL +RUPEE +STOLE +QUASI +EXILE +KAPPA +SNOOP +VAGUE +RUSTY +STING +BRAVO +BASIL +SHACK +SLEEK +HITCH +TANGO +QUEER +COMMA +FREED +CHEEK +BOWEL +MAFIA +SHIRE +LIPID +PRISM +VEGAN +GROOM +KAYAK +ALTAR +RISEN +RHINO +RULER +SWEPT +TROOP +AROSE +FLOCK +SHAVE +SWAMP +FAINT +GLAND +STOKE +NASAL +LOSER +JOLLY +FEMME +SIEGE +BUTTE +CHALK +WRATH +GRIND +BLITZ +RAINY +VIOLA +RUMOR +DIVER +BLAZE +WRECK +RISKY +TULIP +OWING +DITCH +SLICK +CHUNK +SLEPT +TENOR +SCRUB +CELLO +TOPAZ +DUSTY +PATTY +CRATE +SWORN +BEECH +TENSE +DECAL +FRITZ +MOVER +FAUNA +DETOX +QUARK +SNEAK +OCTET +WILLY +TIDAL +CRUST +DOLLY +MINER +DINER +MOUND +SCION +REGAL +CURLY +HOUND +WHARF +FLICK +DATUM +MAIZE +PSALM +SWELL +IRONY +VIPER +GYPSY +FLARE +WIGHT +CRANK +BRACE +MANGO +THIGH +WINDY +STEER +VOGUE +VODKA +MOIST +STALL +SERIF +UTTER +CATER +PINCH +TROLL +FILTH +ALGAE +SHADY +ERECT +VALET +MADAM +TEASE +AROMA +DWELL +STAIR +ROTOR +QUART +BISON +FUNGI +GREED +BLEED +INCUR +FUDGE +WEAVE +BUGGY +SLACK +GORGE +BANJO +STOUT +STARE +MISSY +FLAIR +AISLE +SEIZE +SPAWN +EPOXY +STONY +CRYPT +TYING +DIODE +MOTTO +DETER +FURRY +RINSE +VENOM +MUMMY +ETHER +HAUTE +WACKY +MUDDY +SHALT +VISOR +NAIVE +FOLIO +FIERY +ACORN +BASAL +SMOKY +FLIRT +SLANG +FINCH +TALLY +CREEP +AGILE +KIOSK +IONIC +STRAY +POPPY +FORTE +WAIVE +GREET +LYMPH +LATCH +DRANK +TORSO +HINGE +STUNT +WITTY +FLOWN +SILKY +REPAY +AWAIT +FETUS +CIDER +LILAC +PIVOT +GLIDE +CREME +WALTZ +BLUSH +MODAL +CADET +TWEAK +TRAIT +EATER +BEZEL +HAVOC +SLING +AXIAL +EPOCH +PLAID +FABLE +OBESE +SOBER +TREAD +PADDY +OTTER +SASSY +DREAD +NEEDY +WEARY +TWEED +SNOWY +GENIE +APRON +HUSKY +BLAND +ADEPT +ESTER +SNAIL +MOWER +SWINE +HERON +GRAFT +ENVOY +ABORT +DUVET +SPADE +GLARE +WAFER +STASH +SEMEN +HOVER +AGONY +LUPUS +BULLY +RHYME +SNORT +TRIAD +CAMEO +LEACH +FANNY +MILKY +NAVEL +SPOOL +ANNOY +TOXIN +AXIOM +JOKER +BRINK +TRUSS +KHAKI +PENAL +LAPSE +SHRUB +FINER +SMACK +CLOAK +MANIC +CHOKE +GRAVY +PAYER +GLAZE +DIZZY +VERGE +NOMAD +THORN +SPOIL +SISSY +PALSY +BAYOU +TONIC +DITTO +ODDLY +UNDUE +CHANT +HUTCH +PARRY +MAMMA +FOLLY +MURAL +WAGER +PURGE +POSER +PERKY +STUMP +SCALP +MELON +SIREN +CLASP +SONAR +ULCER +ETHIC +OPIUM +ENEMA +BARGE +SLANT +BROOM +SNARE +SHANK +LEASH +GEESE +MECCA +BROTH +TAPER +REVUE +SMEAR +SLAIN +QUAIL +ICING +STRUT +PLUME +PLANK +ENACT +DEITY +MANLY +PERIL +SWIRL +ABODE +SAVOY +COMFY +POLKA +NICER +BOAST +PERCH +ANGST +GECKO +FACET +VERVE +SPREE +EMBED +BRUTE +BUTCH +DEFER +CRAVE +SALTY +VALOR +GOOFY +MIMIC +VIGIL +ITCHY +BULKY +BOOZE +WIDEN +ADORE +FLUKE +STOMP +GLADE +CASTE +DRUID +SWARM +LEDGE +DROWN +ABYSS +NIECE +FLASK +HIPPO +PLEAD +SHEEN +MEDIC +GRAIL +LAPEL +PECAN +CHIME +REMIT +EXERT +PINTO +LUCID +INFER +DANDY +SYNOD +LOUSY +CADDY +BLEAK +TRAMP +LATHE +BYLAW +CROOK +STALE +PUTTY +PATSY +HASTE +PRONG +BERTH +LINGO +PIGGY +LAGER +FOYER +BROOD +AZURE +SNIFF +POSSE +PIXIE +UNSET +MELEE +LLAMA +VOWEL +FECAL +HUMID +GUILE +LOFTY +MORON +DOGMA +WINCH +UNZIP +FLOSS +SHAWL +PENCE +THYME +OVARY +ABBOT +MAGMA +ARSON +GEEKY +HAUNT +BRAID +LEFTY +VIGOR +DEUCE +RISER +KNOLL +INLAY +CRAZE +TOTEM +CARVE +APNEA +SWISH +RELIC +ETHOS +CLING +NUDGE +SKUNK +ANVIL +STALK +INERT +EJECT +RAYON +MOCHA +SQUAT +PUBIC +TIARA +KOALA +CROCK +RETRY +PRIMO +FLANK +ATOLL +SPOOF +SPOUT +HEFTY +HOIST +AGATE +SWAMI +FERAL +TRUCE +PETAL +PLUCK +PRICK +ASCOT +VIXEN +BULGE +SLUMP +GLOOM +STINK +OVERT +SLIME +SWUNG +LIBEL +SKIER +SNUFF +TALON +STINT +SIEVE +BINGE +JAZZY +LIMBO +SHOVE +FLAKE +WISER +FLUNG +JUROR +DONUT +TENET +CHUTE +MULCH +WHINE +VICAR +PRANK +SUING +BRINE +FLUFF +UNFIT +ROUSE +SPIEL +SCAMP +EVOKE +GULLY +AGORA +MACHO +BAGEL +AMAZE +EASEL +QUILL +VERSO +CLEFT +GROIN +LATTE +MOURN +GIRLY +BLURB +ERODE +BRISK +EERIE +SHALE +ANODE +CREPE +AVERT +GUISE +VOMIT +BONGO +SHAKY +BLOKE +PHONY +JETTY +RERUN +JERKY +MORPH +SPECK +PAPAL +MOGUL +DROIT +FROWN +PRIVY +SEPIA +GOODY +STORK +TUNIC +FARCE +HOWDY +WHACK +DRONE +GODLY +SPIRE +STEAD +SHRUG +COVEY +WINCE +BOSOM +PICKY +LUMEN +PIOUS +TONAL +FOGGY +SHONE +LEAFY +TROVE +FILER +LUSTY +STEED +HASTY +MUNCH +CLOVE +LEECH +GIVER +NYMPH +FRAIL +CHURN +AWOKE +PARKA +PRUNE +CAIRN +NUTTY +SEVER +FLING +DRIER +CRUMB +PINKY +GRATE +FIEND +TAUPE +PIQUE +SWORE +FISHY +FILMY +TIMID +LEAKY +MOLAR +GULCH +BRIBE +AORTA +DELVE +CRIMP +CLOUT +FELLA +GLYPH +PLUMB +UNIFY +BRIAR +JUNTA +GROUT +TAKER +SCORN +WHIRL +TIBIA +PESKY +KNACK +RIVET +GRUNT +RABID +CUMIN +PAYEE +HIPPY +ROOST +EVADE +SHUNT +TAWNY +MUCUS +APTLY +LIEGE +SHEIK +PIETY +FROZE +TACIT +WHISK +UNMET +MAUVE +SPORE +CREPT +BUGLE +RADII +ROOMY +LASSO +BEVEL +DODGY +BRAWL +TUBAL +CHORE +OCTAL +GAUZE +AMUSE +FIXER +IDIOM +TRAWL +GAMUT +FILET +LORRY +SAUTE +REVEL +MADLY +GRIPE +LOWLY +SCANT +FLIER +AMITY +DROOL +ZONAL +FREER +MURKY +CABAL +CYNIC +PESTO +BOUGH +SLOTH +TACKY +CHASM +MASSE +DECOY +GRIME +RIGOR +GUSTO +DWELT +HILLY +PRAWN +ROWDY +TORUS +CHOCK +CURIO +VOILA +SAVOR +TABBY +GOURD +FORAY +FILLY +ALOFT +SLURP +BATHE +GIRTH +BRUNT +EMBER +ISLET +BALSA +CAPER +QUACK +DRAPE +GIDDY +BROIL +CACTI +JIFFY +SAUCY +ASKEW +GROAN +ABATE +MOSSY +HATER +EXPEL +SWOOP +GUMBO +ADORN +LARVA +GROWL +BRASH +CHARD +SULLY +INEPT +THUMP +SALVO +FLECK +ALIBI +CREDO +FEMUR +BUSHY +GOLEM +AFFIX +QUIRK +FLAKY +REPEL +PUREE +SWATH +HAREM +FUSSY +SHOAL +HEADY +HOARD +SPELT +NERDY +KNELT +CINCH +SCRUM +SNIPE +SALVE +FRIAR +PYGMY +PANSY +DINGO +BAGGY +HEIST +WIELD +ADAGE +BUDGE +KNEEL +BATTY +TWINE +AGAPE +BILGE +SCOUR +LEAPT +TAFFY +BONEY +BITTY +FLACK +CRASS +RUMBA +SPUNK +STUNG +SMIRK +CRAMP +FUGUE +PENNE +BLEEP +IGLOO +ALLOT +MEATY +EGRET +PINEY +FELON +SPASM +DUSKY +SLUSH +VOUCH +GIPSY +RUDDY +FJORD +DIRGE +EDICT +SHREW +MIRTH +BOOBY +CONCH +CORNY +NOOSE +RECUR +TAINT +CRIER +BESET +RAMEN +LADLE +SURLY +CLEAT +BEEFY +SPINY +CLUNG +GRAZE +AFOOT +WOKEN +HOTLY +STAVE +CRICK +STOOP +CURVY +TITHE +IDLER +GLEAM +TAMER +TRICE +NADIR +GAVEL +SPLAT +VYING +POOCH +ENSUE +SLIMY +CLUMP +TILDE +PROWL +DIMLY +HYMEN +FORGO +POISE +DUCHY +ETUDE +GAUNT +SUAVE +TULLE +GOOEY +TANGY +ARTSY +WHOOP +CACAO +SOGGY +BERET +GOLLY +CRUMP +MINCE +HUNCH +SPURT +UMBRA +AMPLY +INANE +SPOOK +OLDEN +REEDY +WREAK +BLOAT +WENCH +MIDGE +SLEET +SOAPY +TARDY +SMELT +YEARN +ERUPT +LUMPY +HEAVE +RARER +COVEN +ELEGY +SLOOP +TAUNT +APHID +MYRRH +CHAFF +SHOWY +EMCEE +SPIED +ALOOF +SNORE +QUELL +POSIT +DOWNY +LEMUR +TERSE +UTILE +TRITE +SHARD +DOWEL +SEEDY +QUOTH +DITTY +SKIFF +GUMMY +REBAR +HUNKY +TIPSY +CRESS +FICUS +FROTH +SMOTE +AXION +MUSHY +DOWRY +SNUCK +AWASH +ABACK +SCALY +GLINT +STOIC +AMISS +LIVID +SMITE +CHUMP +HYENA +PLEAT +SWASH +CLANK +BLIMP +LOOPY +MACAW +PLIER +BANAL +GUAVA +WIMPY +SNOUT +BIGOT +WOOLY +IRATE +PUSHY +DINGY +TRUER +SLUNG +BURLY +MANGE +GAUDY +SWOON +KEBAB +GHOUL +CLACK +WHIFF +CAULK +TUBER +PLIED +TWANG +OMBRE +GOUGE +CHIRP +HALVE +ROWER +AUNTY +SNAFU +SNIDE +GRUFF +BUXOM +MUSTY +KNEAD +REBUT +SEGUE +TWIRL +INGOT +GUPPY +GLEAN +PASTY +CRONE +REBUS +FOAMY +LURID +WHINY +MISER +TEPID +SMOCK +SOOTY +COVET +MINTY +WAVER +CONIC +SAPPY +ELUDE +TRIPE +SPILT +SHYLY +SCONE +THROB +EVICT +NEIGH +BALMY +ZESTY +CANNY +KRILL +MOLDY +TRYST +LURCH +QUASH +SNEER +ALLAY +DECRY +GUSTY +LEPER +LUNGE +POLYP +REFIT +SLINK +DROLL +ENDOW +JOIST +FIZZY +OFFAL +STAID +PITHY +SHIRK +KNELL +EXALT +GUANO +BEGAT +RIPEN +BRAWN +WRING +STILT +CLANG +HUMUS +FROCK +WREST +ASHEN +SNARL +LEGGY +AMASS +ILIAC +SPIKY +SUMAC +WORDY +JAUNT +LOUSE +SCOFF +MUSKY +FUROR +UDDER +BIOME +FLUME +BORAX +USURP +SCOLD +CROAK +ABHOR +DOPEY +TWEET +LEERY +ELFIN +JUMPY +TESTY +MINIM +CLINK +BOSSY +BICEP +DICEY +DOWDY +DILLY +BAWDY +BEAUT +JOUST +WEEDY +GROPE +AFOUL +RAJAH +SULKY +DROOP +FLAIL +ARDOR +DROSS +BALER +ATONE +PALER +FRISK +LOATH +EXULT +EXTOL +KNAVE +CROUP +RATTY +SCREE +ANTIC +RECUT +OVINE +UNTIE +ANNUL +LANKY +OUTDO +MAMMY +ENNUI +PURER +BLEAT +SPRIG +OVATE +DALLY +SHORN +AMBLE +SOOTH +AIDER +SCRAM +GRIMY +LOAMY +BIDDY +CRONY +TEARY +SOWER +SATYR +LITHE +CLUCK +KAZOO +GAFFE +LIKEN +NOSEY +TODDY +GAILY +TATTY +GAZER +JUNTO +UNDID +BETEL +UNWED +CLUED +CREAK +ECLAT +ABLED +WRUNG +AUGUR +IDYLL +BEGET +WISPY +CHAFE +NOBLY +FEIGN +MEALY +BOULE +SHUCK +SCOWL +BELCH +AFIRE +GLOAT +SQUIB +DUNCE +DRAWL +WARTY +MUCKY +VAPID +AGLOW +TROPE +CATTY +SKIMP +HARPY +SINGE +PLUNK +CAPUT +RASPY +OVOID +IMBUE +ACRID +MATEY +BELIE +SLYLY +SHIED +OPINE +PLAIT +SCALD +LEANT +STANK +FRILL +DRYLY +CORER +GONAD +BASTE +COWER +DEIGN +UNLIT +WRYLY +PRUDE +BOTCH +SWILL +COPSE +RUDER +PUDGY +DUMPY +BEADY +TAPIR +BLURT +FROND +IMPEL +HUSSY +SANER +PRIED +GRUEL +GONER +BLUER +FLUNK +SPURN +HOVEL +STUNK +FETID +MOULT +ELOPE +SINEW +RIPER +TWIXT +PREEN +SURER +WOOZY +FLOUT +BOOZY +KNEED +TIZZY +WHELP +POUTY +EDIFY +OAKEN +WRACK +PUPAL +BRINY +CAGEY +DULLY +CHIDE +GASSY +BUSED +POESY +GAYER +CABBY +ODDER +HUMPH +GNASH +SHUSH +KEFIR +FOIST +MANGY +DEBAR +COYLY +TEPEE +SLOSH +UVULA +BLARE +REARM +DEMUR +APING +PULPY +SLUNK +DAUNT +NINNY +PARER +RETCH +QUALM +OUTGO +WAXEN +GAWKY +EYING +BEFIT +GOFER +ABASE +GAYLY +THRUM +CAVIL +EKING +SKULK +ELIDE +ICILY +UNFED +SNAKY +VAUNT +ELATE +WOOER diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ca7048 --- /dev/null +++ b/go.mod @@ -0,0 +1,68 @@ +module gitea.konchin.com/ytshih/inp2025 + +go 1.25.2 + +require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/go-resty/resty/v2 v2.16.5 + github.com/gorilla/websocket v1.5.3 + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 + github.com/uptrace/bun v1.2.15 + github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 + github.com/uptrace/bun/driver/sqliteshim v1.2.15 + github.com/uptrace/bunrouter v1.0.23 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.28.0 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1ff855f --- /dev/null +++ b/go.sum @@ -0,0 +1,173 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE= +github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI= +github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA= +github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk= +github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU= +github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..864cc4b --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.25.2 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..c21dce4 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,22 @@ +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/handlers/auth/handlers.go b/handlers/auth/handlers.go new file mode 100644 index 0000000..498b304 --- /dev/null +++ b/handlers/auth/handlers.go @@ -0,0 +1,11 @@ +package auth + +import "github.com/uptrace/bun" + +type Handlers struct { + db *bun.DB +} + +func NewHandlers(db *bun.DB) *Handlers { + return &Handlers{db: db} +} diff --git a/handlers/auth/postLogin.go b/handlers/auth/postLogin.go new file mode 100644 index 0000000..6211200 --- /dev/null +++ b/handlers/auth/postLogin.go @@ -0,0 +1,59 @@ +package auth + +import ( + "net/http" + + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/models" + "gitea.konchin.com/ytshih/inp2025/types" + "gitea.konchin.com/ytshih/inp2025/utils" + + "github.com/uptrace/bunrouter" +) + +func (self *Handlers) PostLogin( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + user, ok := ctx.Value(types.UserKey).(models.User) + if !ok { + return middlewares.HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "user not login", + } + } + + res, err := self.db.NewUpdate(). + Model((*models.User)(nil)). + Set("login_count = login_count + ?", 1). + Set("is_logged = ?", true). + Where("is_logged = ?", false). + Where("username = ?", user.Username). + Exec(ctx) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to update login count", + OriginError: err, + } + } + if cnt, err := res.RowsAffected(); err != nil || cnt == 0 { + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to get affected row count", + OriginError: err, + } + } + // debug + return utils.Success(w) + if cnt == 0 { + return middlewares.HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "already logged in", + } + } + } + return utils.Success(w) +} diff --git a/handlers/auth/postLogout.go b/handlers/auth/postLogout.go new file mode 100644 index 0000000..1137c00 --- /dev/null +++ b/handlers/auth/postLogout.go @@ -0,0 +1,39 @@ +package auth + +import ( + "net/http" + + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/models" + "gitea.konchin.com/ytshih/inp2025/types" + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/uptrace/bunrouter" +) + +func (self *Handlers) PostLogout( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + user, ok := ctx.Value(types.UserKey).(models.User) + if !ok { + return middlewares.HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "user not login", + } + } + + _, err := self.db.NewUpdate(). + Set("is_logged = ?", false). + Where("username = ?", user.Username). + Exec(ctx) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to update logged in status", + OriginError: err, + } + } + + return utils.Success(w) +} diff --git a/handlers/auth/postRegister.go b/handlers/auth/postRegister.go new file mode 100644 index 0000000..898b3ae --- /dev/null +++ b/handlers/auth/postRegister.go @@ -0,0 +1,66 @@ +package auth + +import ( + "encoding/json" + "io" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/models" + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/uptrace/bunrouter" +) + +func (self *Handlers) PostRegister( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + + b, err := io.ReadAll(req.Body) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "failed to read body payload", + OriginError: err, + } + } + + var user models.User + if err := json.Unmarshal(b, &user); err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "failed to unmarshal json into user", + OriginError: err, + } + } + + res, err := self.db.NewInsert(). + Model(&user). + On("CONFLICT (username) DO NOTHING"). + Exec(ctx) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to insert user", + OriginError: err, + } + } + if cnt, err := res.RowsAffected(); err != nil || cnt == 0 { + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to get affected row count", + OriginError: err, + } + } + if cnt == 0 { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "username already exist", + } + } + } + + return utils.Success(w) +} diff --git a/handlers/wordle/getState.go b/handlers/wordle/getState.go new file mode 100644 index 0000000..1ea5b75 --- /dev/null +++ b/handlers/wordle/getState.go @@ -0,0 +1,55 @@ +package wordle + +import ( + "net/http" + + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/types" + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/gorilla/websocket" + "github.com/uptrace/bunrouter" + "github.com/vmihailenco/msgpack/v5" +) + +func (self *Handlers) GetState( + w http.ResponseWriter, + req bunrouter.Request, +) error { + // fmt.Fprintf(os.Stderr, "GET /api/state\n") + c, err := self.upgrader.Upgrade(w, req.Request, nil) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to upgrade websocket", + OriginError: err, + } + } + defer c.Close() + + username, _, ok := req.BasicAuth() + if !ok { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "username not exist", + } + } + dataCh := make(chan types.WordleState) + self.opCh <- &OperationSubs{ + Username: username, + SubsCh: &dataCh, + } + + for data := range dataCh { + b, err := msgpack.Marshal(data) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to marshal data into msgpack", + OriginError: err, + } + } + c.WriteMessage(websocket.BinaryMessage, b) + } + + return utils.Success(w) +} diff --git a/handlers/wordle/handlers.go b/handlers/wordle/handlers.go new file mode 100644 index 0000000..8c1952a --- /dev/null +++ b/handlers/wordle/handlers.go @@ -0,0 +1,98 @@ +package wordle + +import ( + "fmt" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/types" + "github.com/gorilla/websocket" +) + +type Operation interface { + Run(self *Handlers) error +} + +type OperationSubs struct { + Username string + SubsCh *chan types.WordleState +} + +func (op *OperationSubs) Run(self *Handlers) error { + _, ok := self.state.History[op.Username] + if !ok { + self.state.History[op.Username] = [types.GUESS_COUNT_LIMIT]types.Guess{} + } + // fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History) + self.subs = append(self.subs, op.SubsCh) + + return nil +} + +type OperationGuess struct { + Username string `msgpack:"username"` + Guess string `msgpack:"guess"` +} + +func (op *OperationGuess) Run(self *Handlers) error { + self.state.CurrentGuess[op.Username] = op.Guess + + if len(self.state.CurrentGuess) < len(self.state.History) { + return nil + } + + for user, guess := range self.state.CurrentGuess { + if guess == self.ans { + self.state.GameEnd = true + } + guesses := self.state.History[user] + guesses[self.state.Round] = types.NewGuess(guess, self.ans) + self.state.History[user] = guesses + } + self.state.CurrentGuess = make(map[types.UsernameType]string) + self.state.Round++ + if self.state.Round >= types.GUESS_COUNT_LIMIT { + self.state.GameEnd = true + } + + return nil +} + +type Handlers struct { + ans string + state types.WordleState + upgrader websocket.Upgrader + subs []*chan types.WordleState + + opCh chan Operation +} + +func NewHandlers(ans string) *Handlers { + ret := &Handlers{ + ans: ans, + state: types.NewWordleState(), + upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, + }, + + opCh: make(chan Operation), + } + go ret.GameFlow() + return ret +} + +func (self *Handlers) GameFlow() { + for op := range self.opCh { + if err := op.Run(self); err != nil { + panic(fmt.Errorf("game flow operation failed, %w", err)) + } + + // fmt.Fprintf(os.Stderr, "[DEBUG] %+v\n", self.state.History) + for _, ch := range self.subs { + *ch <- self.state + } + } +} diff --git a/handlers/wordle/postGuess.go b/handlers/wordle/postGuess.go new file mode 100644 index 0000000..dee397b --- /dev/null +++ b/handlers/wordle/postGuess.go @@ -0,0 +1,54 @@ +package wordle + +import ( + "io" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/uptrace/bunrouter" + "github.com/vmihailenco/msgpack/v5" +) + +type PostGuessInput struct { + Guess string `msgpack:"guess"` +} + +func (self *Handlers) PostGuess( + w http.ResponseWriter, + req bunrouter.Request, +) error { + // fmt.Fprintf(os.Stderr, "POST /api/guess\n") + b, err := io.ReadAll(req.Body) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "failed to read body payload", + OriginError: err, + } + } + + var input PostGuessInput + if err := msgpack.Unmarshal(b, &input); err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "failed to unmarshal from msgpack", + OriginError: err, + } + } + + username, _, ok := req.BasicAuth() + if !ok { + return middlewares.HTTPError{ + StatusCode: http.StatusBadRequest, + Message: "username not exist", + } + } + + self.opCh <- &OperationGuess{ + Username: username, + Guess: input.Guess, + } + + return utils.Success(w) +} diff --git a/logs/wordle-stderr.log b/logs/wordle-stderr.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/wordle-stdout.log b/logs/wordle-stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/middlewares/auth.go b/middlewares/auth.go new file mode 100644 index 0000000..3714c64 --- /dev/null +++ b/middlewares/auth.go @@ -0,0 +1,60 @@ +package middlewares + +import ( + "context" + "database/sql" + "errors" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/models" + "gitea.konchin.com/ytshih/inp2025/types" + "github.com/uptrace/bunrouter" +) + +func (self *Handlers) Auth( + next bunrouter.HandlerFunc, +) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + ctx := req.Context() + + username, password, ok := req.BasicAuth() + if !ok { + return HTTPError{ + StatusCode: http.StatusNotFound, + Message: "username not exist", + } + } + + dbUser := models.User{Username: username} + err := self.db.NewSelect(). + Model(&dbUser). + WherePK(). + Scan(ctx) + if errors.Is(err, sql.ErrNoRows) { + return HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "username not exist", + OriginError: err, + } + } + if err != nil { + return HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to select user from db", + OriginError: err, + } + } + if password != dbUser.Password { + return HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "password incorrect", + OriginError: err, + } + } + return next(w, req.WithContext(context.WithValue( + ctx, types.UserKey, models.User{ + Username: username, + Password: password, + }))) + } +} diff --git a/middlewares/errorHandler.go b/middlewares/errorHandler.go new file mode 100644 index 0000000..dc9be12 --- /dev/null +++ b/middlewares/errorHandler.go @@ -0,0 +1,63 @@ +package middlewares + +import ( + "fmt" + "net/http" + "os" + + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +type HTTPError struct { + StatusCode int `json:"code"` + Message string `json:"message"` + OriginError error `json:"-"` +} + +func (e HTTPError) Error() string { + return e.Message +} + +func NewHTTPError(err error) HTTPError { + return HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "Internal server error with unknown reason", + OriginError: err, + } +} + +func (self *Handlers) ErrorHandler( + next bunrouter.HandlerFunc, +) bunrouter.HandlerFunc { + return func(w http.ResponseWriter, req bunrouter.Request) error { + err := next(w, req) + + var httpErr HTTPError + switch err := err.(type) { + case nil: + return nil + + case HTTPError: + httpErr = err + + default: + fmt.Fprintf(os.Stderr, "unhandled error, %v\n", err) + zap.L().Error("unhandled error", + zap.Error(err)) + httpErr = NewHTTPError(err) + } + + if httpErr.OriginError == nil { + zap.L().Warn(httpErr.Message) + } else { + zap.L().Warn(httpErr.Message, + zap.Error(httpErr.OriginError)) + } + + w.WriteHeader(httpErr.StatusCode) + _ = bunrouter.JSON(w, httpErr) + + return err + } +} diff --git a/middlewares/handlers.go b/middlewares/handlers.go new file mode 100644 index 0000000..573380c --- /dev/null +++ b/middlewares/handlers.go @@ -0,0 +1,11 @@ +package middlewares + +import "github.com/uptrace/bun" + +type Handlers struct { + db *bun.DB +} + +func NewHandlers(db *bun.DB) *Handlers { + return &Handlers{db: db} +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..48f5aae --- /dev/null +++ b/models/user.go @@ -0,0 +1,13 @@ +package models + +import "github.com/uptrace/bun" + +type User struct { + bun.BaseModel `bun:"table:user" json:"-"` + + Username string `bun:"username,pk" json:"username"` + Password string `bun:"password" json:"password"` + + IsLogged bool `bun:"is_logged" json:"isLogged"` + LoginCount int `bun:"login_count" json:"loginCount"` +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..ef5bf62 --- /dev/null +++ b/player.go @@ -0,0 +1,37 @@ +package main + +import ( + "gitea.konchin.com/ytshih/inp2025/stages" + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var playerCmd = &cobra.Command{ + Use: "player", + Run: func(cmd *cobra.Command, args []string) { + queue := []*tea.Program{} + + base := stages.NewBaseModel(&queue, viper.GetString("auth-endpoint")) + queue = append(queue, + tea.NewProgram(stages.NewLandingModel(base))) + + for len(queue) > 0 { + program := queue[0] + queue = queue[1:] + _, err := program.Run() + if err != nil { + panic(err) + } + } + }, +} + +func init() { + playerCmd.Flags(). + Int("udp-listen-port", 18787, "") + playerCmd.Flags(). + StringSlice("udp-endpoints", []string{"localhost:18787"}, "") + playerCmd.Flags(). + String("auth-endpoint", "http://localhost:8888", "") +} diff --git a/stages/base.go b/stages/base.go new file mode 100644 index 0000000..59a5199 --- /dev/null +++ b/stages/base.go @@ -0,0 +1,23 @@ +package stages + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/go-resty/resty/v2" +) + +type BaseModel struct { + queue *[]*tea.Program + client *resty.Client +} + +func NewBaseModel( + queue *[]*tea.Program, + endpoint string, +) *BaseModel { + return &BaseModel{ + queue: queue, + client: resty.New(). + SetBaseURL(endpoint). + SetDisableWarn(true), + } +} diff --git a/stages/landing.go b/stages/landing.go new file mode 100644 index 0000000..f2fc6e8 --- /dev/null +++ b/stages/landing.go @@ -0,0 +1,241 @@ +package stages + +import ( + "fmt" + "net/http" + "strings" + + "gitea.konchin.com/ytshih/inp2025/models" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type landingOperationType int + +const ( + landingOperationChoose landingOperationType = iota + landingOperationLoginCred + landingOperationRegistCred +) + +type focusTargetType int + +const ( + focusTargetUsername focusTargetType = iota + focusTargetPassword +) + +type LandingModel struct { + *BaseModel + op landingOperationType + + focus focusTargetType + username textinput.Model + password textinput.Model + + info string + err error +} + +func NewLandingModel(base *BaseModel) *LandingModel { + username := textinput.New() + username.Placeholder = "Username" + username.CharLimit = 32 + username.Width = 32 + username.Blur() + + password := textinput.New() + password.Placeholder = "Password" + password.CharLimit = 32 + password.Width = 32 + password.Blur() + password.EchoMode = textinput.EchoPassword + password.EchoCharacter = '•' + + return &LandingModel{ + BaseModel: base, + op: landingOperationChoose, + + username: username, + password: password, + } +} + +func (m *LandingModel) reset() { + m.op = landingOperationChoose + m.username.Blur() + m.username.Reset() + m.password.Blur() + m.password.Reset() +} + +type postLoginMsg struct{} + +func (m *LandingModel) postLogin() tea.Cmd { + return func() tea.Msg { + resp, err := m.BaseModel.client.R(). + SetBasicAuth(m.username.Value(), m.password.Value()). + Post("/auth/login") + if err == nil { + switch resp.StatusCode() { + case http.StatusOK: + m.BaseModel.client.SetBasicAuth( + m.username.Value(), m.password.Value()) + m.info = "login success.\n" + m.err = nil + case http.StatusUnauthorized: + m.err = fmt.Errorf("user not exist or password incorrect, %s", + string(resp.Body())) + default: + m.err = fmt.Errorf("unknown server error, %s", + string(resp.Body())) + } + } else { + m.err = fmt.Errorf("failed to post login, %w", err) + } + return postLoginMsg{} + } +} + +type postRegisterMsg struct{} + +func (m *LandingModel) postRegister() tea.Cmd { + return func() tea.Msg { + resp, err := m.BaseModel.client.R(). + SetBody(models.User{ + Username: m.username.Value(), + Password: m.password.Value(), + }). + Post("/auth/register") + if err == nil { + switch resp.StatusCode() { + case http.StatusOK: + m.info = "register success.\n" + m.err = nil + case http.StatusBadRequest: + m.err = fmt.Errorf("username already exist, %s", + string(resp.Body())) + default: + m.err = fmt.Errorf("unknown server error, %s", + string(resp.Body())) + } + } else { + m.err = fmt.Errorf("failed to post register, %s", + string(resp.Body())) + } + return postRegisterMsg{} + } +} + +func (m *LandingModel) Init() tea.Cmd { + return tea.ClearScreen +} + +func (m *LandingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + case "L", "l": + if m.op == landingOperationChoose { + m.op = landingOperationLoginCred + m.focus = focusTargetUsername + cmds = append(cmds, m.username.Focus()) + return m, tea.Batch(cmds...) + } + case "R", "r": + if m.op == landingOperationChoose { + m.op = landingOperationRegistCred + m.focus = focusTargetUsername + cmds = append(cmds, m.username.Focus()) + return m, tea.Batch(cmds...) + } + case "shift+tab", "up": + if m.op == landingOperationLoginCred || + m.op == landingOperationRegistCred { + switch m.focus { + case focusTargetUsername: + m.username.Blur() + m.focus = focusTargetPassword + cmds = append(cmds, m.password.Focus()) + case focusTargetPassword: + m.password.Blur() + m.focus = focusTargetUsername + cmds = append(cmds, m.username.Focus()) + } + } + case "tab", "down", "enter": + if m.op == landingOperationLoginCred || + m.op == landingOperationRegistCred { + switch m.focus { + case focusTargetUsername: + m.username.Blur() + m.focus = focusTargetPassword + cmds = append(cmds, m.password.Focus()) + case focusTargetPassword: + if msg.String() == "enter" { + switch m.op { + case landingOperationLoginCred: + cmds = append(cmds, m.postLogin()) + case landingOperationRegistCred: + cmds = append(cmds, m.postRegister()) + } + } else { + m.password.Blur() + m.focus = focusTargetUsername + cmds = append(cmds, m.username.Focus()) + } + } + } + } + case postLoginMsg: + if m.err == nil { + *m.queue = append(*m.queue, + tea.NewProgram(NewLobbyModel(m.BaseModel))) + return m, tea.Quit + } else { + m.reset() + } + case postRegisterMsg: + m.reset() + } + + var cmd tea.Cmd + m.username, cmd = m.username.Update(msg) + cmds = append(cmds, cmd) + m.password, cmd = m.password.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m *LandingModel) View() string { + var b strings.Builder + + switch m.op { + case landingOperationChoose: + fmt.Fprintf(&b, "Choose Operation\n(L)ogin / (R)egister\n") + case landingOperationLoginCred: + fmt.Fprintf(&b, "Login: \n") + b.WriteString(m.username.View() + "\n") + b.WriteString(m.password.View() + "\n") + case landingOperationRegistCred: + fmt.Fprintf(&b, "Register: \n") + b.WriteString(m.username.View() + "\n") + b.WriteString(m.password.View() + "\n") + } + + if m.info != "" { + b.WriteString(m.info + "\n") + } + + if m.err != nil { + b.WriteString("----------\n") + b.WriteString(m.err.Error() + "\n") + } + + return b.String() +} diff --git a/stages/lobby.go b/stages/lobby.go new file mode 100644 index 0000000..08adaff --- /dev/null +++ b/stages/lobby.go @@ -0,0 +1,283 @@ +package stages + +import ( + "fmt" + "net" + "strings" + "time" + + "gitea.konchin.com/ytshih/inp2025/types" + "gitea.konchin.com/ytshih/inp2025/utils" + "gitea.konchin.com/ytshih/inp2025/workflows" + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/viper" + "github.com/vmihailenco/msgpack/v5" +) + +const ( + REFRESH_TIME = time.Second +) + +type lobbyOperationType int + +const ( + lobbyOperationChoose lobbyOperationType = iota + lobbyOperationServerWaiting + lobbyOperationServerChoose + lobbyOperationClientScannning +) + +type LobbyModel struct { + *BaseModel + op lobbyOperationType + + shutdown types.ShutdownFunc + local string + remote string + + // server + remoteUser string + listener net.Listener + + // client + cursor int + endpoints []string + + info string + err error +} + +func NewLobbyModel(base *BaseModel) *LobbyModel { + return &LobbyModel{ + BaseModel: base, + op: lobbyOperationChoose, + + shutdown: func() {}, + } +} + +type serverListenMsg struct{} + +func (m *LobbyModel) serverListen() tea.Cmd { + return func() tea.Msg { + dataCh := make(chan string) + m.local, m.shutdown, m.err = utils.ListenUDPData( + viper.GetInt("udp-listen-port"), dataCh) + if m.err != nil { + m.err = fmt.Errorf("failed to listen, %w", m.err) + return serverListenMsg{} + } + + req := <-dataCh + m.shutdown() + m.shutdown = func() {} + + var joinRequest types.JoinRequest + if err := msgpack.Unmarshal([]byte(req), &joinRequest); err != nil { + m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err) + return serverListenMsg{} + } + + m.remote = joinRequest.Endpoint + m.remoteUser = joinRequest.Username + return serverListenMsg{} + } +} + +type serverSendReplyMsg struct{} + +func (m *LobbyModel) serverSendReply(response bool) tea.Cmd { + return func() tea.Msg { + if response { + // Start Wordle Server listener + m.listener, m.err = net.Listen("tcp4", ":0") + if m.err != nil { + m.err = fmt.Errorf("failed to listen on anonymous port, %w", m.err) + return serverSendReplyMsg{} + } + + local := fmt.Sprintf("%s:%d", + m.listener.Addr().(*net.TCPAddr).IP.String(), + m.listener.Addr().(*net.TCPAddr).Port) + m.err = utils.SendPayload(local, m.remote, + types.JoinResponse{Endpoint: local}) + + // Store wordle server endpoint + m.remote = local + } else { + m.err = utils.SendPayload("", m.remote, + types.JoinResponse{Endpoint: ""}) + } + return serverSendReplyMsg{} + } +} + +type clientScanMsg time.Time + +func (m *LobbyModel) clientScan() tea.Cmd { + return tea.Tick(REFRESH_TIME, func(t time.Time) tea.Msg { + m.endpoints = []string{} + for _, endpoint := range viper.GetStringSlice("udp-endpoints") { + if err := utils.Ping(endpoint); err == nil { + m.endpoints = append(m.endpoints, endpoint) + } + } + return clientScanMsg(t) + }) +} + +type clientJoinMsg struct{} + +func (m *LobbyModel) clientJoin() tea.Cmd { + return func() tea.Msg { + dataCh := make(chan string) + m.local, m.shutdown, m.err = utils.ListenUDPData(0, dataCh) + if m.err != nil { + m.err = fmt.Errorf("failed to listen udp data, %w", m.err) + return clientJoinMsg{} + } + + m.err = utils.SendPayload(m.local, m.remote, types.JoinRequest{ + Endpoint: m.local, + Username: m.client.UserInfo.Username, + }) + if m.err != nil { + m.err = fmt.Errorf("failed to send invitation, %w", m.err) + return clientJoinMsg{} + } + + data := <-dataCh + m.shutdown() + m.shutdown = func() {} + + var joinResponse types.JoinResponse + if err := msgpack.Unmarshal([]byte(data), &joinResponse); err != nil { + m.err = fmt.Errorf("failed to unmarshal msgpack, %w", err) + return clientJoinMsg{} + } + + m.remote = joinResponse.Endpoint + return clientJoinMsg{} + } +} + +func (m *LobbyModel) Init() tea.Cmd { + return tea.ClearScreen +} + +func (m *LobbyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + return m, tea.Quit + } + } + + switch m.op { + case lobbyOperationChoose: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "S", "s": + m.op = lobbyOperationServerWaiting + cmds = append(cmds, m.serverListen()) + case "C", "c": + m.op = lobbyOperationClientScannning + cmds = append(cmds, m.clientScan()) + } + } + case lobbyOperationServerWaiting: + switch msg.(type) { + case serverListenMsg: + m.op = lobbyOperationServerChoose + } + case lobbyOperationServerChoose: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "Y", "y", "enter": + cmds = append(cmds, m.serverSendReply(true)) + case "N", "n": + m.op = lobbyOperationServerWaiting + cmds = append(cmds, m.serverSendReply(false)) + } + case serverSendReplyMsg: + if m.err == nil { + m.shutdown() + m.client.SetBaseURL("http://" + m.remote) + shutdown := workflows.WordleServer(m.listener) + *m.queue = append(*m.queue, + tea.NewProgram(NewWordleClientModel(m.BaseModel, shutdown))) + return m, tea.Quit + } else { + m.op = lobbyOperationServerWaiting + } + } + case lobbyOperationClientScannning: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "shift+tab", "up": + if n := len(m.endpoints); n > 0 { + m.cursor = (m.cursor - 1 + n) % n + } + case "tab", "down": + if n := len(m.endpoints); n > 0 { + m.cursor = (m.cursor + 1) % n + } + case "enter": + m.remote = m.endpoints[m.cursor] + cmds = append(cmds, m.clientJoin()) + } + case clientScanMsg: + cmds = append(cmds, m.clientScan()) + case clientJoinMsg: + if m.err == nil && m.remote != "" { + m.shutdown() + m.client.SetBaseURL("http://" + m.remote) + *m.BaseModel.queue = append(*m.BaseModel.queue, + tea.NewProgram(NewWordleClientModel(m.BaseModel, func() {}))) + return m, tea.Quit + } + } + } + + return m, tea.Batch(cmds...) +} + +func (m *LobbyModel) View() string { + var b strings.Builder + + switch m.op { + case lobbyOperationChoose: + b.WriteString("Choose Role\n(S)erver / (C)lient\n") + case lobbyOperationServerWaiting: + b.WriteString("Wait for user to join...\n") + case lobbyOperationServerChoose: + fmt.Fprintf(&b, "Receive Join Request by '%s'\nAccept (Y)/N\n", + m.remoteUser) + case lobbyOperationClientScannning: + b.WriteString("Scanning server...\nChoose one to join\n") + for i, endpoint := range m.endpoints { + if i == m.cursor { + fmt.Fprintf(&b, "(•) %s\n", endpoint) + } else { + fmt.Fprintf(&b, "( ) %s\n", endpoint) + } + } + } + + if m.info != "" { + b.WriteString(m.info + "\n") + } + + if m.err != nil { + b.WriteString("----------\n") + b.WriteString(m.err.Error() + "\n") + } + + return b.String() +} diff --git a/stages/udp.go b/stages/udp.go new file mode 100644 index 0000000..439c360 --- /dev/null +++ b/stages/udp.go @@ -0,0 +1 @@ +package stages diff --git a/stages/wordle.go b/stages/wordle.go new file mode 100644 index 0000000..02dff2a --- /dev/null +++ b/stages/wordle.go @@ -0,0 +1,179 @@ +package stages + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "gitea.konchin.com/ytshih/inp2025/handlers/wordle" + "gitea.konchin.com/ytshih/inp2025/types" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/gorilla/websocket" + "github.com/vmihailenco/msgpack/v5" +) + +const ( + WEBSOCKET_RETRY int = 5 + WEBSOCKET_BACKOFF time.Duration = 100 * time.Millisecond +) + +type WordleClientModel struct { + *BaseModel + conn *websocket.Conn + state types.WordleState + shutdown types.ShutdownFunc + + input textinput.Model + err error +} + +func NewWordleClientModel( + base *BaseModel, + shutdown types.ShutdownFunc, +) *WordleClientModel { + input := textinput.New() + input.Focus() + input.CharLimit = types.GUESS_WORD_LENGTH + input.Width = types.GUESS_WORD_LENGTH + return &WordleClientModel{ + BaseModel: base, + input: input, + shutdown: shutdown, + } +} + +func (m *WordleClientModel) getState() tea.Cmd { + return func() tea.Msg { + if m.conn == nil { + u, err := url.Parse(m.client.BaseURL) + if err != nil { + m.err = fmt.Errorf("failed to parse BaseURL, %w", err) + } + u.Scheme = "ws" + u.Path = "/api/state" + + for try := 0; try < WEBSOCKET_RETRY; try++ { + req, _ := http.NewRequest("GET", + /*placeholder*/ "http://localhost", nil) + req.SetBasicAuth( + m.client.UserInfo.Username, + m.client.UserInfo.Password) + m.conn, _, err = websocket.DefaultDialer.Dial( + u.String(), req.Header) + if err == nil { + break + } + time.Sleep(WEBSOCKET_BACKOFF) + } + if err != nil { + m.err = fmt.Errorf("failed to dial, %w", err) + } + } + + _, b, err := m.conn.ReadMessage() + if err == nil { + var state types.WordleState + if err := msgpack.Unmarshal(b, &state); err != nil { + m.err = fmt.Errorf("failed to unmarshal state, %w", err) + } + return state + } else { + m.err = fmt.Errorf("failed to read message, %w", err) + return nil + } + } +} + +func (m *WordleClientModel) postGuess(guess string) tea.Cmd { + return func() tea.Msg { + b, err := msgpack.Marshal(wordle.OperationGuess{ + Username: m.client.UserInfo.Username, + Guess: guess, + }) + if err != nil { + m.err = fmt.Errorf("failed to post guess, %w", err) + } + _, err = m.client.R(). + SetBody(b). + Post("/api/guess") + if err != nil { + m.err = fmt.Errorf("failed to post guess, %w", err) + } + return nil + } +} + +func (m *WordleClientModel) Init() tea.Cmd { + return tea.Sequence(tea.ClearScreen, + tea.Batch(m.getState(), textinput.Blink)) +} + +func (m *WordleClientModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + m.conn.Close() + m.shutdown() + return m, tea.Quit + case "enter": + if m.state.GameEnd { + m.conn.Close() + m.shutdown() + *m.queue = append(*m.queue, + tea.NewProgram(NewLobbyModel(m.BaseModel))) + return m, tea.Quit + } + if len(m.input.Value()) == types.GUESS_WORD_LENGTH { + input := strings.ToUpper(m.input.Value()) + m.input.Reset() + cmds = append(cmds, m.postGuess(input)) + } + } + case types.WordleState: + m.state = msg + if m.state.GameEnd { + m.input.Blur() + } + if _, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok { + m.input.Blur() + } else { + cmds = append(cmds, m.input.Focus()) + } + cmds = append(cmds, m.getState()) + } + + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m *WordleClientModel) View() string { + var b strings.Builder + + fmt.Fprintf(&b, "Login as '%s'\n", m.client.UserInfo.Username) + fmt.Fprintf(&b, "remote addr: %s\n", m.client.BaseURL) + + b.WriteString(m.state.View() + "\n") + b.WriteString(types.NewWordBank(m.state).View() + "\n") + + if m.state.GameEnd { + fmt.Fprintf(&b, "Game End. Press 'Enter' to lobby.\n") + } else { + if guess, ok := m.state.CurrentGuess[m.client.UserInfo.Username]; ok { + fmt.Fprintf(&b, "Current guess for the round: %s\n", guess) + } else { + fmt.Fprintf(&b, "guess: %s\n", m.input.View()) + } + if m.err != nil { + fmt.Fprintf(&b, "error: %+v\n", m.err) + } + } + + return b.String() +} diff --git a/test.go b/test.go new file mode 100644 index 0000000..54df20c --- /dev/null +++ b/test.go @@ -0,0 +1,38 @@ +package main + +import ( + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/spf13/cobra" +) + +var testCmd = &cobra.Command{ + Use: "test", +} + +var testServerCmd = &cobra.Command{ + Use: "server", + Run: func(cmd *cobra.Command, args []string) { + dataCh := make(chan string) + _, shutdown, err := utils.ListenUDPData(18787, dataCh) + if err != nil { + panic(err) + } + defer shutdown() + <-dataCh + }, +} + +var testClientCmd = &cobra.Command{ + Use: "client", + Run: func(cmd *cobra.Command, args []string) { + err := utils.Ping("localhost:18787") + if err != nil { + panic(err) + } + }, +} + +func init() { + testCmd.AddCommand(testServerCmd) + testCmd.AddCommand(testClientCmd) +} diff --git a/types/auth.go b/types/auth.go new file mode 100644 index 0000000..dd30c15 --- /dev/null +++ b/types/auth.go @@ -0,0 +1,7 @@ +package types + +type UserType struct{} + +var ( + UserKey = UserType{} +) diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..6097cbe --- /dev/null +++ b/types/types.go @@ -0,0 +1,5 @@ +package types + +type ShutdownFunc func() + +type UsernameType = string diff --git a/types/udp.go b/types/udp.go new file mode 100644 index 0000000..8d58c6d --- /dev/null +++ b/types/udp.go @@ -0,0 +1,10 @@ +package types + +type JoinRequest struct { + Endpoint string + Username UsernameType +} + +type JoinResponse struct { + Endpoint string +} diff --git a/types/wordle.go b/types/wordle.go new file mode 100644 index 0000000..e2c9769 --- /dev/null +++ b/types/wordle.go @@ -0,0 +1,209 @@ +package types + +import ( + "fmt" + "sort" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +const ( + GUESS_COUNT_LIMIT int = 6 + GUESS_WORD_LENGTH int = 5 + + ALPHABET_COUNT int = 26 +) + +type GuessStateType int + +const ( + GuessStateNotGuessed GuessStateType = iota + GuessStateWrongChar + GuessStateWrongPos + GuessStateCorrect +) + +type GuessChar struct { + Char rune + State GuessStateType +} + +func (self GuessChar) View() string { + style := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFFFF")) + switch self.State { + case GuessStateWrongChar: + style = style.Background(lipgloss.Color("#3a3a3c")) + case GuessStateWrongPos: + style = style.Background(lipgloss.Color("#b59f3b")) + case GuessStateCorrect: + style = style.Background(lipgloss.Color("#538d4e")) + } + return style.Render(string(self.Char)) +} + +type Guess struct { + Data [GUESS_WORD_LENGTH]GuessChar +} + +type guessLeftType struct { + Rune rune + Idx int +} + +func NewGuess(guess, ans string) Guess { + var ret Guess + + var ansLeft []rune + var guessLeft []guessLeftType + for i := range ret.Data { + ret.Data[i] = GuessChar{ + Char: rune(guess[i]), + State: GuessStateWrongChar, + } + if guess[i] == ans[i] { + ret.Data[i].State = GuessStateCorrect + } else { + ansLeft = append(ansLeft, rune(ans[i])) + guessLeft = append(guessLeft, guessLeftType{ + Rune: rune(guess[i]), + Idx: i, + }) + } + } + + sort.Slice(ansLeft, func(i, j int) bool { + return int32(ansLeft[i]) < int32(ansLeft[j]) + }) + + sort.Slice(guessLeft, func(i, j int) bool { + return int32(guessLeft[i].Rune) < int32(guessLeft[j].Rune) + }) + + i, j := 0, 0 + for i < len(ansLeft) && j < len(guessLeft) { + if int32(ansLeft[i]) == int32(guessLeft[j].Rune) { + ret.Data[guessLeft[j].Idx].State = GuessStateWrongPos + i++ + j++ + } else { + if int32(ansLeft[i]) < int32(guessLeft[j].Rune) { + i++ + } else { + j++ + } + } + } + + return ret +} + +func (self Guess) View() string { + var b strings.Builder + for _, ch := range self.Data { + b.WriteString(ch.View()) + } + return b.String() +} + +type WordleState struct { + History map[UsernameType][GUESS_COUNT_LIMIT]Guess + CurrentGuess map[UsernameType]string + + Round int + GameEnd bool +} + +func NewWordleState() WordleState { + return WordleState{ + History: make(map[UsernameType][GUESS_COUNT_LIMIT]Guess), + CurrentGuess: make(map[UsernameType]string), + + Round: 0, + GameEnd: false, + } +} + +func (self *WordleState) View() string { + var b strings.Builder + + col := []string{} + for user := range self.History { + col = append(col, user) + } + + sort.Slice(col, func(i, j int) bool { + return col[i] < col[j] + }) + + fmt.Fprintf(&b, " ") + for _, user := range col { + fmt.Fprintf(&b, "%5s ", user) + } + b.WriteRune('\n') + + table := make([][]string, GUESS_COUNT_LIMIT) + for i := range table { + table[i] = make([]string, len(col)) + } + for j, user := range col { + for i, guess := range self.History[user] { + table[i][j] = guess.View() + } + } + + for i, row := range table { + fmt.Fprintf(&b, "%1d ", i+1) + for _, e := range row { + b.WriteString(e + " ") + } + b.WriteRune('\n') + } + + return b.String() +} + +type WordBank struct { + Data [ALPHABET_COUNT]GuessStateType +} + +func NewWordBank(state WordleState) WordBank { + // FIXME: breaks when enter ^A-Z + var ret WordBank + for _, guesses := range state.History { + for i := 0; i < state.Round; i++ { + for _, ch := range guesses[i].Data { + ret.Data[int32(ch.Char)-'A'] = max( + ret.Data[int32(ch.Char)-'A'], + ch.State) + } + } + } + return ret +} + +func (self WordBank) View() string { + var b strings.Builder + + for i, state := range self.Data { + style := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFFFF")) + switch state { + case GuessStateNotGuessed: + style = style.Background(lipgloss.Color("#2c3032")) + case GuessStateWrongChar: + style = style.Background(lipgloss.Color("#3a3a3c")) + case GuessStateWrongPos: + style = style.Background(lipgloss.Color("#b59f3b")) + case GuessStateCorrect: + style = style.Background(lipgloss.Color("#538d4e")) + } + b.WriteString(style.Render(string('A' + i))) + if i%10 == 9 { + b.WriteRune('\n') + } + } + + return b.String() +} diff --git a/utils/initDB.go b/utils/initDB.go new file mode 100644 index 0000000..f017db5 --- /dev/null +++ b/utils/initDB.go @@ -0,0 +1,13 @@ +package utils + +import ( + "context" + + "gitea.konchin.com/ytshih/inp2025/models" + "github.com/uptrace/bun" +) + +func InitDB(ctx context.Context, db *bun.DB) error { + return db.ResetModel(ctx, + (*models.User)(nil)) +} diff --git a/utils/success.go b/utils/success.go new file mode 100644 index 0000000..6fd2f3b --- /dev/null +++ b/utils/success.go @@ -0,0 +1,14 @@ +package utils + +import ( + "io" + "net/http" +) + +func Success(w http.ResponseWriter) error { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain") + _, err := io.WriteString(w, + `{"code":200, "message": "success"}`+"\n") + return err +} diff --git a/utils/udp.go b/utils/udp.go new file mode 100644 index 0000000..e33b37a --- /dev/null +++ b/utils/udp.go @@ -0,0 +1,143 @@ +package utils + +import ( + "fmt" + "net" + + "gitea.konchin.com/ytshih/inp2025/types" + "github.com/vmihailenco/msgpack/v5" +) + +const ( + BUFFER_SIZE int = 1024 + MAGIC_NUMBER int = 114514 +) + +type UDPReqType int + +const ( + UDPReqTypeData UDPReqType = iota + UDPReqTypePingRequest + UDPReqTypePingReply +) + +type UDPPayload struct { + MagicNumber int `msgpack:"magicNumber"` + Endpoint string `msgpack:"endpoint"` + Type UDPReqType `msgpack:"type"` + + Data string `msgpack:"data"` +} + +func ListenUDPData( + port int, + dataCh chan string, +) (string, types.ShutdownFunc, error) { + return ListenUDP(port, dataCh, nil) +} + +func Ping(endpoint string) error { + pingCh := make(chan string) + local, shutdown, err := ListenUDP(0, nil, pingCh) + if err != nil { + return err + } + defer shutdown() + + err = SendRawPayload(endpoint, UDPPayload{ + MagicNumber: MAGIC_NUMBER, + Endpoint: local, + Type: UDPReqTypePingRequest, + }) + if err != nil { + return err + } + <-pingCh + return nil +} + +func ListenUDP( + port int, + dataCh chan string, + pingCh chan string, +) (string, types.ShutdownFunc, error) { + conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port}) + if err != nil { + return "", nil, fmt.Errorf("failed to listen udp, %w", err) + } + local := conn.LocalAddr().String() + + go func() { + for { + buffer := make([]byte, BUFFER_SIZE) + + n, _, err := conn.ReadFromUDP(buffer) + if err != nil { + continue + } + + var payload UDPPayload + err = msgpack.Unmarshal(buffer[:n], &payload) + if err == nil && payload.MagicNumber == MAGIC_NUMBER { + switch payload.Type { + case UDPReqTypeData: + if dataCh != nil { + dataCh <- payload.Data + } + case UDPReqTypePingRequest: + SendRawPayload(payload.Endpoint, UDPPayload{ + MagicNumber: MAGIC_NUMBER, + Endpoint: local, + Type: UDPReqTypePingReply, + }) + case UDPReqTypePingReply: + if pingCh != nil { + pingCh <- payload.Endpoint + } + } + } + } + }() + + return local, func() { conn.Close() }, nil +} + +func SendPayload( + local, remote string, + data any, +) error { + sdata, err := msgpack.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal data, %w", err) + } + + return SendRawPayload(remote, UDPPayload{ + MagicNumber: MAGIC_NUMBER, + Endpoint: local, + Type: UDPReqTypeData, + Data: string(sdata), + }) +} + +func SendRawPayload( + endpoint string, + payload UDPPayload, +) error { + conn, err := net.Dial("udp", endpoint) + if err != nil { + return fmt.Errorf("failed to dial endpoint, %w", err) + } + defer conn.Close() + + b, err := msgpack.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload, %w", err) + } + + _, err = conn.Write(b) + if err != nil { + return fmt.Errorf("failed to send payload, %w", err) + } + + return nil +} diff --git a/workflows/authServer.go b/workflows/authServer.go new file mode 100644 index 0000000..013fd4a --- /dev/null +++ b/workflows/authServer.go @@ -0,0 +1,63 @@ +package workflows + +import ( + "context" + "database/sql" + "fmt" + "net/http" + + "gitea.konchin.com/ytshih/inp2025/handlers/auth" + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/utils" + "github.com/spf13/viper" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/sqlitedialect" + "github.com/uptrace/bun/driver/sqliteshim" + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +func AuthServer() { + sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") + if err != nil { + panic(fmt.Errorf("failed to init sqlite, %w", err)) + } + db := bun.NewDB(sqldb, sqlitedialect.New()) + if err := utils.InitDB(context.Background(), db); err != nil { + panic(fmt.Errorf("failed to init db schema, %w", err)) + } + + logger := zap.NewExample() + defer logger.Sync() + undo := zap.ReplaceGlobals(logger) + defer undo() + + authHandlers := auth.NewHandlers(db) + middlewareHandlers := middlewares.NewHandlers(db) + + router := bunrouter.New() + + authGroup := router.NewGroup("/auth"). + Use(middlewareHandlers.ErrorHandler) + authGroup.POST("/register", + authHandlers.PostRegister) + authGroup.POST("/login", + middlewareHandlers.Auth(authHandlers.PostLogin)) + authGroup.POST("/logout", + middlewareHandlers.Auth(authHandlers.PostLogout)) + + server := &http.Server{ + Addr: viper.GetString("listen-addr"), + Handler: http.Handler(router), + } + + defer func() { + sqldb.Close() + if err := server.Shutdown(context.Background()); err != nil { + panic(fmt.Errorf("failed to shutdown wordle server, %w", err)) + } + }() + + fmt.Printf("server up\n") + fmt.Println(server.ListenAndServe()) +} diff --git a/workflows/wordleServer.go b/workflows/wordleServer.go new file mode 100644 index 0000000..fa3152a --- /dev/null +++ b/workflows/wordleServer.go @@ -0,0 +1,86 @@ +package workflows + +import ( + "context" + "fmt" + "math/rand" + "net" + "net/http" + "os" + + "gitea.konchin.com/ytshih/inp2025/handlers/wordle" + "gitea.konchin.com/ytshih/inp2025/middlewares" + "gitea.konchin.com/ytshih/inp2025/types" + "github.com/uptrace/bunrouter" + "go.uber.org/zap" +) + +const ( + DICT_FILE string = "./dict" +) + +func generateWordleAns() (string, error) { + file, err := os.Open(DICT_FILE) + if err != nil { + return "", fmt.Errorf("failed to open dictionary, %w", err) + } + + dict := []string{} + for { + var w string + _, err := fmt.Fscanf(file, "%s", &w) + if err != nil { + break + } + dict = append(dict, w) + } + + return dict[rand.Intn(len(dict))], nil +} + +func WordleServer(listener net.Listener) types.ShutdownFunc { + ans, err := generateWordleAns() + if err != nil { + panic(fmt.Errorf("failed to generate answer, %w", err)) + } + + logger, _ := zap.Config{ + Level: zap.NewAtomicLevelAt(zap.InfoLevel), + Encoding: "json", + OutputPaths: []string{"logs/wordle-stdout.log"}, + ErrorOutputPaths: []string{"logs/wordle-stderr.log"}, + EncoderConfig: zap.NewProductionEncoderConfig(), + }.Build() + undo := zap.ReplaceGlobals(logger) + + wordleHandlers := wordle.NewHandlers(ans) + middlewareHandlers := middlewares.NewHandlers(nil) + + router := bunrouter.New() + + apiGroup := router.NewGroup("/api"). + Use(middlewareHandlers.ErrorHandler) + apiGroup.GET("/state", + wordleHandlers.GetState) + apiGroup.POST("/guess", + wordleHandlers.PostGuess) + + server := &http.Server{ + Handler: http.Handler(router), + } + + go func() { + fmt.Printf("server up\n") + if err := server.Serve(listener); err != http.ErrServerClosed { + panic(fmt.Errorf("wordle server failed, %w", err)) + } + }() + + return func() { + logger.Sync() + undo() + if err := server.Shutdown(context.Background()); err != nil { + panic(fmt.Errorf("failed to shutdown wordle server, %w", err)) + } + } +}