diff --git a/examples/pogo-optimizer/data/moves.json b/examples/pogo-optimizer/data/moves.json deleted file mode 100644 index 73a18c62..00000000 --- a/examples/pogo-optimizer/data/moves.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":13,"type":"Normal","name":"Wrap"},{"id":14,"type":"Normal","name":"Hyper Beam"},{"id":16,"type":"Dark","name":"Dark Pulse"},{"id":18,"type":"Poison","name":"Sludge"},{"id":20,"type":"Normal","name":"Vice Grip"},{"id":21,"type":"Fire","name":"Flame Wheel"},{"id":22,"type":"Bug","name":"Megahorn"},{"id":24,"type":"Fire","name":"Flamethrower"},{"id":26,"type":"Ground","name":"Dig"},{"id":28,"type":"Fighting","name":"Cross Chop"},{"id":30,"type":"Psychic","name":"Psybeam"},{"id":31,"type":"Ground","name":"Earthquake"},{"id":32,"type":"Rock","name":"Stone Edge"},{"id":33,"type":"Ice","name":"Ice Punch"},{"id":34,"type":"Psychic","name":"Heart Stamp"},{"id":35,"type":"Electric","name":"Discharge"},{"id":36,"type":"Steel","name":"Flash Cannon"},{"id":38,"type":"Flying","name":"Drill Peck"},{"id":39,"type":"Ice","name":"Ice Beam"},{"id":40,"type":"Ice","name":"Blizzard"},{"id":42,"type":"Fire","name":"Heat Wave"},{"id":45,"type":"Flying","name":"Aerial Ace"},{"id":46,"type":"Ground","name":"Drill Run"},{"id":47,"type":"Grass","name":"Petal Blizzard"},{"id":48,"type":"Grass","name":"Mega Drain"},{"id":49,"type":"Bug","name":"Bug Buzz"},{"id":50,"type":"Poison","name":"Poison Fang"},{"id":51,"type":"Dark","name":"Night Slash"},{"id":53,"type":"Water","name":"Bubble Beam"},{"id":54,"type":"Fighting","name":"Submission"},{"id":56,"type":"Fighting","name":"Low Sweep"},{"id":57,"type":"Water","name":"Aqua Jet"},{"id":58,"type":"Water","name":"Aqua Tail"},{"id":59,"type":"Grass","name":"Seed Bomb"},{"id":60,"type":"Psychic","name":"Psyshock"},{"id":62,"type":"Rock","name":"Ancient Power"},{"id":63,"type":"Rock","name":"Rock Tomb"},{"id":64,"type":"Rock","name":"Rock Slide"},{"id":65,"type":"Rock","name":"Power Gem"},{"id":66,"type":"Ghost","name":"Shadow Sneak"},{"id":67,"type":"Ghost","name":"Shadow Punch"},{"id":69,"type":"Ghost","name":"Ominous Wind"},{"id":70,"type":"Ghost","name":"Shadow Ball"},{"id":72,"type":"Steel","name":"Magnet Bomb"},{"id":74,"type":"Steel","name":"Iron Head"},{"id":75,"type":"Electric","name":"Parabolic Charge"},{"id":77,"type":"Electric","name":"Thunder Punch"},{"id":78,"type":"Electric","name":"Thunder"},{"id":79,"type":"Electric","name":"Thunderbolt"},{"id":80,"type":"Dragon","name":"Twister"},{"id":82,"type":"Dragon","name":"Dragon Pulse"},{"id":83,"type":"Dragon","name":"Dragon Claw"},{"id":84,"type":"Fairy","name":"Disarming Voice"},{"id":85,"type":"Fairy","name":"Draining Kiss"},{"id":86,"type":"Fairy","name":"Dazzling Gleam"},{"id":87,"type":"Fairy","name":"Moonblast"},{"id":88,"type":"Fairy","name":"Play Rough"},{"id":89,"type":"Poison","name":"Cross Poison"},{"id":90,"type":"Poison","name":"Sludge Bomb"},{"id":91,"type":"Poison","name":"Sludge Wave"},{"id":92,"type":"Poison","name":"Gunk Shot"},{"id":94,"type":"Ground","name":"Bone Club"},{"id":95,"type":"Ground","name":"Bulldoze"},{"id":96,"type":"Ground","name":"Mud Bomb"},{"id":99,"type":"Bug","name":"Signal Beam"},{"id":100,"type":"Bug","name":"X Scissor"},{"id":101,"type":"Fire","name":"Flame Charge"},{"id":102,"type":"Fire","name":"Flame Burst"},{"id":103,"type":"Fire","name":"Fire Blast"},{"id":104,"type":"Water","name":"Brine"},{"id":105,"type":"Water","name":"Water Pulse"},{"id":106,"type":"Water","name":"Scald"},{"id":107,"type":"Water","name":"Hydro Pump"},{"id":108,"type":"Psychic","name":"Psychic"},{"id":109,"type":"Psychic","name":"Psystrike"},{"id":111,"type":"Ice","name":"Icy Wind"},{"id":114,"type":"Grass","name":"Giga Drain"},{"id":115,"type":"Fire","name":"Fire Punch"},{"id":116,"type":"Grass","name":"Solar Beam"},{"id":117,"type":"Grass","name":"Leaf Blade"},{"id":118,"type":"Grass","name":"Power Whip"},{"id":121,"type":"Flying","name":"Air Cutter"},{"id":122,"type":"Flying","name":"Hurricane"},{"id":123,"type":"Fighting","name":"Brick Break"},{"id":125,"type":"Normal","name":"Swift"},{"id":126,"type":"Normal","name":"Horn Attack"},{"id":127,"type":"Normal","name":"Stomp"},{"id":129,"type":"Normal","name":"Hyper Fang"},{"id":131,"type":"Normal","name":"Body Slam"},{"id":132,"type":"Normal","name":"Rest"},{"id":133,"type":"Normal","name":"Struggle"},{"id":134,"type":"Water","name":"Scald Blastoise"},{"id":135,"type":"Water","name":"Hydro Pump Blastoise"},{"id":136,"type":"Normal","name":"Wrap Green"},{"id":137,"type":"Normal","name":"Wrap Pink"},{"id":200,"name":"Fury Cutter"},{"id":201,"name":"Bug Bite"},{"id":202,"name":"Bite"},{"id":203,"name":"Sucker Punch"},{"id":204,"name":"Dragon Breath"},{"id":205,"name":"Thunder Shock"},{"id":206,"name":"Spark"},{"id":207,"name":"Low Kick"},{"id":208,"name":"Karate Chop"},{"id":209,"name":"Ember"},{"id":210,"name":"Wing Attack"},{"id":211,"name":"Peck"},{"id":212,"name":"Lick"},{"id":213,"name":"Shadow Claw"},{"id":214,"name":"Vine Whip"},{"id":215,"name":"Razor Leaf"},{"id":216,"name":"Mud Shot"},{"id":217,"name":"Ice Shard"},{"id":218,"name":"Frost Breath"},{"id":219,"name":"Quick Attack"},{"id":220,"name":"Scratch"},{"id":221,"name":"Tackle"},{"id":222,"name":"Pound"},{"id":223,"name":"Cut"},{"id":224,"name":"Poison Jab"},{"id":225,"name":"Acid"},{"id":226,"name":"Psycho Cut"},{"id":227,"name":"Rock Throw"},{"id":228,"name":"Metal Claw"},{"id":229,"name":"Bullet Punch"},{"id":230,"name":"Water Gun"},{"id":231,"name":"Splash"},{"id":232,"name":"Water Gun Blastoise"},{"id":233,"name":"Mud Slap"},{"id":234,"name":"Zen Headbutt"},{"id":235,"name":"Confusion"},{"id":236,"name":"Poison Sting"},{"id":237,"name":"Bubble"},{"id":238,"name":"Feint Attack"},{"id":239,"name":"Steel Wing"},{"id":240,"name":"Fire Fang"},{"id":241,"name":"Rock Smash"}] diff --git a/examples/pogo-optimizer/data/pokemon.json b/examples/pogo-optimizer/data/pokemon.json deleted file mode 100644 index d5004ce2..00000000 --- a/examples/pogo-optimizer/data/pokemon.json +++ /dev/null @@ -1 +0,0 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","AltName":"CHARMENDER","Classification":"Lizard Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokèmon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokèmon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","AltName":"NIDORAN_FEMALE","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokèmon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","AltName":"NIDORAN_MALE","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"NidoranM candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokèmon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","AltName":"CLEFARY","Classification":"Fairy Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpi"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokèmon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokèmon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokèmon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokèmon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokèmon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","AltName":"ALAKHAZAM","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokèmon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokèmon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","AltName":"GEODUGE","Classification":"Rock Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokèmon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokèmon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokèmon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"E"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokèmon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokèmon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokèmon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokèmon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokèmon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokèmon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokèmon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokèmon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokèmon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokèmon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokèmon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] diff --git a/examples/pogo-optimizer/pogo-optimizer-cli.py b/examples/pogo-optimizer/pogo-optimizer-cli.py deleted file mode 100644 index 1e78d853..00000000 --- a/examples/pogo-optimizer/pogo-optimizer-cli.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import re -import sys -import json -import time -import struct -import pprint -import logging -import requests -import argparse -import getpass - -# add directory of this file to PATH, so that the package will be found -sys.path.append(os.path.dirname(os.path.realpath(__file__))) - -# import Pokemon Go API lib -from pgoapi import pgoapi -from pgoapi import utilities as util - -# other stuff -from google.protobuf.internal import encoder -from tabulate import tabulate -from collections import defaultdict - -log = logging.getLogger(__name__) - -def encode(cellid): - output = [] - encoder._VarintEncoder()(output.append, cellid) - return ''.join(output) - -def init_config(): - parser = argparse.ArgumentParser() - config_file = "config.json" - - # If config file exists, load variables from json - load = {} - if os.path.isfile(config_file): - with open(config_file) as data: - load.update(json.load(data)) - - # Read passed in Arguments - required = lambda x: not x in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", - required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) - parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') - parser.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - config.__dict__[key] = str(load[key]) - - if config.__dict__["password"] is None: - log.info("Secure Password Input (if there is no password prompt, use --password ):") - config.__dict__["password"] = getpass.getpass() - - if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None - - return config - -def main(): - # log settings - # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - if config.test: - return - - # instantiate pgoapi - api = pgoapi.PGoApi() - - if not api.login(config.auth_service, config.username, config.password): - return - - # get inventory call - # ---------------------- - api.get_inventory() - - # execute the RPC call - response_dict = api.call() - - approot = os.path.dirname(os.path.realpath(__file__)) - - with open(os.path.join(approot, 'data/moves.json')) as data_file: - moves = json.load(data_file) - - with open(os.path.join(approot, 'data/pokemon.json')) as data_file: - pokemon = json.load(data_file) - - def format(i): - i = i['inventory_item_data']['pokemon_data'] - i = {k: v for k, v in i.items() if k in ['nickname','move_1', 'move_2', 'pokemon_id', 'individual_defense', 'stamina', 'cp', 'individual_stamina', 'individual_attack']} - i['individual_defense'] = i.get('individual_defense', 0) - i['individual_attack'] = i.get('individual_attack', 0) - i['individual_stamina'] = i.get('individual_stamina', 0) - i['power_quotient'] = round(((float(i['individual_defense']) + float(i['individual_attack']) + float(i['individual_stamina'])) / 45) * 100) - i['name'] = list(filter(lambda j: int(j['Number']) == i['pokemon_id'], pokemon))[0]['Name'] - i['move_1'] = list(filter(lambda j: j['id'] == i['move_1'], moves))[0]['name'] - i['move_2'] = list(filter(lambda j: j['id'] == i['move_2'], moves))[0]['name'] - return i - - all_pokemon = filter(lambda i: 'pokemon_data' in i['inventory_item_data'] and 'is_egg' not in i['inventory_item_data']['pokemon_data'], response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) - all_pokemon = list(map(format, all_pokemon)) - all_pokemon.sort(key=lambda x: x['power_quotient'], reverse=True) - - print(tabulate(all_pokemon, headers = "keys")) - -if __name__ == '__main__': - main() diff --git a/examples/spiral_poi_search.py b/examples/spiral_poi_search.py deleted file mode 100755 index bc5b2277..00000000 --- a/examples/spiral_poi_search.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import re -import sys -import json -import time -import struct -import random -import logging -import requests -import argparse -import pprint - -from pgoapi import PGoApi -from pgoapi.utilities import f2i, h2f -from pgoapi import utilities as util - -from google.protobuf.internal import encoder -from geopy.geocoders import GoogleV3 -from s2sphere import Cell, CellId, LatLng - -log = logging.getLogger(__name__) - -def get_pos_by_name(location_name): - geolocator = GoogleV3() - loc = geolocator.geocode(location_name) - if not loc: - return None - - log.info('Your given location: %s', loc.address.encode('utf-8')) - log.info('lat/long/alt: %s %s %s', loc.latitude, loc.longitude, loc.altitude) - - return (loc.latitude, loc.longitude, loc.altitude) - -def get_cell_ids(lat, long, radius = 10): - origin = CellId.from_lat_lng(LatLng.from_degrees(lat, long)).parent(15) - walk = [origin.id()] - right = origin.next() - left = origin.prev() - - # Search around provided radius - for i in range(radius): - walk.append(right.id()) - walk.append(left.id()) - right = right.next() - left = left.prev() - - # Return everything - return sorted(walk) - -def encode(cellid): - output = [] - encoder._VarintEncoder()(output.append, cellid) - return ''.join(output) - -def init_config(): - parser = argparse.ArgumentParser() - config_file = "config.json" - - # If config file exists, load variables from json - load = {} - if os.path.isfile(config_file): - with open(config_file) as data: - load.update(json.load(data)) - - # Read passed in Arguments - required = lambda x: x not in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", - required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) - parser.add_argument("-p", "--password", help="Password", required=required("password")) - parser.add_argument("-l", "--location", help="Location", required=required("location")) - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') - parser.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - config.__dict__[key] = load[key] - - if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None - - return config - -def main(): - # log settings - # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - position = get_pos_by_name(config.location) - if not position: - return - - if config.test: - return - - # instantiate pgoapi - api = PGoApi() - - # provide player position on the earth - api.set_position(*position) - - if not api.login(config.auth_service, config.username, config.password): - return - - # chain subrequests (methods) into one RPC call - - # get player profile call - # ---------------------- - response_dict = api.get_player() - - # apparently new dict has binary data in it, so formatting it with this method no longer works, pprint works here but there are other alternatives - # print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) - print('Response dictionary: \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(response_dict))) - find_poi(api, position[0], position[1]) - -def find_poi(api, lat, lng): - poi = {'pokemons': {}, 'forts': []} - step_size = 0.0015 - step_limit = 49 - coords = generate_spiral(lat, lng, step_size, step_limit) - for coord in coords: - lat = coord['lat'] - lng = coord['lng'] - api.set_position(lat, lng, 0) - - - #get_cellid was buggy -> replaced through get_cell_ids from pokecli - #timestamp gets computed a different way: - cell_ids = get_cell_ids(lat, lng) - timestamps = [0,] * len(cell_ids) - response_dict = api.get_map_objects(latitude = util.f2i(lat), longitude = util.f2i(lng), since_timestamp_ms = timestamps, cell_id = cell_ids) - if (response_dict['responses']): - if 'status' in response_dict['responses']['GET_MAP_OBJECTS']: - if response_dict['responses']['GET_MAP_OBJECTS']['status'] == 1: - for map_cell in response_dict['responses']['GET_MAP_OBJECTS']['map_cells']: - if 'wild_pokemons' in map_cell: - for pokemon in map_cell['wild_pokemons']: - pokekey = get_key_from_pokemon(pokemon) - pokemon['hides_at'] = time.time() + pokemon['time_till_hidden_ms']/1000 - poi['pokemons'][pokekey] = pokemon - - # time.sleep(0.51) - # new dict, binary data - # print('POI dictionary: \n\r{}'.format(json.dumps(poi, indent=2))) - print('POI dictionary: \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(poi))) - print('Open this in a browser to see the path the spiral search took:') - print_gmaps_dbug(coords) - -def get_key_from_pokemon(pokemon): - return '{}-{}'.format(pokemon['spawn_point_id'], pokemon['pokemon_data']['pokemon_id']) - -def print_gmaps_dbug(coords): - url_string = 'http://maps.googleapis.com/maps/api/staticmap?size=400x400&path=' - for coord in coords: - url_string += '{},{}|'.format(coord['lat'], coord['lng']) - print(url_string[:-1]) - -def generate_spiral(starting_lat, starting_lng, step_size, step_limit): - coords = [{'lat': starting_lat, 'lng': starting_lng}] - steps,x,y,d,m = 1, 0, 0, 1, 1 - rlow = 0.0 - rhigh = 0.0005 - - while steps < step_limit: - while 2 * x * d < m and steps < step_limit: - x = x + d - steps += 1 - lat = x * step_size + starting_lat + random.uniform(rlow, rhigh) - lng = y * step_size + starting_lng + random.uniform(rlow, rhigh) - coords.append({'lat': lat, 'lng': lng}) - while 2 * y * d < m and steps < step_limit: - y = y + d - steps += 1 - lat = x * step_size + starting_lat + random.uniform(rlow, rhigh) - lng = y * step_size + starting_lng + random.uniform(rlow, rhigh) - coords.append({'lat': lat, 'lng': lng}) - - d = -1 * d - m = m + 1 - return coords - -if __name__ == '__main__': - main() diff --git a/pgoapi/auth.py b/pgoapi/auth.py index 38fe30e8..1dd0103c 100755 --- a/pgoapi/auth.py +++ b/pgoapi/auth.py @@ -29,8 +29,8 @@ from pgoapi.utilities import get_time, get_format_time_diff -class Auth: +class Auth: def __init__(self): self.log = logging.getLogger(__name__) @@ -38,21 +38,19 @@ def __init__(self): self._login = False - """ - oauth2 uses refresh tokens (which basically never expires) + """ + oauth2 uses refresh tokens (which basically never expires) to get an access_token which is only valid for a certain time) """ self._refresh_token = None self._access_token = None self._access_token_expiry = 0 - # TODO: can be removed - self._auth_token = None - """ - Pokemon Go uses internal tickets, like an internal + """ + Pokemon Go uses internal tickets, like an internal session to keep a user logged in over a certain time (30 minutes) """ - self._ticket_expire = None + self._ticket_expire = 0 self._ticket_start = None self._ticket_end = None @@ -66,39 +64,34 @@ def get_token(self): return self._access_token def has_ticket(self): - if self._ticket_expire and self._ticket_start and self._ticket_end: - return True - else: - return False + return (self._ticket_expire and self._ticket_start and self._ticket_end) def set_ticket(self, params): self._ticket_expire, self._ticket_start, self._ticket_end = params def is_new_ticket(self, new_ticket_time_ms): - if self._ticket_expire is None or new_ticket_time_ms > self._ticket_expire: - return True - else: - return False + return (not self._ticket_expire or new_ticket_time_ms > self._ticket_expire) def check_ticket(self): - if self.has_ticket(): - now_ms = get_time(ms = True) - if now_ms < (self._ticket_expire - 10000): - h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) - self.log.debug('Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) - return True - else: - self.log.debug('Removed expired Session Ticket (%s < %s)', now_ms, self._ticket_expire) - self._ticket_expire, self._ticket_start, self._ticket_end = (None, None, None) - return False - else: + if not self.has_ticket(): return False + now_ms = get_time(ms=True) + if now_ms < (self._ticket_expire + 10000): + h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) + self.log.debug( + 'Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) + return True + + self.log.debug('Removed expired Session Ticket (%s < %s)', + now_ms, self._ticket_expire) + self._ticket_expire, self._ticket_start, self._ticket_end = ( + 0, None, None) + return False def get_ticket(self): if self.check_ticket(): return (self._ticket_expire, self._ticket_start, self._ticket_end) - else: - return False + return False def user_login(self, username, password): raise NotImplementedError() @@ -106,28 +99,44 @@ def user_login(self, username, password): def set_refresh_token(self, username, password): raise NotImplementedError() - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): raise NotImplementedError() - def check_access_token(self): - """ - Add few seconds to now so the token get refreshed - before it invalidates in the middle of the request - """ - now_s = get_time() + 120 - - if self._access_token is not None: - if self._access_token_expiry == 0: - self.log.debug('No Access Token Expiry found - assuming it is still valid!') - return True - elif self._access_token_expiry > now_s: - h, m, s = get_format_time_diff(now_s, self._access_token_expiry, False) - self.log.debug('Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_s, self._access_token_expiry) - return True - else: - self.log.info('Access Token expired!') - return False - else: + if self._access_token is None: self.log.debug('No Access Token available!') - return False \ No newline at end of file + return False + + now_s = get_time() + if self._access_token_expiry == 0: + self.log.debug( + 'No Access Token Expiry found - assuming it is still valid!') + return True + elif self._access_token_expiry > now_s: + h, m, s = get_format_time_diff( + now_s, self._access_token_expiry, False) + self.log.debug( + 'Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_s, self._access_token_expiry) + return True + + self.log.info('Access Token expired!') + return False + + def check_authentication(self, expire_timestamp_ms, start, end): + if self.is_new_ticket(expire_timestamp_ms): + + had_ticket = self.has_ticket() + self.set_ticket([expire_timestamp_ms, start, end]) + + now_ms = get_time(ms=True) + h, m, s = get_format_time_diff(now_ms, expire_timestamp_ms, True) + + if had_ticket: + self.log.debug( + 'Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) + else: + self.log.debug( + 'Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) diff --git a/pgoapi/auth_google.py b/pgoapi/auth_google.py index dab31f5d..c90b4660 100755 --- a/pgoapi/auth_google.py +++ b/pgoapi/auth_google.py @@ -25,17 +25,16 @@ from __future__ import absolute_import -import logging - from pgoapi.auth import Auth from pgoapi.exceptions import AuthException, InvalidCredentialsException, AuthGoogleTwoFactorRequiredException from gpsoauth import perform_master_login, perform_oauth from six import string_types + class AuthGoogle(Auth): GOOGLE_LOGIN_ANDROID_ID = '9774d56d682e549c' - GOOGLE_LOGIN_SERVICE= 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' + GOOGLE_LOGIN_SERVICE = 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' GOOGLE_LOGIN_APP = 'com.nianticlabs.pokemongo' GOOGLE_LOGIN_CLIENT_SIG = '321187995bc7cdc2b5fc91b11a96e2baa8602c62' @@ -52,13 +51,20 @@ def set_proxy(self, proxy_config): def user_login(self, username, password): self.log.info('Google User Login for: {}'.format(username)) - if not isinstance(username, string_types) or not isinstance(password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(username, string_types) or not isinstance( + password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") - user_login = perform_master_login(username, password, self.GOOGLE_LOGIN_ANDROID_ID, proxy=self._proxy) + user_login = perform_master_login( + username, + password, + self.GOOGLE_LOGIN_ANDROID_ID, + proxy=self._proxy) if user_login and user_login.get('Error', None) == 'NeedsBrowser': - raise AuthGoogleTwoFactorRequiredException(user_login['Url'], user_login['ErrorDetail']) + raise AuthGoogleTwoFactorRequiredException( + user_login['Url'], user_login['ErrorDetail']) try: refresh_token = user_login.get('Token', None) @@ -79,7 +85,7 @@ def set_refresh_token(self, refresh_token): self.log.info('Google Refresh Token provided by user') self._refresh_token = refresh_token - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): token_validity = self.check_access_token() if token_validity is True and force_refresh is False: @@ -91,8 +97,14 @@ def get_access_token(self, force_refresh = False): else: self.log.info('Request Google Access Token...') - token_data = perform_oauth(None, self._refresh_token, self.GOOGLE_LOGIN_ANDROID_ID, self.GOOGLE_LOGIN_SERVICE, self.GOOGLE_LOGIN_APP, - self.GOOGLE_LOGIN_CLIENT_SIG, proxy=self._proxy) + token_data = perform_oauth( + None, + self._refresh_token, + self.GOOGLE_LOGIN_ANDROID_ID, + self.GOOGLE_LOGIN_SERVICE, + self.GOOGLE_LOGIN_APP, + self.GOOGLE_LOGIN_CLIENT_SIG, + proxy=self._proxy) access_token = token_data.get('Auth', None) if access_token is not None: @@ -101,7 +113,8 @@ def get_access_token(self, force_refresh = False): self._login = True self.log.info('Google Access Token successfully received.') - self.log.debug('Google Access Token: %s...', self._access_token[:25]) + self.log.debug('Google Access Token: %s...', + self._access_token[:25]) return self._access_token else: self._access_token = None diff --git a/pgoapi/auth_ptc.py b/pgoapi/auth_ptc.py index afae6c87..da4b4ff1 100755 --- a/pgoapi/auth_ptc.py +++ b/pgoapi/auth_ptc.py @@ -24,149 +24,182 @@ """ from __future__ import absolute_import -from future.standard_library import install_aliases -install_aliases() import requests - -from urllib.parse import parse_qs, urlsplit from six import string_types from pgoapi.auth import Auth from pgoapi.utilities import get_time from pgoapi.exceptions import AuthException, AuthTimeoutException, InvalidCredentialsException +from requests.exceptions import RequestException, Timeout, ProxyError, SSLError, ConnectionError -from requests.exceptions import RequestException, Timeout class AuthPtc(Auth): - PTC_LOGIN_URL1 = 'https://sso.pokemon.com/sso/oauth2.0/authorize?client_id=mobile-app_pokemon-go&redirect_uri=https%3A%2F%2Fwww.nianticlabs.com%2Fpokemongo%2Ferror' - PTC_LOGIN_URL2 = 'https://sso.pokemon.com/sso/login?service=http%3A%2F%2Fsso.pokemon.com%2Fsso%2Foauth2.0%2FcallbackAuthorize' - PTC_LOGIN_OAUTH = 'https://sso.pokemon.com/sso/oauth2.0/accessToken' - PTC_LOGIN_CLIENT_SECRET = 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR' - - def __init__(self, username=None, password=None, user_agent=None, timeout=None): + def __init__(self, + username=None, + password=None, + user_agent=None, + timeout=None, + locale=None): Auth.__init__(self) self._auth_provider = 'ptc' - - self._session = requests.session() - self._session.headers = {'User-Agent': user_agent or 'pokemongo/1 CFNetwork/811.4.18 Darwin/16.5.0', 'Host': 'sso.pokemon.com', 'X-Unity-Version': '2017.1.2f1'} self._username = username self._password = password - self.timeout = timeout or 15 + self.timeout = timeout or 10 + self.locale = locale or 'en_US' + self.proxies = None + self.user_agent = user_agent or 'pokemongo/0 CFNetwork/893.14.2 Darwin/17.3.0' def set_proxy(self, proxy_config): - self._session.proxies = proxy_config - - def user_login(self, username=None, password=None, retry=True): + self.proxies = proxy_config + + def get_session(self): + session = requests.session() + session.headers = { + 'Host': 'sso.pokemon.com', + 'Accept': '*/*', + 'Connection': 'keep-alive', + 'User-Agent': self.user_agent, + 'Accept-Language': self.locale.lower().replace('_', '-'), + 'Accept-Encoding': 'br, gzip, deflate', + 'X-Unity-Version': '2017.1.2f1' + } + if self.proxies: + session.proxies = self.proxies + return session + + def user_login(self, username=None, password=None): self._username = username or self._username self._password = password or self._password - if not isinstance(self._username, string_types) or not isinstance(self._password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(self._username, string_types) or not isinstance( + self._password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") self.log.info('PTC User Login for: {}'.format(self._username)) - self._session.cookies.clear() - now = get_time() + session = self.get_session() + session.cookies.clear() try: - r = self._session.get(self.PTC_LOGIN_URL1, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth GET timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) + now = get_time() - try: - data = r.json() + logout_params = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize' + } + r = session.get( + 'https://sso.pokemon.com/sso/logout', + params=logout_params, + timeout=self.timeout, + allow_redirects=False) + r.close() + + login_params_get = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + r = session.get( + 'https://sso.pokemon.com/sso/login', + params=login_params_get, + timeout=self.timeout) + + data = r.json(encoding='utf-8') + + assert 'lt' in data data.update({ '_eventId': 'submit', 'username': self._username, - 'password': self._password, + 'password': self._password }) - except (ValueError, AttributeError) as e: - self.log.error('PTC User Login Error - invalid JSON response: {}'.format(e)) - raise AuthException('Invalid JSON response: {}'.format(e)) - try: - r = self._session.post(self.PTC_LOGIN_URL2, data=data, timeout=self.timeout, allow_redirects=False) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') + login_params_post = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + login_headers_post = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = session.post( + 'https://sso.pokemon.com/sso/login', + params=login_params_post, + headers=login_headers_post, + data=data, + timeout=self.timeout, + allow_redirects=False) + + try: + self._access_token = session.cookies['CASTGC'] + except (AttributeError, KeyError, TypeError): + try: + j = r.json(encoding='utf-8') + except ValueError as e: + raise AuthException('Unable to decode second response: {}'.format(e)) + try: + if j.get('error_code') == 'users.login.activation_required': + raise AuthException('Account email not verified.') + raise AuthException(j['errors'][0]) + except (AttributeError, IndexError, KeyError, TypeError) as e: + raise AuthException('Unable to login or get error information: {}'.format(e)) + + token_data = { + 'client_id': 'mobile-app_pokemon-go', + 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', + 'client_secret': 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR', + 'grant_type': 'refresh_token', + 'code': r.headers['Location'].split("ticket=")[1] + } + token_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = session.post( + 'https://sso.pokemon.com/sso/oauth2.0/accessToken', + headers=token_headers, + data=token_data, + timeout=self.timeout) + r.close() + + profile_data = { + 'access_token': self._access_token, + 'client_id': 'mobile-app_pokemon-go', + 'locale': self.locale + } + profile_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = session.post( + 'https://sso.pokemon.com/sso/oauth2.0/profile', + headers=profile_headers, + data=profile_data, + timeout=self.timeout) + r.close() + + except (ProxyError, SSLError, ConnectionError) as e: + raise AuthException('Proxy connection error during user_login: {}'.format(e)) + except Timeout as e: + raise AuthTimeoutException('user_login timeout') except RequestException as e: raise AuthException('Caught RequestException: {}'.format(e)) + except (AssertionError, TypeError, ValueError) as e: + raise AuthException('Invalid initial JSON response: {}'.format(e)) - try: - qs = parse_qs(urlsplit(r.headers['Location'])[3]) - self._refresh_token = qs.get('ticket')[0] - except Exception as e: - raise AuthException('Could not retrieve token! {}'.format(e)) - - self._access_token = self._session.cookies.get('CASTGC') if self._access_token: self._login = True - self._access_token_expiry = int(now) + 7200 + self._access_token_expiry = now + 7195.0 self.log.info('PTC User Login successful.') - elif self._refresh_token and retry: - self.get_access_token() - else: - self._login = False - raise AuthException("Could not retrieve a PTC Access Token") - return self._login + return self._login - def set_refresh_token(self, refresh_token): - self.log.info('PTC Refresh Token provided by user') - self._refresh_token = refresh_token + self._login = False + raise AuthException("Could not retrieve a PTC Access Token") - def get_access_token(self, force_refresh=False): - token_validity = self.check_access_token() - if token_validity is True and force_refresh is False: + def get_access_token(self, force_refresh=False): + if not force_refresh and self.check_access_token(): self.log.debug('Using cached PTC Access Token') return self._access_token - else: - if force_refresh: - self.log.info('Forced request of PTC Access Token!') - else: - self.log.info('Request PTC Access Token...') - - data = { - 'client_id': 'mobile-app_pokemon-go', - 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', - 'client_secret': self.PTC_LOGIN_CLIENT_SECRET, - 'grant_type': 'refresh_token', - 'code': self._refresh_token, - } - try: - r = self._session.post(self.PTC_LOGIN_OAUTH, data=data, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) - - token_data = parse_qs(r.text) - - access_token = token_data.get('access_token') - if access_token is not None: - self._access_token = access_token[0] - - # set expiration to an hour less than value received because Pokemon OAuth - # login servers return an access token with an explicit expiry time of - # three hours, however, the token stops being valid after two hours. - # See issue #86 - expires = int(token_data.get('expires', [0])[0]) - 3600 - if expires > 0: - self._access_token_expiry = expires + get_time() - else: - self._access_token_expiry = 0 - - self._login = True - - self.log.info('PTC Access Token successfully retrieved.') - self.log.debug('PTC Access Token: {}'.format(self._access_token)) - else: - self._access_token = None - self._login = False - if force_refresh: - self.log.info('Reauthenticating with refresh token failed, using credentials instead.') - return self.user_login(retry=False) - raise AuthException("Could not retrieve a PTC Access Token") + self._access_token = None + self._ticket_expire = 0 + self._login = False + self.user_login() + return self._access_token diff --git a/pgoapi/exceptions.py b/pgoapi/exceptions.py index af432a65..8b0edee2 100755 --- a/pgoapi/exceptions.py +++ b/pgoapi/exceptions.py @@ -27,9 +27,11 @@ class PgoapiError(Exception): """Any custom exception in this module""" + class HashServerException(PgoapiError): """Parent class of all hashing server errors""" + class TimeoutException(PgoapiError): """Raised when a request times out.""" @@ -37,9 +39,11 @@ class TimeoutException(PgoapiError): class AuthException(PgoapiError): """Raised when logging in fails""" + class AuthTimeoutException(AuthException, TimeoutException): """Raised when an auth request times out.""" + class InvalidCredentialsException(AuthException, ValueError): """Raised when the username, password, or provider are empty/invalid""" @@ -47,6 +51,7 @@ class InvalidCredentialsException(AuthException, ValueError): class AuthTokenExpiredException(PgoapiError): """Raised when your auth token has expired (code 102)""" + class AuthGoogleTwoFactorRequiredException(Exception): def __init__(self, redirectUrl, message): self.redirectUrl = redirectUrl @@ -59,6 +64,7 @@ def __str__(self): class BadRequestException(PgoapiError): """Raised when HTTP code 400 is returned""" + class BadHashRequestException(BadRequestException): """Raised when hashing server returns code 400""" @@ -70,10 +76,13 @@ class BannedAccountException(PgoapiError): class MalformedResponseException(PgoapiError): """Raised when the response is empty or not in an expected format""" + class MalformedNianticResponseException(PgoapiError): """Raised when a Niantic response is empty or not in an expected format""" -class MalformedHashResponseException(MalformedResponseException, HashServerException): + +class MalformedHashResponseException(MalformedResponseException, + HashServerException): """Raised when the response from the hash server cannot be parsed.""" @@ -88,15 +97,20 @@ class NotLoggedInException(PgoapiError): class ServerBusyOrOfflineException(PgoapiError): """Raised when unable to establish a connection with a server""" + class NianticOfflineException(ServerBusyOrOfflineException): """Raised when unable to establish a conection with Niantic""" + class NianticTimeoutException(NianticOfflineException, TimeoutException): """Raised when an RPC request times out.""" -class HashingOfflineException(ServerBusyOrOfflineException, HashServerException): + +class HashingOfflineException(ServerBusyOrOfflineException, + HashServerException): """Raised when unable to establish a conection with the hashing server""" + class HashingTimeoutException(HashingOfflineException, TimeoutException): """Raised when a request to the hashing server times out.""" @@ -112,12 +126,16 @@ class PleaseInstallProtobufVersion3(PgoapiError): class ServerSideAccessForbiddenException(PgoapiError): """Raised when access to a server is forbidden""" + class NianticIPBannedException(ServerSideAccessForbiddenException): """Raised when Niantic returns a 403, meaning your IP is probably banned""" -class HashingForbiddenException(ServerSideAccessForbiddenException, HashServerException): + +class HashingForbiddenException(ServerSideAccessForbiddenException, + HashServerException): """Raised when the hashing server returns 403""" + class TempHashingBanException(HashingForbiddenException): """Raised when your IP is temporarily banned for sending too many requests with invalid keys.""" @@ -125,22 +143,28 @@ class TempHashingBanException(HashingForbiddenException): class ServerSideRequestThrottlingException(PgoapiError): """Raised when too many requests were made in a short period""" + class NianticThrottlingException(ServerSideRequestThrottlingException): """Raised when too many requests to Niantic were made in a short period""" -class HashingQuotaExceededException(ServerSideRequestThrottlingException, HashServerException): + +class HashingQuotaExceededException(ServerSideRequestThrottlingException, + HashServerException): """Raised when you exceed your hashing server quota""" class UnexpectedResponseException(PgoapiError): """Raised when an unhandled HTTP status code is received""" -class UnexpectedHashResponseException(UnexpectedResponseException, HashServerException): + +class UnexpectedHashResponseException(UnexpectedResponseException, + HashServerException): """Raised when an unhandled HTTP code is received from the hash server""" class ServerApiEndpointRedirectException(PgoapiError): """Raised when the API redirects you to another endpoint""" + def __init__(self): self._api_endpoint = None diff --git a/pgoapi/hash_engine.py b/pgoapi/hash_engine.py deleted file mode 100644 index e68e8881..00000000 --- a/pgoapi/hash_engine.py +++ /dev/null @@ -1,15 +0,0 @@ -class HashEngine: - def __init__(self): - self.location_hash = None - self.location_auth_hash = None - self.request_hashes = [] - - def hash(self, timestamp, latitude, longitude, altitude, authticket, sessiondata, requests): - raise NotImplementedError() - - def get_location_hash(self): - return self.location_hash - def get_location_auth_hash(self): - return self.location_auth_hash - def get_request_hashes(self): - return self.request_hashes diff --git a/pgoapi/hash_server.py b/pgoapi/hash_server.py index 9bafca41..38a65d32 100644 --- a/pgoapi/hash_server.py +++ b/pgoapi/hash_server.py @@ -6,54 +6,74 @@ from struct import pack, unpack -from pgoapi.hash_engine import HashEngine from pgoapi.exceptions import BadHashRequestException, HashingOfflineException, HashingQuotaExceededException, HashingTimeoutException, MalformedHashResponseException, NoHashKeyException, TempHashingBanException, UnexpectedHashResponseException -class HashServer(HashEngine): + +class HashServer: _session = requests.session() - _adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) + _adapter = requests.adapters.HTTPAdapter(pool_maxsize=500, pool_block=True) + _session.mount('http://', _adapter) _session.mount('https://', _adapter) _session.verify = True - _session.headers.update({'User-Agent': 'Python pgoapi @pogodev'}) - endpoint = "https://pokehash.buddyauth.com/api/v157_5/hash" - status = {} + _endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' + _headers = { + 'User-Agent': 'Python pgoapi @pogodev', + 'content-type': 'application/json', + 'Accept': 'application/json', + 'X-MaxRPMCount': '32000' + } + + @staticmethod + def hash(timestamp, latitude, longitude, accuracy, authticket, + sessiondata, requestslist, token): - def __init__(self, auth_token): - if not auth_token: + if not token: raise NoHashKeyException('Token not provided for hashing server.') - self.headers = {'content-type': 'application/json', 'Accept' : 'application/json', 'X-AuthToken' : auth_token} - def hash(self, timestamp, latitude, longitude, accuracy, authticket, sessiondata, requestslist): - self.location_hash = None - self.location_auth_hash = None - self.request_hashes = [] + headers = HashServer._headers.copy() + headers['X-AuthToken'] = token payload = { - 'Timestamp': timestamp, - 'Latitude64': unpack('>31))<<32)|cnt - self.log.debug("Incremented RPC Request ID: %s", reqid) - - return reqid - - def decode_raw(self, raw): +class RpcApi: + log = logging.getLogger(__name__) + _session = None + + @staticmethod + def create_session(): + session = requests.session() + adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) + # proxies use the adapter by it's own url not endpoint so all 3 are needed + session.mount('http://', adapter) + session.mount('https://', adapter) + session.mount('socks5://', adapter) + + # requests' Session calls .default_headers() in init, which + # makes it set a bunch of default headers, including + # 'Connection': 'keep-alive', so we overwrite all of them. + session.headers = { + 'User-Agent': 'Niantic App', + 'Content-Type': 'application/binary', + 'Accept-Encoding': 'identity, gzip' + } + session.verify = True + return session + + @staticmethod + def get_session(state): + if state.session: + return state.session + if not RpcApi._session: + RpcApi._session = RpcApi.create_session() + return RpcApi._session + + @staticmethod + def decode_raw(raw): output = error = None try: - process = subprocess.Popen(['protoc', '--decode_raw'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + process = subprocess.Popen( + ['protoc', '--decode_raw'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) output, error = process.communicate(raw) except (subprocess.SubprocessError, OSError): output = "Couldn't find protoc in your environment OR other issue..." return output - def get_class(self, cls): + @staticmethod + def get_class(cls): module_, class_ = cls.rsplit('.', 1) class_ = getattr(import_module(module_), to_camel_case(class_)) return class_ - def _make_rpc(self, endpoint, request_proto_plain): - self.log.debug('Execution of RPC') + @staticmethod + def _make_rpc(endpoint, request_proto_plain, state, proxies): + RpcApi.log.debug('Execution of RPC') request_proto_serialized = request_proto_plain.SerializeToString() try: - http_response = self._session.post(endpoint, data=request_proto_serialized, timeout=30) + # adapter = RpcApi.get_session(state).adapters['https://'] + # RpcApi.log.error(adapter.poolmanager.connection_from_url(endpoint).num_connections) + http_response = RpcApi.get_session(state).post( + endpoint, data=request_proto_serialized, timeout=30, + proxies=proxies) except requests.exceptions.Timeout: raise NianticTimeoutException('RPC request timed out.') except requests.exceptions.ConnectionError as e: @@ -124,204 +122,250 @@ def _make_rpc(self, endpoint, request_proto_plain): return http_response - def request(self, endpoint, subrequests, platforms, player_position, use_dict = True): + @staticmethod + def request(endpoint, + subrequests, + platforms, + player_position, + state, + hash_key, + proxies): - if not self._auth_provider or self._auth_provider.is_login() is False: + if not state.auth_provider or state.auth_provider.is_login() is False: raise NotLoggedInException() - self.request_proto = self.request_proto or self._build_main_request(subrequests, platforms, player_position) - response = self._make_rpc(endpoint, self.request_proto) + (request, hash_headers) = RpcApi._build_main_request( + subrequests, platforms, player_position, state, hash_key) + response = RpcApi._make_rpc(endpoint, request, state, proxies) - response_dict = self._parse_main_response(response, subrequests, use_dict) + response_dict = RpcApi._parse_main_response(response, subrequests) # some response validations - if isinstance(response_dict, dict): - if use_dict: - status_code = response_dict.get('status_code') - if ('auth_ticket' in response_dict) and ('expire_timestamp_ms' in response_dict['auth_ticket']): - ticket = response_dict['auth_ticket'] - self.check_authentication(ticket['expire_timestamp_ms'], ticket['start'], ticket['end']) + status_code = response_dict['envelope'].status_code + ticket = response_dict['envelope'].auth_ticket + if ticket: + state.auth_provider.check_authentication( + ticket.expire_timestamp_ms, ticket.start, ticket.end) + + if status_code == 102: + raise AuthTokenExpiredException + elif status_code == 52: + raise NianticThrottlingException( + "Request throttled by server... slow down man") + elif status_code == 53: + api_url = response_dict.get('api_url') + if api_url: + exception = ServerApiEndpointRedirectException() + exception.set_redirected_endpoint(api_url) + raise exception else: - status_code = response_dict['envelope'].status_code - ticket = response_dict['envelope'].auth_ticket - if ticket: - self.check_authentication(ticket.expire_timestamp_ms, ticket.start, ticket.end) - - if status_code == 102: - raise AuthTokenExpiredException - elif status_code == 52: - raise NianticThrottlingException("Request throttled by server... slow down man") - elif status_code == 53: - api_url = response_dict.get('api_url') - if api_url: - exception = ServerApiEndpointRedirectException() - exception.set_redirected_endpoint(api_url) - raise exception - else: - raise UnexpectedResponseException + raise UnexpectedResponseException + # add hash headers + response_dict['hash_headers'] = hash_headers return response_dict - def check_authentication(self, expire_timestamp_ms, start, end): - if self._auth_provider.is_new_ticket(expire_timestamp_ms): - - had_ticket = self._auth_provider.has_ticket() - self._auth_provider.set_ticket([expire_timestamp_ms, start, end]) - - now_ms = get_time(ms=True) - h, m, s = get_format_time_diff(now_ms, expire_timestamp_ms, True) - - if had_ticket: - self.log.debug('Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) - else: - self.log.debug('Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) - - def _build_main_request(self, subrequests, platforms, player_position=None): - self.log.debug('Generating main RPC request...') + @staticmethod + def _build_main_request(subrequests, + platforms, + player_position, + state, + hash_key): + RpcApi.log.debug('Generating main RPC request...') request = RequestEnvelope() request.status_code = 2 - request.request_id = self.get_rpc_id() - request.accuracy = random.choice((5, 5, 5, 5, 10, 10, 10, 30, 30, 50, 65, random.uniform(66,80))) + request.request_id = state.get_next_request_id() + RpcApi.log.debug('RPC Request ID: %s.', request.request_id) + # 5: 43%, 10: 30%, 30: 5%, 50: 4%, 65: 10%, 200: 1%, float: 7% + request.accuracy = weighted_choice([ + (5, 43), + (10, 30), + (30, 5), + (50, 4), + (65, 10), + (200, 1), + (random.uniform(65, 200), 7) + ]) if player_position: request.latitude, request.longitude, altitude = player_position # generate sub requests before Signature generation - request = self._build_sub_requests(request, subrequests) - request = self._build_platform_requests(request, platforms) + request = RpcApi._build_sub_requests(request, subrequests) + request = RpcApi._build_platform_requests(request, platforms) - ticket = self._auth_provider.get_ticket() + ticket = state.auth_provider.get_ticket() if ticket: - self.log.debug('Found Session Ticket - using this instead of oauth token') + RpcApi.log.debug( + 'Found Session Ticket - using this instead of oauth token') request.auth_ticket.expire_timestamp_ms, request.auth_ticket.start, request.auth_ticket.end = ticket ticket_serialized = request.auth_ticket.SerializeToString() else: - self.log.debug('No Session Ticket found - using OAUTH Access Token') - request.auth_info.provider = self._auth_provider.get_name() - request.auth_info.token.contents = self._auth_provider.get_access_token() - request.auth_info.token.unknown2 = self.token2 - ticket_serialized = request.auth_info.SerializeToString() #Sig uses this when no auth_ticket available + RpcApi.log.debug( + 'No Session Ticket found - using OAUTH Access Token') + auth_provider = state.auth_provider + request.auth_info.provider = auth_provider.get_name() + request.auth_info.token.contents = auth_provider.get_access_token() + request.auth_info.token.unknown2 = state.token2 + # Sig uses this when no auth_ticket available. + ticket_serialized = request.auth_info.SerializeToString() sig = Signature() - sig.session_hash = self.session_hash + sig.session_hash = state.session_hash sig.timestamp = get_time(ms=True) - sig.timestamp_since_start = get_time(ms=True) - RpcApi.START_TIME - if sig.timestamp_since_start < 5000: - sig.timestamp_since_start = random.randint(5000, 8000) + sig.timestamp_since_start = get_time(ms=True) - state.start_time - self._hash_engine.hash(sig.timestamp, request.latitude, request.longitude, request.accuracy, ticket_serialized, sig.session_hash, request.requests) - sig.location_hash1 = self._hash_engine.get_location_auth_hash() - sig.location_hash2 = self._hash_engine.get_location_hash() - for req_hash in self._hash_engine.get_request_hashes(): - sig.request_hash.append(ctypes.c_uint64(req_hash).value) + (sig.location_hash2, sig.location_hash1, request_hash, headers) = HashServer.hash( + sig.timestamp, request.latitude, request.longitude, + request.accuracy, ticket_serialized, sig.session_hash, + request.requests, hash_key) + sig.request_hash.extend(request_hash) loc = sig.location_fix.add() sen = sig.sensor_info.add() - sen.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 100) - loc.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 1000) + sen.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(93, 4900, 3000)) + loc.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(320, 3000, 1000)) - loc.provider = random.choice(('network', 'network', 'network', 'network', 'fused')) + loc.provider = 'fused' loc.latitude = request.latitude loc.longitude = request.longitude - loc.altitude = altitude or random.triangular(300, 400, 350) + loc.altitude = altitude or random.uniform(150, 250) - if random.random() > .95: - # no reading for roughly 1 in 20 updates + if random.random() > .85: + # no reading for roughly 1 in 7 updates loc.course = -1 loc.speed = -1 else: - self.course = random.triangular(0, 360, self.course) - loc.course = self.course - loc.speed = random.triangular(0.2, 4.25, 1) + loc.course = state.course + loc.speed = random.triangular(0.25, 9.7, 8.2) loc.provider_status = 3 loc.location_type = 1 - if request.accuracy >= 65: - loc.vertical_accuracy = random.triangular(35, 100, 65) - loc.horizontal_accuracy = random.choice((request.accuracy, 65, 65, random.uniform(66,80), 200)) + if isinstance(request.accuracy, float): + loc.horizontal_accuracy = weighted_choice([ + (request.accuracy, 50), + (65, 40), + (200, 10) + ]) + loc.vertical_accuracy = weighted_choice([ + (random.uniform(10, 96), 50), + (10, 34), + (12, 5), + (16, 3), + (24, 4), + (32, 2), + (48, 1), + (96, 1) + ]) else: - if request.accuracy > 10: - loc.vertical_accuracy = random.choice((24, 32, 48, 48, 64, 64, 96, 128)) - else: - loc.vertical_accuracy = random.choice((3, 4, 6, 6, 8, 12, 24)) loc.horizontal_accuracy = request.accuracy - - sen.linear_acceleration_x = random.triangular(-3, 1, 0) - sen.linear_acceleration_y = random.triangular(-2, 3, 0) - sen.linear_acceleration_z = random.triangular(-4, 2, 0) - sen.magnetic_field_x = random.triangular(-50, 50, 0) - sen.magnetic_field_y = random.triangular(-60, 50, -5) - sen.magnetic_field_z = random.triangular(-60, 40, -30) - sen.magnetic_field_accuracy = random.choice((-1, 1, 1, 2, 2, 2, 2)) - sen.attitude_pitch = random.triangular(-1.5, 1.5, 0.2) - sen.attitude_yaw = random.uniform(-3, 3) - sen.attitude_roll = random.triangular(-2.8, 2.5, 0.25) - sen.rotation_rate_x = random.triangular(-6, 4, 0) - sen.rotation_rate_y = random.triangular(-5.5, 5, 0) - sen.rotation_rate_z = random.triangular(-5, 3, 0) - sen.gravity_x = random.triangular(-1, 1, 0.15) - sen.gravity_y = random.triangular(-1, 1, -.2) - sen.gravity_z = random.triangular(-1, .7, -0.8) + if request.accuracy >= 10: + loc.vertical_accuracy = weighted_choice([ + (6, 4), + (8, 34), + (10, 35), + (12, 11), + (16, 4), + (24, 8), + (32, 3), + (48, 1) + ]) + else: + loc.vertical_accuracy = weighted_choice([ + (3, 15), + (4, 39), + (6, 14), + (8, 13), + (10, 14), + (12, 5) + ]) + + sen.magnetic_field_accuracy = weighted_choice([ + (-1, 8), + (0, 2), + (1, 42), + (2, 48) + ]) + if sen.magnetic_field_accuracy == -1: + sen.magnetic_field_x = 0 + sen.magnetic_field_y = 0 + sen.magnetic_field_z = 0 + else: + sen.magnetic_field_x = state.magnetic_field_x + sen.magnetic_field_y = state.magnetic_field_y + sen.magnetic_field_z = state.magnetic_field_z + + sen.linear_acceleration_x = random.triangular(-1.5, 2.5, 0) + sen.linear_acceleration_y = random.triangular(-1.2, 1.4, 0) + sen.linear_acceleration_z = random.triangular(-1.4, .9, 0) + sen.attitude_pitch = random.triangular(-1.56, 1.57, 0.475) + sen.attitude_yaw = random.triangular(-1.56, 3.14, .1) + sen.attitude_roll = random.triangular(-3.14, 3.14, 0) + sen.rotation_rate_x = random.triangular(-3.2, 3.52, 0) + sen.rotation_rate_y = random.triangular(-3.1, 4.88, 0) + sen.rotation_rate_z = random.triangular(-6, 3.7, 0) + sen.gravity_x = random.triangular(-1, 1, 0.01) + sen.gravity_y = random.triangular(-1, 1, -.4) + sen.gravity_z = random.triangular(-1, 1, -.4) sen.status = 3 sig.unknown25 = 4500779412463383546 - - if self.device_info: - for key in self.device_info: - setattr(sig.device_info, key, self.device_info[key]) - if self.device_info['device_brand'] == 'Apple': - sig.activity_status.stationary = True - else: - sig.activity_status.stationary = True + for key in state.device_info: + setattr(sig.device_info, key, state.device_info[key]) + sig.activity_status.stationary = True signature_proto = sig.SerializeToString() - if self._needsPtr8(subrequests): + if RpcApi._needsPtr8(subrequests): plat_eight = UnknownPtr8Request() plat_eight.message = '15c79df0558009a4242518d2ab65de2a59e09499' plat8 = request.platform_requests.add() plat8.type = 8 plat8.request_message = plat_eight.SerializeToString() - + sig_request = SendEncryptedSignatureRequest() - sig_request.encrypted_signature = pycrypt(signature_proto, sig.timestamp_since_start) + sig_request.encrypted_signature = pycrypt(signature_proto, + sig.timestamp_since_start) plat = request.platform_requests.add() plat.type = 6 plat.request_message = sig_request.SerializeToString() - request.ms_since_last_locationfix = int(random.triangular(300, 30000, 10000)) + request.ms_since_last_locationfix = sig.timestamp_since_start - loc.timestamp_snapshot - self.log.debug('Generated protobuf request: \n\r%s', request) + RpcApi.log.debug('Generated protobuf request: \n\r%s', request) - return request + return (request, headers) - def _needsPtr8(self, requests): + @staticmethod + def _needsPtr8(requests): if len(requests) == 0: return False randval = random.uniform(0, 1) rtype, _ = requests[0] # GetMapObjects or GetPlayer: 50% # Encounter: 10% - # Others: 3% + # Others: 3% if ((rtype in (2, 106) and randval > 0.5) - or (rtype == 102 and randval > 0.9) - or randval > 0.97): + or (rtype == 102 and randval > 0.9) or randval > 0.97): return True return False - - def _build_sub_requests(self, mainrequest, subrequest_list): - self.log.debug('Generating sub RPC requests...') + + @staticmethod + def _build_sub_requests(mainrequest, subrequest_list): + RpcApi.log.debug('Generating sub RPC requests...') for entry_id, params in subrequest_list: if params: entry_name = RequestType.Name(entry_id) proto_name = entry_name.lower() + '_message' - bytes = self._get_proto_bytes('pogoprotos.networking.requests.messages.', proto_name, params) + bytes = RpcApi._get_proto_bytes( + 'pogoprotos.networking.requests.messages.', proto_name, + params) subrequest = mainrequest.requests.add() subrequest.request_type = entry_id @@ -333,8 +377,9 @@ def _build_sub_requests(self, mainrequest, subrequest_list): return mainrequest - def _build_platform_requests(self, mainrequest, platform_list): - self.log.debug('Generating platform RPC requests...') + @staticmethod + def _build_platform_requests(mainrequest, platform_list): + RpcApi.log.debug('Generating platform RPC requests...') for entry_id, params in platform_list: if params: @@ -342,7 +387,9 @@ def _build_platform_requests(self, mainrequest, platform_list): if entry_name == 'UNKNOWN_PTR_8': entry_name = 'UNKNOWN_PTR8' proto_name = entry_name.lower() + '_request' - bytes = self._get_proto_bytes('pogoprotos.networking.platform.requests.', proto_name, params) + bytes = RpcApi._get_proto_bytes( + 'pogoprotos.networking.platform.requests.', proto_name, + params) platform = mainrequest.platform_requests.add() platform.type = entry_id @@ -353,96 +400,111 @@ def _build_platform_requests(self, mainrequest, platform_list): platform.type = entry_id return mainrequest - - def _get_proto_bytes(self, path, name, entry_content): + @staticmethod + def _get_proto_bytes(path, name, entry_content): proto_classname = path + name + '_pb2.' + name - proto = self.get_class(proto_classname)() + proto = RpcApi.get_class(proto_classname)() - self.log.debug("Subrequest class: %s", proto_classname) + RpcApi.log.debug("Subrequest class: %s", proto_classname) for key, value in entry_content.items(): if isinstance(value, list): - self.log.debug("Found list: %s - trying as repeated", key) + RpcApi.log.debug("Found list: %s - trying as repeated", key) for i in value: try: - self.log.debug("%s -> %s", key, i) + RpcApi.log.debug("%s -> %s", key, i) r = getattr(proto, key) r.append(i) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, i, proto_name, e) + RpcApi.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, i, proto_classname, e) elif isinstance(value, dict): for k in value.keys(): try: r = getattr(proto, key) setattr(r, k, value[k]) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, str(value), proto_name, e) + RpcApi.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, str(value), proto_classname, e) else: try: setattr(proto, key, value) except Exception as e: try: - self.log.debug("%s -> %s", key, value) + RpcApi.log.debug("%s -> %s", key, value) r = getattr(proto, key) r.append(value) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, value, proto_name, e) + RpcApi.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, value, proto_classname, e) return proto.SerializeToString() - def _parse_main_response(self, response_raw, subrequests, use_dict = True): - self.log.debug('Parsing main RPC response...') + @staticmethod + def _parse_main_response(response_raw, subrequests): + RpcApi.log.debug('Parsing main RPC response...') if response_raw.status_code == 400: raise BadRequestException("400: Bad Request") if response_raw.status_code == 403: - raise NianticIPBannedException("Seems your IP Address is banned or something else went badly wrong...") + raise NianticIPBannedException( + "Seems your IP Address is banned or something else went badly wrong..." + ) elif response_raw.status_code in (502, 503, 504): - raise NianticOfflineException('{} Server Error'.format(response_raw.status_code)) + raise NianticOfflineException( + '{} Server Error'.format(response_raw.status_code)) elif response_raw.status_code != 200: - error = 'Unexpected HTTP server response - needs 200 got {}'.format(response_raw.status_code) - self.log.warning(error) - self.log.debug('HTTP output: \n%s', response_raw.content.decode('utf-8')) + error = 'Unexpected HTTP server response - needs 200 got {}'.format( + response_raw.status_code) + RpcApi.log.warning(error) + RpcApi.log.debug('HTTP output: \n%s', + response_raw.content.decode('utf-8')) raise UnexpectedResponseException(error) if not response_raw.content: - self.log.warning('Empty server response!') + RpcApi.log.warning('Empty server response!') raise MalformedNianticResponseException('Empty server response!') response_proto = ResponseEnvelope() try: response_proto.ParseFromString(response_raw.content) except message.DecodeError as e: - self.log.error('Could not parse response: %s', e) - raise MalformedNianticResponseException('Could not decode response.') + RpcApi.log.error('Could not parse response: %s', e) + raise MalformedNianticResponseException( + 'Could not decode response.') - self.log.debug('Protobuf structure of rpc response:\n\r%s', response_proto) + RpcApi.log.debug('Protobuf structure of rpc response:\n\r%s', + response_proto) try: - self.log.debug('Decode raw over protoc (protoc has to be in your PATH):\n\r%s', self.decode_raw(response_raw.content).decode('utf-8')) + RpcApi.log.debug( + 'Decode raw over protoc (protoc has to be in your PATH):\n\r%s', + RpcApi.decode_raw(response_raw.content).decode('utf-8')) except Exception: - self.log.debug('Error during protoc parsing - ignored.') + RpcApi.log.debug('Error during protoc parsing - ignored.') - if use_dict: - response_proto_dict = protobuf_to_dict(response_proto) - if 'returns' in response_proto_dict: - del response_proto_dict['returns'] - else: - response_proto_dict = {'envelope': response_proto} + response_proto_dict = {'envelope': response_proto} if not response_proto_dict: - raise MalformedNianticResponseException('Could not convert protobuf to dict.') - - response_proto_dict = self._parse_sub_responses(response_proto, subrequests, response_proto_dict, use_dict) - - #It can't be done before - if not use_dict: - del response_proto_dict['envelope'].returns[:] + raise MalformedNianticResponseException( + 'Could not convert protobuf to dict.') + + response_proto_dict = RpcApi._parse_sub_responses( + response_proto, subrequests, response_proto_dict) + + # It can't be done before. + del response_proto_dict['envelope'].returns[:] return response_proto_dict - def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_dict, use_dict = True): - self.log.debug('Parsing sub RPC responses...') + @staticmethod + def _parse_sub_responses(response_proto, + subrequests_list, + response_proto_dict): + RpcApi.log.debug('Parsing sub RPC responses...') response_proto_dict['responses'] = {} if response_proto.status_code == 53: @@ -457,31 +519,77 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ proto_name = entry_name.lower() + '_response' proto_classname = 'pogoprotos.networking.responses.' + proto_name + '_pb2.' + proto_name - self.log.debug("Parsing class: %s", proto_classname) + RpcApi.log.debug("Parsing class: %s", proto_classname) subresponse_return = None try: - subresponse_extension = self.get_class(proto_classname)() - except Exception as e: + subresponse_extension = RpcApi.get_class(proto_classname)() + except Exception: subresponse_extension = None - error = 'Protobuf definition for {} not found'.format(proto_classname) + error = 'Protobuf definition for {} not found'.format( + proto_classname) subresponse_return = error - self.log.warning(error) + RpcApi.log.warning(error) if subresponse_extension: try: subresponse_extension.ParseFromString(subresponse) - if use_dict: - - subresponse_return = protobuf_to_dict(subresponse_extension) - else: - subresponse_return = subresponse_extension + subresponse_return = subresponse_extension except Exception: - error = "Protobuf definition for {} seems not to match".format(proto_classname) + error = "Protobuf definition for {} seems not to match".format( + proto_classname) subresponse_return = error - self.log.warning(error) + RpcApi.log.warning(error) response_proto_dict['responses'][entry_name] = subresponse_return i += 1 return response_proto_dict + + +# Original by Noctem. +class RpcState: + def __init__(self, device_info, auth_provider): + self.session_hash = os.urandom(16) + self.mag_x_min = random.uniform(-80, 60) + self.mag_x_max = self.mag_x_min + 20 + self.mag_y_min = random.uniform(-120, 90) + self.mag_y_max = self.mag_y_min + 30 + self.mag_z_min = random.uniform(-70, 40) + self.mag_z_max = self.mag_y_min + 15 + self._course = random.uniform(0, 359.99) + self.auth_provider = auth_provider + + # data fields for SignalAgglom + self.token2 = random.randint(1, 59) + self.course = random.uniform(0, 360) + + self.device_info = device_info + + self.RPC_ID_LOW = 1 + self.RPC_ID_HIGH = 1 + self.start_time = get_time(ms=True) - random.randint(6000, 10000) + self.session = None + + @property + def magnetic_field_x(self): + return random.uniform(self.mag_x_min, self.mag_x_max) + + @property + def magnetic_field_y(self): + return random.uniform(self.mag_y_min, self.mag_y_max) + + @property + def magnetic_field_z(self): + return random.uniform(self.mag_z_min, self.mag_z_max) + + @property + def course(self): + self._course = random.triangular(0, 359.99, self._course) + return self._course + + def get_next_request_id(self): + self.RPC_ID_LOW += 1 + self.RPC_ID_HIGH = ((7**5) * self.RPC_ID_HIGH) % ((2**31) - 1) + reqid = (self.RPC_ID_HIGH << 32) | self.RPC_ID_LOW + return reqid diff --git a/pgoapi/utilities.py b/pgoapi/utilities.py index 709aecdc..3a57af36 100755 --- a/pgoapi/utilities.py +++ b/pgoapi/utilities.py @@ -20,15 +20,10 @@ """ import time -import struct +import random import logging -from json import JSONEncoder -from binascii import unhexlify - # other stuff -from google.protobuf.internal import encoder -from geopy.geocoders import GoogleV3 from s2sphere import LatLng, Angle, Cap, RegionCoverer, math log = logging.getLogger(__name__) @@ -36,44 +31,9 @@ EARTH_RADIUS = 6371000 # radius of Earth in meters -def f2i(float): - return struct.unpack(' 1500: radius = 1500 # radius = 1500 is max allowed by the server - region = Cap.from_axis_angle(LatLng.from_degrees(lat, long).to_point(), Angle.from_degrees(360*radius/(2*math.pi*EARTH_RADIUS))) + region = Cap.from_axis_angle( + LatLng.from_degrees(lat, long).to_point(), + Angle.from_degrees(360 * radius / (2 * math.pi * EARTH_RADIUS))) coverer = RegionCoverer() coverer.min_level = 15 coverer.max_level = 15 @@ -90,14 +52,14 @@ def get_cell_ids(lat, long, radius=500): return sorted([x.id() for x in cells]) -def get_time(ms = False): +def get_time(ms=False): if ms: return int(time.time() * 1000) else: return int(time.time()) -def get_format_time_diff(low, high, ms = True): +def get_format_time_diff(low, high, ms=True): diff = (high - low) if ms: m, s = divmod(diff / 1000, 60) @@ -113,3 +75,14 @@ def parse_api_endpoint(api_url): api_url = 'https://{}/rpc'.format(api_url) return api_url + + +def weighted_choice(choices): + total = sum(w for c, w in choices) + r = random.uniform(0, total) + upto = 0 + for c, w in choices: + if upto + w >= r: + return c + upto += w + assert False, "Shouldn't get here" diff --git a/requirements.txt b/requirements.txt index 0fd822f0..804560ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,5 @@ -geopy>=1.11.0 protobuf>=3.0.0 requests[socks]>=2.10.0 s2sphere>=0.2.4 gpsoauth>=0.4.0 -protobuf3-to-dict>=0.1.4 -future -six pycrypt>=0.7.1 diff --git a/scripts/accept-tos.py b/scripts/accept-tos.py deleted file mode 100644 index 489f491d..00000000 --- a/scripts/accept-tos.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -"""accept-tos.py: Example script to accept in-game Terms of Service""" - -from pgoapi import PGoApi -from pgoapi.utilities import f2i -from pgoapi import utilities as util -from pgoapi.exceptions import AuthException -import pprint -import time -import threading - -def accept_tos(username, password, lat, lon, alt, auth='ptc'): - api = PGoApi() - api.set_position(lat, lon, alt) - api.login(auth, username, password) - time.sleep(2) - req = api.create_request() - req.mark_tutorial_complete(tutorials_completed = 0, send_marketing_emails = False, send_push_notifications = False) - response = req.call() - print('Accepted Terms of Service for {}'.format(username)) - #print('Response dictionary: \r\n{}'.format(pprint.PrettyPrinter(indent=4).pformat(response))) - -"""auth service defaults to ptc if not given""" - -accept_tos('username', 'password', 40.7127837, -74.005941, 0.0) -accept_tos('username2', 'password', 5.612711763, -1.0632, 15.0, 'ptc') -accept_tos('username3', 'password', -22.2156, 35.1237, 3.45, 'google') diff --git a/scripts/pokecli.py b/scripts/pokecli.py deleted file mode 100755 index 77fcfd60..00000000 --- a/scripts/pokecli.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import sys -import json -import time -import pprint -import logging -import getpass -import argparse - -# add directory of this file to PATH, so that the package will be found -sys.path.append(os.path.dirname(os.path.realpath(__file__))) - -# import Pokemon Go API lib -from pgoapi import pgoapi -from pgoapi import utilities as util - - -log = logging.getLogger(__name__) - -def init_config(): - parser = argparse.ArgumentParser() - config_file = "config.json" - - # If config file exists, load variables from json - load = {} - if os.path.isfile(config_file): - with open(config_file) as data: - load.update(json.load(data)) - - # Read passed in Arguments - required = lambda x: not x in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", - required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) - parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-l", "--location", help="Location", required=required("location")) - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') - parser.add_argument("-px", "--proxy", help="Specify a socks5 proxy url") - parser.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - config.__dict__[key] = str(load[key]) - - if config.__dict__["password"] is None: - log.info("Secure Password Input (if there is no password prompt, use --password ):") - config.__dict__["password"] = getpass.getpass() - - if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None - - return config - - -def main(): - # log settings - # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - - # instantiate pgoapi - api = pgoapi.PGoApi() - if config.proxy: - api.set_proxy({'http': config.proxy, 'https': config.proxy}) - - # parse position - position = util.get_pos_by_name(config.location) - if not position: - log.error('Your given location could not be found by name') - return - elif config.test: - return - - # set player position on the earth - api.set_position(*position) - - # new authentication initialitation - if config.proxy: - api.set_authentication(provider = config.auth_service, username = config.username, password = config.password, proxy_config = {'http': config.proxy, 'https': config.proxy}) - else: - api.set_authentication(provider = config.auth_service, username = config.username, password = config.password) - - # print get maps object - cell_ids = util.get_cell_ids(position[0], position[1]) - timestamps = [0,] * len(cell_ids) - response_dict = api.get_map_objects(latitude =position[0], longitude = position[1], since_timestamp_ms = timestamps, cell_id = cell_ids) - print('Response dictionary (get_player): \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(response_dict))) - - -if __name__ == '__main__': - main()