diff --git a/README.md b/README.md index 758bf9fed8..1834026450 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,8 @@ If you do not want any data to be gathered, you can turn off this feature by set * Breeze Ro * bruno-kenji * Gobberwart + * javajohnHub + * kolinkorr839 ## Disclaimer ©2016 Niantic, Inc. ©2016 Pokémon. ©1995–2016 Nintendo / Creatures Inc. / GAME FREAK inc. © 2016 Pokémon/Nintendo Pokémon and Pokémon character names are trademarks of Nintendo. The Google Maps Pin is a trademark of Google Inc. and the trade dress in the product design is a trademark of Google Inc. under license to The Pokémon Company. Other trademarks are the property of their respective owners. diff --git a/configs/config.json.example b/configs/config.json.example index 826fe1a4a5..164d1d5f00 100644 --- a/configs/config.json.example +++ b/configs/config.json.example @@ -153,6 +153,7 @@ "type": "EvolvePokemon", "config": { "enabled": false, + "log_interval": 120, "// evolve only pidgey and drowzee": "", "// evolve_list": "pidgey, drowzee", @@ -211,6 +212,14 @@ "recycle_force_max": "00:05:00" } }, + { + "type": "CatchLimiter", + "config": { + "enabled": false, + "min_balls": 20, + "duration": 15 + } + }, { "type": "Sniper", "config": { diff --git a/configs/config.json.map.example b/configs/config.json.map.example index 239519c366..af64d62a77 100644 --- a/configs/config.json.map.example +++ b/configs/config.json.map.example @@ -209,6 +209,14 @@ "enabled": true } }, + { + "type": "CatchLimiter", + "config": { + "enabled": false, + "min_balls": 20, + "duration": 15 + } + }, { "type": "MoveToMapPokemon", "config": { diff --git a/configs/config.json.optimizer.example b/configs/config.json.optimizer.example index b40f89fa52..ebe43960b8 100644 --- a/configs/config.json.optimizer.example +++ b/configs/config.json.optimizer.example @@ -72,7 +72,7 @@ "mode": "by_pokemon", "names": ["!with_next_evolution"], "top": 1, - "sort": ["dps_attack"], + "sort": ["dps_attack", "iv"], "keep": {"iv": 0.9} } ] diff --git a/docs/configuration_files.md b/docs/configuration_files.md index bcf1d588eb..f956af9579 100644 --- a/docs/configuration_files.md +++ b/docs/configuration_files.md @@ -28,6 +28,7 @@ - [Settings Description](#settings-description) - [`flee_count` and `flee_duration`](#flee_count-and-flee_duration) - [Previous `catch_simulation` Behaviour](#previous-catch_simulation-behaviour) +- [CatchLimiter Settings](#catchlimiter-settings) - [Sniping _(MoveToLocation)_](#sniping-_-movetolocation-_) - [Description](#description) - [Options](#options) @@ -190,9 +191,14 @@ The behaviors of the bot are configured via the `tasks` key in the `config.json` * `changeball_wait_max`: 5 | Maximum time to wait when changing balls * `newtodex_wait_min`: 20 | Minimum time to wait if we caught a new type of pokemon * `newtodex_wait_max`: 39 | Maximum time to wait if we caught a new type of pokemon +* Catch Limiter + * `enabled`: Default false | Enable/disable the task + * `min_balls`: Default 20 | Minimum balls on hand before catch tasks enabled + * `duration`: Default 15 | Length of time to disable catch tasks * EvolvePokemon * `enable`: Disable or enable this task. * `evolve_all`: Default `NONE` | Depreciated. Please use evolve_list and donot_evolve_list + * `log_interval`: `Default: 120`. Time (in seconds) to periodically print how far you are from having enough pokemon to evolve (more than `min_pokemon_to_be_evolved`) * `evolve_list`: Default `all` | Set to all, or specifiy different pokemon seperated by a comma * `donot_evolve_list`: Default `none` | Pokemon seperated by comma, will be ignored from evolve_list * `min_evolve_speed`: Default `25` | Minimum seconds to wait between each evolution @@ -207,6 +213,9 @@ The behaviors of the bot are configured via the `tasks` key in the `config.json` * `enable`: Disable or enable this task. * `spin_wait_min`: Default 3 | Minimum wait time after fort spin * `spin_wait_max`: Default 5 | Maximum wait time after fort spin + * `daily_spin_limit`: Default 2000 | Daily spin limit + * `min_interval`: Default 120 | When daily spin limit is reached, how often should the warning message be shown + * `exit_on_limit_reached`: Default `True` | Code will exits if daily_spin_limit is reached * HandleSoftBan * IncubateEggs * `enable`: Disable or enable this task. @@ -570,7 +579,7 @@ Key | Info "enabled": true, "dont_nickname_favorite": false, "good_attack_threshold": 0.7, - "nickname_template": "{iv_pct}-{iv_ads}" + "nickname_template": "{iv_pct}-{iv_ads}", "locale": "en" } } @@ -652,6 +661,30 @@ If you want to make your bot behave as it did prior to the catch_simulation upda } ``` +## CatchLimiter Settings +[[back to top](#table-of-contents)] + +These settings define thresholds and duration to disable all catching tasks for a specified duration when balls are low. This allows your bot to spend time moving/looting and recovering balls spent catching. + +## Default Settings + +``` +"enabled": false, +"min_balls": 20, +"duration": 15 +``` + +### Settings Description +[[back to top](#table-of-contents)] + +Setting | Description +---- | ---- +`enabled` | Specify whether this task should run or not +`min_balls` | Determine minimum ball level required for catching tasks to be enabled +`duration` | How long to disable catching + +Catching will be disabled when balls on hand reaches/is below "min_balls" and will be re-enabled when "duration" is reached, or when balls on hand > min_balls (whichever is later) + ## Sniping _(MoveToLocation)_ [[back to top](#table-of-contents)] @@ -852,8 +885,8 @@ This task is an upgrade version of the MoveToMapPokemon task. It will fetch poke Walk to the specified locations loaded from .gpx or .json file. It is highly recommended to use website such as [GPSies](http://www.gpsies.com) which allow you to export your created track in JSON file. Note that you'll have to first convert its JSON file into the format that the bot can understand. See [Example of pier39.json] below for the content. I had created a simple python script to do the conversion. -The json file can contain for each point an optional `loiter` field. This -indicated the number of seconds the bot should loiter after reaching the point. +The json file can contain for each point an optional `wander` field. This +indicated the number of seconds the bot should wander after reaching the point. During this time, the next Task in the configuration file is executed, e.g. a MoveToFort task. This allows the bot to walk around the waypoint looking for forts for a limited time. @@ -872,9 +905,9 @@ forts for a limited time. ### Notice If you use the `single` `path_mode` without e.g. a `MoveToFort` task, your bot with /not move at all/ when the path is finished. Similarly, if you use the -`loiter` option in your json path file without a following `MoveToFort` or -similar task, your bot will not move during the loitering period. Please -make sure, when you use `single` mode or the `loiter` option, that another +`wander` option in your json path file without a following `MoveToFort` or +similar task, your bot will not move during the wandering period. Please +make sure, when you use `single` mode or the `wander` option, that another move-type task follows the `FollowPath` task in your `config.json`. ### Sample Configuration diff --git a/docs/pokemon_optimizer.md b/docs/pokemon_optimizer.md index a5d87bc284..b00710aa84 100644 --- a/docs/pokemon_optimizer.md +++ b/docs/pokemon_optimizer.md @@ -44,7 +44,7 @@ It will also collect the candies from your Buddy and select the next buddy. # Configuration ## Default configuration -``` +```json { "tasks": [ { @@ -119,7 +119,7 @@ It will also collect the candies from your Buddy and select the next buddy. "mode": "by_pokemon", "names": ["!with_next_evolution"], "top": 1, - "sort": ["dps_attack"], + "sort": ["dps_attack", "iv"], "keep": {"iv": 0.9} } ] @@ -363,7 +363,7 @@ You can define `groups` of Pokemon to help you restrict rules to a specific set
You can then use these `groups` names in the [`names`](#rule-names) parameter of your rule to refer to list of Pokemon `groups` are list of Pokemon names: -``` +```json "groups": { "gym": ["Dragonite", "Snorlax"], "my_love": ["Pikachu"], @@ -398,36 +398,78 @@ The order in which the rule are defined may have an impact on the behavior. Especially, if there not enough candies/stardust to evolve/upgrade all the selected Pokemon, the Pokemon selected by the first rule will be evolved/upgraded first, then the ones of the second rule etc. More generally, the first rule always have higher priority for evolve, upgrade or buddy. -``` +```json "rules": [ { + "// Of all Pokemon with less than 124 candies, buddy the Pokemon having the highest maximum cp": {}, + "mode": "overall", + "top": 1, + "sort": ["max_cp", "cp"], + "keep": {"candy": -124}, + "evolve": false, + "buddy": true + }, + { + "// Buddy the Pokemon having the less candies. In case no Pokemon match first rule": {}, + "mode": "overall", + "top": 1, + "sort": ["-candy", "max_cp", "cp"], + "evolve": false, + "buddy": true + }, + { + "mode": "by_pokemon", + "names": ["gym"], + "top": 3, + "sort": ["iv", "ncp"], + "evolve": {"iv": 0.9, "ncp": 0.9}, + "upgrade": {"iv": 0.9, "ncp": 0.9} + }, + { + "// Keep best iv of each family and evolve it if its iv is greater than 0.9": {}, "mode": "by_family", "top": 1, "sort": ["iv"], "evolve": {"iv": 0.9} }, { + "// Keep best ncp of each family and evolve it if its ncp is greater than 0.9": {}, "mode": "by_family", "top": 1, "sort": ["ncp"], "evolve": {"ncp": 0.9} }, { + "// Keep best cp of each family but do not evolve it": {}, "mode": "by_family", "top": 1, - "sort": ["cp"] + "sort": ["cp"], + "evolve": false }, { - "mode": "by_family", - "top": 3, - "names": ["gym"], - "sort": ["iv", "ncp"], - "evolve": {"iv": 0.9, "ncp": 0.9}, - "upgrade": {"iv": 0.9, "ncp": 0.9} + "// For Pokemon of final evolution and with iv greater than 0.9, keep the best dps_attack": {}, + "mode": "by_pokemon", + "names": ["!with_next_evolution"], + "top": 1, + "sort": ["dps_attack", "iv"], + "keep": {"iv": 0.9} } ] ``` +The following table describe how the parameters of a rule affect the selection of Pokemon: + +| | | Balbusaur `{"iv": 0.38}` | Ivysaur `{"iv": 0.98}` | Venusaur `{"iv": 0.71}` | ... | Dratini `{"iv": 0.47}` | Dratini `{"iv": 0.93}` | Dragonair `{"iv": 0.82}` | Dragonair `{"iv": 0.91}` | Dragonite `{"iv": 1.0}` | +|:-------:|:-------------:|:------------------------:|:----------------------:|:-----------------------:|:---:|:----------------------:|:----------------------:|:------------------------:|:------------------------:|:-----------------------:| +| mode | `per_family` | A | A | A | | B | B | B | B | B | +| names | `Dragonite` | | | | | x | x | x | x | x | +| keep | `{"iv": 0.8}` | | | | | | x | x | x | x | +| sort | `["iv"]` | | | | | | 2 | 4 | 3 | 1 | +| top | `3` | | | | | | x | | x | x | +| evolve | `{"iv": 0.9}` | | | | | | x | | x | | +| upgrade | `{"iv": 1.0}` | | | | | | | | | x | + + [[back to top](#pokemon-optimizer)] #### rule mode @@ -653,7 +695,7 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, # FAQ #### How do I keep the 2 best `iv` of every single Pokemon, and evolve them if they are over `0.9` `iv` ? -``` +```json { "mode": "by_pokemon", "top": 2, @@ -664,7 +706,7 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, #### How do I keep the 2 best `iv` of every single Pokemon, and evolve them if they are over `0.9` `ncp` ? -``` +```json { "mode": "by_pokemon", "top": 2, @@ -675,10 +717,10 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, #### How do I keep my 10 best `cp` Dragonite and Snorlax to fight gyms ? -``` +```json { "mode": "by_pokemon", - "names": ["Dragonite", "Snorlax"] + "names": ["Dragonite", "Snorlax"], "top": 10, "sort": ["cp"] }, @@ -686,10 +728,10 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, #### How do I keep the Gyarados with the best moveset for attack ? -``` +```json { "mode": "by_pokemon", - "names": ["Gyarados"] + "names": ["Gyarados"], "top": 1, "sort": ["dps_attack"] }, @@ -697,10 +739,10 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, #### How do I keep the Gyarados with the best fast attack ? -``` +```json { "mode": "by_pokemon", - "names": ["Gyarados"] + "names": ["Gyarados"], "top": 1, "sort": ["dps1"] }, @@ -708,17 +750,17 @@ For Eevee Pokemon family, and any other family with multiple paths of evolution, #### How do I keep all my Poliwag with `cp` less that `20` ? -``` +```json { "mode": "by_pokemon", - "names": ["Poliwag"] + "names": ["Poliwag"], "keep": {"cp": -20} }, ``` #### How do I buddy the Pokemon for which I have the less number of candies ? -``` +```json { "mode": "overall", "top": 1, diff --git a/docs/telegramtask.md b/docs/telegramtask.md index a98ec2d6ae..6706a5d8b5 100644 --- a/docs/telegramtask.md +++ b/docs/telegramtask.md @@ -54,8 +54,19 @@ This will subscribe you to be notified every time a Dratini has been caught with > /top 10 iv -List top 10 pokemon, ordered by IV +List top 10 pokemon, ordered by IV, descending order > /top 15 cp -List top 15 pokemon, ordered by CP +List top 15 pokemon, ordered by CP, descending order + +> /top 5 dated + +List top 5 pokemon, ordered by catching date, descending order + +Same logic for : +/evolved +/hatched +/caught +/released +/vanished diff --git a/pokemongo_bot/__init__.py b/pokemongo_bot/__init__.py index dddff7da51..2a258b22aa 100644 --- a/pokemongo_bot/__init__.py +++ b/pokemongo_bot/__init__.py @@ -117,11 +117,14 @@ def __init__(self, db, config): self.heartbeat_counter = 0 self.last_heartbeat = time.time() self.hb_locked = False # lock hb on snip - + # Inventory refresh limiting self.inventory_refresh_threshold = 10 self.inventory_refresh_counter = 0 self.last_inventory_refresh = time.time() + + # Catch on/off + self.catch_disabled = False self.capture_locked = False # lock catching while moving to VIP pokemon @@ -477,6 +480,10 @@ def _register_events(self): 'pokemon_evolved', parameters=('pokemon', 'iv', 'cp', 'candy', 'xp') ) + self.event_manager.register_event( + 'pokemon_evolve_check', + parameters=('has', 'needs') + ) self.event_manager.register_event( 'pokemon_upgraded', parameters=('pokemon', 'iv', 'cp', 'candy', 'stardust') @@ -717,6 +724,11 @@ def _register_events(self): self.event_manager.register_event('sniper_log', parameters=('message', 'message')) self.event_manager.register_event('sniper_error', parameters=('message', 'message')) self.event_manager.register_event('sniper_teleporting', parameters=('latitude', 'longitude', 'name')) + + # Catch-limiter + self.event_manager.register_event('catch_limit_on') + self.event_manager.register_event('catch_limit_off') + def tick(self): self.health_record.heartbeat() @@ -1476,4 +1488,4 @@ def _refresh_inventory(self): inventory.refresh_inventory() self.last_inventory_refresh = now self.inventory_refresh_counter += 1 - + diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index b6e3d4133b..87249763bc 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -31,3 +31,4 @@ from .camp_fort import CampFort from .discord_task import DiscordTask from .buddy_pokemon import BuddyPokemon +from .catch_limiter import CatchLimiter diff --git a/pokemongo_bot/cell_workers/catch_limiter.py b/pokemongo_bot/cell_workers/catch_limiter.py new file mode 100644 index 0000000000..ee79bda8a5 --- /dev/null +++ b/pokemongo_bot/cell_workers/catch_limiter.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from __future__ import absolute_import + +from datetime import datetime, timedelta +from pokemongo_bot.base_task import BaseTask +from pokemongo_bot.worker_result import WorkerResult +from pokemongo_bot import inventory +from pokemongo_bot.item_list import Item + +class CatchLimiter(BaseTask): + SUPPORTED_TASK_API_VERSION = 1 + + def __init__(self, bot, config): + super(CatchLimiter, self).__init__(bot, config) + self.bot = bot + self.config = config + self.enabled = self.config.get("enabled",False) + self.min_balls = self.config.get("min_balls",20) + self.duration = self.config.get("duration",15) + if not hasattr(self.bot, "catch_resume_at"): self.bot.catch_resume_at = None + + def work(self): + if not self.enabled: + return WorkerResult.SUCCESS + + now = datetime.now() + balls_on_hand = self.get_pokeball_count() + + # If resume time has passed, resume catching tasks + if self.bot.catch_disabled and now >= self.bot.catch_resume_at: + if balls_on_hand > self.min_balls: + self.emit_event( + 'catch_limit_off', + formatted="Resume time has passed and balls on hand ({}) exceeds threshold {}. Re-enabling catch tasks.". + format(balls_on_hand,self.min_balls) + ) + self.bot.catch_disabled = False + + # If balls_on_hand less than threshold, pause catching tasks for duration minutes + if not self.bot.catch_disabled and balls_on_hand <= self.min_balls: + self.bot.catch_resume_at = now + timedelta(minutes = self.duration) + self.bot.catch_disabled = True + self.emit_event( + 'catch_limit_on', + formatted="Balls on hand ({}) has reached threshold {}. Disabling catch tasks until {} or balls on hand > threshold (whichever is later).". + format(balls_on_hand, self.min_balls, self.bot.catch_resume_at.strftime("%H:%M:%S")) + ) + + return WorkerResult.SUCCESS + + def get_pokeball_count(self): + return sum([inventory.items().get(ball.value).count for ball in [Item.ITEM_POKE_BALL, Item.ITEM_GREAT_BALL, Item.ITEM_ULTRA_BALL]]) diff --git a/pokemongo_bot/cell_workers/evolve_pokemon.py b/pokemongo_bot/cell_workers/evolve_pokemon.py index f21c6d9251..d04c8a23a7 100644 --- a/pokemongo_bot/cell_workers/evolve_pokemon.py +++ b/pokemongo_bot/cell_workers/evolve_pokemon.py @@ -17,6 +17,8 @@ def __init__(self, bot, config): def initialize(self): self.start_time = 0 + self.next_log_update = 0 + self.log_interval = self.config.get('log_interval', 120) self.evolve_list = self.config.get('evolve_list', []) self.donot_evolve_list = self.config.get('donot_evolve_list', []) self.min_evolve_speed = self.config.get('min_evolve_speed', 25) @@ -41,7 +43,7 @@ def initialize(self): def _validate_config(self): if isinstance(self.evolve_list, basestring): self.evolve_list = [str(pokemon_name).lower().strip() for pokemon_name in self.evolve_list.split(',')] - + if isinstance(self.donot_evolve_list, basestring): self.donot_evolve_list = [str(pokemon_name).lower().strip() for pokemon_name in self.donot_evolve_list.split(',')] @@ -65,7 +67,10 @@ def work(self): candy = inventory.candies().get(pokemon.pokemon_id) pokemon_to_be_evolved = pokemon_to_be_evolved + min(candy.quantity / (pokemon.evolution_cost - 1), filtered_dict[pokemon.pokemon_id]) - if pokemon_to_be_evolved >= self.min_pokemon_to_be_evolved: + self._log_update_if_should(pokemon_to_be_evolved, self.min_pokemon_to_be_evolved) + + has_minimum_to_evolve = pokemon_to_be_evolved >= self.min_pokemon_to_be_evolved + if has_minimum_to_evolve: if self.use_lucky_egg: self._use_lucky_egg() cache = {} @@ -73,11 +78,26 @@ def work(self): if pokemon.can_evolve_now(): self._execute_pokemon_evolve(pokemon, cache) + def _log_update_if_should(self, has, needs): + if self._should_log_update(): + self._compute_next_log_update() + self.emit_event( + 'pokemon_evolve_check', + formatted='Evolvable: {has}/{needs}', + data={'has': has, 'needs': needs} + ) + + def _compute_next_log_update(self): + self.next_log_update = time.time() + self.log_interval + + def _should_log_update(self): + return time.time() >= self.next_log_update + def _should_run(self): if not self.evolve_list or self.evolve_list[0] == 'none': return False return True - + def _use_lucky_egg(self): using_lucky_egg = time.time() - self.start_time < 1800 if using_lucky_egg: diff --git a/pokemongo_bot/cell_workers/follow_path.py b/pokemongo_bot/cell_workers/follow_path.py index 3bdea10e21..d0819531eb 100644 --- a/pokemongo_bot/cell_workers/follow_path.py +++ b/pokemongo_bot/cell_workers/follow_path.py @@ -18,7 +18,7 @@ from datetime import datetime as dt, timedelta STATUS_MOVING = 0 -STATUS_LOITERING = 1 +STATUS_WANDERING = 1 STATUS_FINISHED = 2 class FollowPath(BaseTask): @@ -28,7 +28,7 @@ def initialize(self): self._process_config() self.points = self.load_path() self.status = STATUS_MOVING - self.loiter_end_time = 0 + self.wander_end_time = 0 self.distance_unit = self.bot.config.distance_unit self.append_unit = False @@ -139,12 +139,12 @@ def endLaps(self): self.bot.login() def work(self): - # If done or loitering allow the next task to run + # If done or wandering allow the next task to run if self.status == STATUS_FINISHED: return WorkerResult.SUCCESS - if self.status == STATUS_LOITERING and time.time() < self.loiter_end_time: - return WorkerResult.RUNNING + if self.status == STATUS_WANDERING and time.time() < self.wander_end_time: + return WorkerResult.SUCCESS last_lat, last_lng, last_alt = self.bot.position @@ -190,12 +190,14 @@ def work(self): } ) - if (self.bot.config.walk_min > 0 and is_at_destination) or (self.status == STATUS_LOITERING and time.time() >= self.loiter_end_time): - if "loiter" in point and self.status != STATUS_LOITERING: - self.logger.info("Loitering for {} seconds...".format(point["loiter"])) - self.status = STATUS_LOITERING - self.loiter_end_time = time.time() + point["loiter"] - return WorkerResult.RUNNING + if (self.bot.config.walk_min > 0 and is_at_destination) or (self.status == STATUS_WANDERING and time.time() >= self.wander_end_time): + if "loiter" in point: + self.logger.warning("'loiter' is obsolete, please change to 'wander' in {}".format(self.path_file)) + if "wander" in point and self.status != STATUS_WANDERING: + self.logger.info("Wandering for {} seconds...".format(point["wander"])) + self.status = STATUS_WANDERING + self.wander_end_time = time.time() + point["wander"] + return WorkerResult.SUCCESS if (self.ptr + 1) == len(self.points): if self.path_mode == 'single': self.status = STATUS_FINISHED diff --git a/pokemongo_bot/cell_workers/incubate_eggs.py b/pokemongo_bot/cell_workers/incubate_eggs.py index 77e2b4bf9d..300ebd7f0a 100644 --- a/pokemongo_bot/cell_workers/incubate_eggs.py +++ b/pokemongo_bot/cell_workers/incubate_eggs.py @@ -216,13 +216,13 @@ def _hatch_eggs(self): formatted=msg, data={ 'name': pokemon.name, - 'cp': pokemon.cp, - 'ncp': round(pokemon.cp_percent, 2), - 'iv_ads': pokemon.iv_display, - 'iv_pct': pokemon.iv, - 'exp': xp[i], - 'stardust': stardust[i], - 'candy': candy[i] + 'cp': str(int(pokemon.cp)), + 'ncp': str(round(pokemon.cp_percent, 2)), + 'iv_ads': str(pokemon.iv_display), + 'iv_pct': str(pokemon.iv), + 'exp': str(xp[i]), + 'stardust': str(stardust[i]), + 'candy': str(candy[i]) } ) # hatching egg gets exp too! @@ -258,9 +258,9 @@ def _print_eggs(self): self.emit_event( 'next_egg_incubates', - formatted='Eggs incubating: [{eggs}] (Eggs left: {eggs_left}, Incubating: {eggs_inc})', + formatted='Eggs incubating: {eggs} (Eggs left: {eggs_left}, Incubating: {eggs_inc})', data={ - 'eggs_left': sorted(all_eggs.iteritems()), + 'eggs_left': str(sorted(all_eggs.iteritems())).strip('[]'), 'eggs_inc': len(self.used_incubators), 'eggs': ', '.join(eggs) } diff --git a/pokemongo_bot/cell_workers/move_to_map_pokemon.py b/pokemongo_bot/cell_workers/move_to_map_pokemon.py index abfdd472ce..9918cad51c 100644 --- a/pokemongo_bot/cell_workers/move_to_map_pokemon.py +++ b/pokemongo_bot/cell_workers/move_to_map_pokemon.py @@ -70,6 +70,7 @@ from pokemongo_bot.cell_workers.pokemon_catch_worker import PokemonCatchWorker from random import uniform from pokemongo_bot.constants import Constants +from datetime import datetime ULTRABALL_ID = 3 GREATBALL_ID = 2 @@ -254,7 +255,7 @@ def snipe(self, pokemon): # If target exists, catch it, otherwise ignore if exists: self._encountered(pokemon) - catch_worker = PokemonCatchWorker(pokemon, self.bot, self.config) + catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() time.sleep(self.config.get('snipe_sleep_sec', 2)) self._teleport_back(last_position) @@ -289,6 +290,15 @@ def work(self): self._emit_log("Not enough balls to start sniping (have {}, {} needed)".format( pokeballs_quantity + superballs_quantity + ultraballs_quantity, self.min_ball)) return WorkerResult.SUCCESS + + if self.bot.catch_disabled: + if not hasattr(self.bot,"mtmp_disabled_global_warning") or \ + (hasattr(self.bot,"mtmp_disabled_global_warning") and not self.bot.mtmp_disabled_global_warning): + self._emit_log("All catching tasks are currently disabled until {}. Sniping will resume when catching tasks are re-enabled".format(self.bot.catch_resume_at.strftime("%H:%M:%S"))) + self.bot.mtmp_disabled_global_warning = True + return WorkerResult.SUCCESS + else: + self.bot.mtmp_disabled_global_warning = False # Retrieve pokemos self.dump_caught_pokemon() diff --git a/pokemongo_bot/cell_workers/pokemon_catch_worker.py b/pokemongo_bot/cell_workers/pokemon_catch_worker.py index 5a7da31c1e..75ecb579df 100644 --- a/pokemongo_bot/cell_workers/pokemon_catch_worker.py +++ b/pokemongo_bot/cell_workers/pokemon_catch_worker.py @@ -16,9 +16,6 @@ from datetime import datetime, timedelta from .utils import getSeconds -from pprint import pprint - - CATCH_STATUS_SUCCESS = 1 CATCH_STATUS_FAILED = 2 CATCH_STATUS_VANISHED = 3 @@ -48,11 +45,21 @@ class PokemonCatchWorker(BaseTask): - def __init__(self, pokemon, bot, config): + def __init__(self, pokemon, bot, config={}): self.pokemon = pokemon + + # Load CatchPokemon config if no config supplied + if not config: + for value in bot.workers: + if hasattr(value, 'catch_pokemon'): + config = value.config + + self.config = config + super(PokemonCatchWorker, self).__init__(bot, config) if self.config.get('debug', False): DEBUG_ON = True + def initialize(self): self.position = self.bot.position self.pokemon_list = self.bot.pokemon_list @@ -62,7 +69,7 @@ def initialize(self): self.response_key = '' self.response_status_key = '' self.rest_completed = False - + self.caught_last_24 = 0 #Config self.min_ultraball_to_keep = self.config.get('min_ultraball_to_keep', 10) @@ -97,6 +104,9 @@ def initialize(self): self.catchsim_newtodex_wait_max = self.catchsim_config.get('newtodex_wait_max', 30) + + + ############################################################################ # public methods ############################################################################ @@ -127,7 +137,7 @@ def work(self, response_dict=None): is_vip = self._is_vip_pokemon(pokemon) # skip ignored pokemon - if not self._should_catch_pokemon(pokemon) and not is_vip: + if (not self._should_catch_pokemon(pokemon) and not is_vip) or self.bot.catch_disabled: if not hasattr(self.bot,'skipped_pokemon'): self.bot.skipped_pokemon = [] @@ -138,15 +148,18 @@ def work(self, response_dict=None): pokemon.ivcp == skipped_pokemon.ivcp: return WorkerResult.SUCCESS + if self.bot.catch_disabled: + self.logger.info("Not catching {}. All catching tasks are currently disabled until {}.".format(pokemon,self.bot.catch_resume_at.strftime("%H:%M:%S"))) + self.bot.skipped_pokemon.append(pokemon) self.emit_event( 'pokemon_appeared', - formatted='Skip ignored {}! (CP {}) (Potential {}) (A/D/S {})'.format(pokemon.name, pokemon.cp, pokemon.iv, pokemon.iv_display), + formatted='Skip ignored {pokemon}! (CP: {cp} IV: {iv} A/D/S {iv_display})', data={ 'pokemon': pokemon.name, - 'cp': pokemon.cp, - 'iv': pokemon.iv, - 'iv_display': pokemon.iv_display, + 'cp': str(int(pokemon.cp)), + 'iv': str(pokemon.iv), + 'iv_display': str(pokemon.iv_display), } ) return WorkerResult.SUCCESS @@ -161,7 +174,7 @@ def work(self, response_dict=None): # log encounter self.emit_event( 'pokemon_appeared', - formatted='*A wild {} appeared!* (CP: {}) (NCP: {}) (Potential {}) (A/D/S {})'.format(pokemon.name, pokemon.cp, round(pokemon.cp_percent, 2), pokemon.iv, pokemon.iv_display), + formatted='A wild {} appeared! (CP: {} IV: {} A/D/S {} NCP: {})'.format(pokemon.name, pokemon.cp, pokemon.iv, pokemon.iv_display, round(pokemon.cp_percent, 2),), data={ 'pokemon': pokemon.name, 'ncp': round(pokemon.cp_percent, 2), @@ -188,10 +201,10 @@ def work(self, response_dict=None): c.execute("SELECT DISTINCT COUNT(encounter_id) FROM catch_log WHERE dated >= datetime('now','-1 day')") result = c.fetchone() - self.caught_last_24_hour = result[0] + while True: - if self.caught_last_24_hour < self.daily_catch_limit: + if result[0] < self.daily_catch_limit: # catch that pokemon! encounter_id = self.pokemon['encounter_id'] catch_rate_by_ball = [0] + response['capture_probability']['capture_probability'] # offset so item ids match indces @@ -282,7 +295,7 @@ def _pokemon_matches_config(self, config, pokemon, default_logic='and'): if pokemon_config.get('catch_above_cp',-1) >= 0: if pokemon.cp >= pokemon_config.get('catch_above_cp'): catch_results['cp'] = True - + if pokemon_config.get('catch_below_cp',-1) >= 0: if pokemon.cp <= pokemon_config.get('catch_below_cp'): catch_results['cp'] = True @@ -316,13 +329,13 @@ def _pokemon_matches_config(self, config, pokemon, default_logic='and'): cr['cp'] = True elif catch_logic == 'orand': cr['cp'] = True, - cr['iv'] = True - + cr['iv'] = True + if pokemon_config.get('catch_above_ncp',-1) >= 0: cr['ncp'] = catch_results['ncp'] if pokemon_config.get('catch_above_cp',-1) >= 0: cr['cp'] = catch_results['cp'] if pokemon_config.get('catch_below_cp',-1) >= 0: cr['cp'] = catch_results['cp'] if pokemon_config.get('catch_above_iv',-1) >= 0: cr['iv'] = catch_results['iv'] - + if DEBUG_ON: print "Debug information for match rules..." print "catch_results ncp = {}".format(catch_results['ncp']) @@ -629,27 +642,34 @@ def _do_catch(self, pokemon, encounter_id, catch_rate_by_ball, is_vip=False): awards = response_dict['responses']['CATCH_POKEMON']['capture_award'] exp_gain, candy_gain, stardust_gain = self.extract_award(awards) + with self.bot.database as conn: + c = conn.cursor() + c.execute( + "SELECT DISTINCT COUNT(encounter_id) FROM catch_log WHERE dated >= datetime('now','-1 day')") + + result = c.fetchone() self.emit_event( 'pokemon_caught', - formatted='Captured {pokemon}! [CP {cp}] [NCP {ncp}] [Potential {iv}] [{iv_display}] ({caught_last_24_hour}/{daily_catch_limit}) [+{exp} exp] [+{stardust} stardust]', + formatted='Captured {pokemon}! (CP: {cp} IV: {iv} {iv_display} NCP: {ncp}) Catch Limit: ({caught_last_24_hour}/{daily_catch_limit}) +{exp} exp +{stardust} stardust', data={ 'pokemon': pokemon.name, - 'ncp': round(pokemon.cp_percent, 2), - 'cp': pokemon.cp, - 'iv': pokemon.iv, - 'iv_display': pokemon.iv_display, - 'exp': exp_gain, + 'ncp': str(round(pokemon.cp_percent, 2)), + 'cp': str(int(pokemon.cp)), + 'iv': str(pokemon.iv), + 'iv_display': str(pokemon.iv_display), + 'exp': str(exp_gain), 'stardust': stardust_gain, - 'encounter_id': self.pokemon['encounter_id'], - 'latitude': self.pokemon['latitude'], - 'longitude': self.pokemon['longitude'], - 'pokemon_id': pokemon.pokemon_id, - 'caught_last_24_hour': self.caught_last_24_hour + 1, - 'daily_catch_limit': self.daily_catch_limit + 'encounter_id': str(self.pokemon['encounter_id']), + 'latitude': str(self.pokemon['latitude']), + 'longitude': str(self.pokemon['longitude']), + 'pokemon_id': str(pokemon.pokemon_id), + 'caught_last_24_hour': str(result[0]), + 'daily_catch_limit': str(self.daily_catch_limit) } ) + inventory.pokemons().add(pokemon) inventory.player().exp += exp_gain self.bot.stardust += stardust_gain diff --git a/pokemongo_bot/cell_workers/pokemon_optimizer.py b/pokemongo_bot/cell_workers/pokemon_optimizer.py index b1f6b594a0..df51ec4a38 100644 --- a/pokemongo_bot/cell_workers/pokemon_optimizer.py +++ b/pokemongo_bot/cell_workers/pokemon_optimizer.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + +# import datetime import difflib import itertools import json @@ -42,6 +45,14 @@ def initialize(self): if self.config.get("keep", None) is not None: raise ConfigException("Pokemon Optimizer configuration has changed. See docs/pokemon_optimized.md or configs/config.json.optimizer.example") +# log_file_path = os.path.join(_base_dir, "data", "pokemon-optimizer-%s.log" % self.bot.config.username) +# +# with open(log_file_path, "a") as _: +# pass +# +# self.log_file = open(log_file_path, "r+") +# self.log_file.seek(0, 2) + self.config_min_slots_left = self.config.get("min_slots_left", 5) self.config_action_wait_min = self.config.get("action_wait_min", 3) self.config_action_wait_max = self.config.get("action_wait_max", 5) @@ -56,13 +67,13 @@ def initialize(self): self.config_upgrade = self.config.get("upgrade", False) self.config_upgrade_level = self.config.get("upgrade_level", 30) self.config_groups = self.config.get("groups", {"gym": ["Dragonite", "Snorlax", "Lapras", "Arcanine"]}) - self.config_rules = self.config.get("rules", [{"mode": "overall", "top": 1, "sort": ["max_cp", "cp"], "keep": {"candy":-124}, "evolve": False, "buddy": True}, + self.config_rules = self.config.get("rules", [{"mode": "overall", "top": 1, "sort": ["max_cp", "cp"], "keep": {"candy": -124}, "evolve": False, "buddy": True}, {"mode": "overall", "top": 1, "sort": ["-candy", "max_cp", "cp"], "evolve": False, "buddy": True}, {"mode": "by_family", "top": 3, "names": ["gym"], "sort": ["iv", "ncp"], "evolve": {"iv": 0.9, "ncp": 0.9}, "upgrade": {"iv": 0.9, "ncp": 0.9}}, {"mode": "by_family", "top": 1, "sort": ["iv"], "evolve": {"iv": 0.9}}, {"mode": "by_family", "top": 1, "sort": ["ncp"], "evolve": {"ncp": 0.9}}, {"mode": "by_family", "top": 1, "sort": ["cp"], "evolve": False}, - {"mode": "by_pokemon", "names": ["!with_next_evolution"], "top": 1, "sort": ["dps_attack"], "keep": {"iv": 0.9}}]) + {"mode": "by_pokemon", "names": ["!with_next_evolution"], "top": 1, "sort": ["dps_attack", "iv"], "keep": {"iv": 0.9}}]) if (not self.config_may_use_lucky_egg) and self.config_evolve_only_with_lucky_egg: self.config_evolve = False @@ -86,6 +97,13 @@ def initialize(self): if pokemon.prev_evolutions_all: self.config_groups["with_previous_evolution"].append(pokemon.name) +# def log(self, txt): +# if self.log_file.tell() >= 1024 * 1024: +# self.log_file.seek(0, 0) +# +# self.log_file.write("[%s] %s\n" % (datetime.datetime.now().isoformat(str(" ")), txt)) +# self.log_file.flush() + def get_pokemon_slot_left(self): pokemon_count = inventory.Pokemons.get_space_used() @@ -312,10 +330,12 @@ def get_family_names(self, family_id): return [inventory.pokemons().name_for(x) for x in ids] def get_closest_name(self, name): - closest_names = difflib.get_close_matches(name, self.pokemon_names, 1) + mapping = {ord(x): ord(y) for x, y in zip("\u2641\u2642.-", "fm ")} + clean_names = {n.lower().translate(mapping): n for n in self.pokemon_names} + closest_names = difflib.get_close_matches(name.lower().translate(mapping), clean_names.keys(), 1) if closest_names: - closest_name = closest_names[0] + closest_name = clean_names[closest_names[0]] if name != closest_name: self.logger.warning("Unknown Pokemon name [%s]. Assuming it is [%s]", name, closest_name) @@ -338,6 +358,83 @@ def get_pokemon_id(self, pokemon): def get_family_id(self, pokemon): return pokemon.first_evolution_id + def score_and_sort(self, pokemon_list, rule): + pokemon_list = list(pokemon_list) + +# self.log("Pokemon %s" % pokemon_list) +# self.log("Rule %s" % rule) + + for pokemon in pokemon_list: + setattr(pokemon, "__score__", self.get_score(pokemon, rule)) + + keep = [p for p in pokemon_list if p.__score__[1] is True] + keep.sort(key=lambda p: p.__score__[0], reverse=True) + + return keep + + def get_score(self, pokemon, rule): + score = [] + + for a in rule.get("sort", []): + if a[0] == "-": + value = -getattr(pokemon, a[1:], 0) + else: + value = getattr(pokemon, a, 0) + + score.append(value) + + rule_keep = rule.get("keep", True) + rule_evolve = rule.get("evolve", True) + rule_upgrade = rule.get("upgrade", False) + rule_buddy = rule.get("buddy", False) + + keep = rule_keep not in [False, {}] + keep &= self.satisfy_requirements(pokemon, rule_keep) + + may_try_evolve = (hasattr(pokemon, "has_next_evolution") and pokemon.has_next_evolution()) + may_try_evolve &= rule_evolve not in [False, {}] + may_try_evolve &= self.satisfy_requirements(pokemon, rule_evolve) + + may_try_upgrade = rule_upgrade not in [False, {}] + may_try_upgrade &= self.satisfy_requirements(pokemon, rule_upgrade) + + may_buddy = rule_buddy not in [False, {}] + may_buddy &= pokemon.in_fort is False + may_buddy &= self.satisfy_requirements(pokemon, may_buddy) + +# self.log("%s %s %s %s %s %s" % (pokemon, tuple(score), keep, may_try_evolve, may_try_upgrade, may_buddy)) + + return tuple(score), keep, may_try_evolve, may_try_upgrade, may_buddy + + def satisfy_requirements(self, pokemon, req): + if type(req) is bool: + return req + + satisfy = True + + for a, v in req.items(): + value = getattr(pokemon, a, 0) + + if (type(v) is str) or (type(v) is unicode): + v = float(v) + + if type(v) is list: + if type(v[0]) is list: + satisfy_range = False + + for r in v: + satisfy_range |= (value >= r[0]) and (value <= r[1]) + + satisfy &= satisfy_range + else: + satisfy &= (value >= v[0]) and (value <= v[1]) + elif v < 0: + satisfy &= (value <= abs(v)) + else: + satisfy &= (value >= v) + + return satisfy + def get_best_pokemon_for_rule(self, pokemon_list, rule): pokemon_list = list(pokemon_list) @@ -419,82 +516,6 @@ def get_better_pokemon(self, pokemon_list, worst, limit=1000): return keep, try_evolve, try_upgrade, buddy - def score_and_sort(self, pokemon_list, rule): - pokemon_list = list(pokemon_list) - - for pokemon in pokemon_list: - setattr(pokemon, "__score__", self.get_score(pokemon, rule)) - - keep = [p for p in pokemon_list if p.__score__[1] is True] - keep.sort(key=lambda p: p.__score__[0], reverse=True) - - return keep - - def get_score(self, pokemon, rule): - score = [] - - for a in rule.get("sort", []): - if a[0] == "-": - value = -getattr(pokemon, a[1:], 0) - else: - value = getattr(pokemon, a, 0) - - score.append(value) - - rule_keep = rule.get("keep", True) - rule_evolve = rule.get("evolve", True) - rule_upgrade = rule.get("upgrade", False) - rule_buddy = rule.get("buddy", False) - - keep = rule_keep not in [False, {}] - keep &= self.satisfy_requirements(pokemon, rule_keep) - - may_try_evolve = (hasattr(pokemon, "has_next_evolution") and pokemon.has_next_evolution()) - may_try_evolve &= rule_evolve not in [False, {}] - may_try_evolve &= self.satisfy_requirements(pokemon, rule_evolve) - - may_try_upgrade = rule_upgrade not in [False, {}] - may_try_upgrade &= self.satisfy_requirements(pokemon, rule_upgrade) - - may_buddy = rule_buddy not in [False, {}] - may_buddy &= pokemon.in_fort is False - may_buddy &= self.satisfy_requirements(pokemon, may_buddy) - - return tuple(score), keep, may_try_evolve, may_try_upgrade, may_buddy - - def satisfy_requirements(self, pokemon, req): - if type(req) is bool: - return req - - satisfy = True - - for a, v in req.items(): - value = getattr(pokemon, a, 0) - - if (type(v) is str) or (type(v) is unicode): - v = float(v) - - if type(v) is list: - if type(v[0]) is list: - satisfy_range = False - - for r in v: - satisfy_range |= (value >= r[0]) and (value <= r[1]) - - satisfy &= satisfy_range - else: - satisfy &= (value >= v[0]) and (value <= v[1]) - elif v < 0: - satisfy &= (value <= abs(v)) - else: - satisfy &= (value >= v) - - return satisfy - - def unique_pokemon_list(self, pokemon_list): - seen = set() - return [p for p in pokemon_list if not (p.unique_id in seen or seen.add(p.unique_id))] - def get_evolution_plan(self, family_id, family_list, keep, try_evolve, try_upgrade): candies = inventory.candies().get(family_id).quantity family_name = inventory.Pokemons().name_for(family_id) @@ -557,7 +578,7 @@ def get_evolution_plan(self, family_id, family_list, keep, try_evolve, try_upgra upgrade.append(pokemon) - if family_name in self.config_evolve_for_xp_blacklist: + if (not self.config_evolve_for_xp) or (family_name in self.config_evolve_for_xp_blacklist): xp = [] transfer = crap elif self.config_evolve_for_xp_whitelist and (family_name not in self.config_evolve_for_xp_whitelist): @@ -583,6 +604,10 @@ def get_evolution_plan(self, family_id, family_list, keep, try_evolve, try_upgra return (transfer, evolve, upgrade, xp) + def unique_pokemon_list(self, pokemon_list): + seen = set() + return [p for p in pokemon_list if not (p.unique_id in seen or seen.add(p.unique_id))] + def apply_optimization(self, transfer, evolve, upgrade, xp): transfer_count = len(transfer) evolve_count = len(evolve) @@ -861,8 +886,12 @@ def get_buddy_walked(self, pokemon): if not success: return False - family_candy_id = response_dict.get("responses", {}).get("GET_BUDDY_WALKED", {}).get("family_candy_id", 0) candy_earned_count = response_dict.get("responses", {}).get("GET_BUDDY_WALKED", {}).get("candy_earned_count", 0) + + if candy_earned_count == 0: + return + + family_candy_id = self.get_family_id(pokemon) candy = inventory.candies().get(family_candy_id) if not self.bot.config.test: diff --git a/pokemongo_bot/cell_workers/sniper.py b/pokemongo_bot/cell_workers/sniper.py index 93a7f0fe92..958bb25a49 100644 --- a/pokemongo_bot/cell_workers/sniper.py +++ b/pokemongo_bot/cell_workers/sniper.py @@ -4,6 +4,8 @@ import json import requests import calendar +import difflib +import hashlib from random import uniform from operator import itemgetter, methodcaller @@ -56,7 +58,7 @@ def fetch(self): for result in results: iv = result.get(self.mappings.iv.param) id = result.get(self.mappings.id.param) - name = self._fixname(result.get(self.mappings.name.param)) + name = self._get_closest_name(self._fixname(result.get(self.mappings.name.param))) latitude = result.get(self.mappings.latitude.param) longitude = result.get(self.mappings.longitude.param) expiration = result.get(self.mappings.expiration.param) @@ -152,10 +154,25 @@ def validate(self): raise def _fixname(self,name): - name = name.replace("mr-mime","mr. mime") - name = name.replace("farfetchd","farfetch'd") + if name: + name = name.replace("mr-mime","mr. mime") + name = name.replace("farfetchd","farfetch'd") + name = name.replace("Nidoran\u2642","nidoran m") + name = name.replace("Nidoran\u2640","nidoran f") return name + def _get_closest_name(self, name): + if not name: + return + + pokemon_names = [p.name for p in inventory.pokemons().STATIC_DATA] + closest_names = difflib.get_close_matches(name, pokemon_names, 1) + + if closest_names: + closest_name = closest_names[0] + return closest_name + + return name # Represents the JSON params mappings class SniperSourceMapping(object): @@ -210,13 +227,12 @@ class Sniper(BaseTask): MIN_SECONDS_ALLOWED_FOR_CELL_CHECK = 10 MIN_SECONDS_ALLOWED_FOR_REQUESTING_DATA = 5 MIN_BALLS_FOR_CATCHING = 10 - MAX_CACHE_LIST_SIZE = 200 + MAX_CACHE_LIST_SIZE = 300 def __init__(self, bot, config): super(Sniper, self).__init__(bot, config) def initialize(self): - self.cache = [] self.disabled = False self.last_cell_check_time = time.time() self.last_data_request_time = time.time() @@ -232,6 +248,9 @@ def initialize(self): self.altitude = uniform(self.bot.config.alt_min, self.bot.config.alt_max) self.sources = [SniperSource(data) for data in self.config.get('sources', [])] + if not hasattr(self.bot,"sniper_cache"): + self.bot.sniper_cache = [] + # Dont bother validating config if task is not even enabled if self.enabled: # Validate ordering @@ -273,11 +292,6 @@ def is_snipeable(self, pokemon): self._trace('{} is expired! Skipping...'.format(pokemon.get('pokemon_name'))) return False - # Skip if already cached - if self._is_cached(pokemon): - self._trace('{} was already handled! Skipping...'.format(pokemon.get('pokemon_name', ''))) - return False - # Skip if not enought balls. Sniping wastes a lot of balls. Theres no point to let user decide this amount if all_balls_count < self.MIN_BALLS_FOR_CATCHING: self._trace('Not enought balls left! Skipping...') @@ -311,60 +325,70 @@ def snipe(self, pokemon): if not self.is_snipeable(pokemon): self._trace('{} is not snipeable! Skipping...'.format(pokemon['pokemon_name'])) else: - # Backup position before anything - last_position = self.bot.position[0:2] - - # Teleport, so that we can see nearby stuff - self.bot.hb_locked = True - self._teleport_to(pokemon) - - # If social is enabled and if no verification is needed, trust it. Otherwise, update IDs! - verify = not pokemon.get('encounter_id') or not pokemon.get('spawn_point_id') - exists = not verify and self.mode == SniperMode.SOCIAL - success = exists - - # If information verification have to be done, do so - if verify: - seconds_since_last_check = time.time() - self.last_cell_check_time - - # Wait a maximum of MIN_SECONDS_ALLOWED_FOR_CELL_CHECK seconds before requesting nearby cells - if (seconds_since_last_check < self.MIN_SECONDS_ALLOWED_FOR_CELL_CHECK): - time.sleep(self.MIN_SECONDS_ALLOWED_FOR_CELL_CHECK - seconds_since_last_check) - - nearby_pokemons = [] - nearby_stuff = self.bot.get_meta_cell() - self.last_cell_check_time = time.time() - - # Retrieve nearby pokemons for validation - nearby_pokemons.extend(nearby_stuff.get('wild_pokemons', [])) - nearby_pokemons.extend(nearby_stuff.get('catchable_pokemons', [])) - - # Make sure the target really/still exists (nearby_pokemon key names are game-bound!) - for nearby_pokemon in nearby_pokemons: - nearby_pokemon_id = nearby_pokemon.get('pokemon_data', {}).get('pokemon_id') or nearby_pokemon.get('pokemon_id') - - # If we found the target, it exists and will very likely be encountered/caught (success) - if nearby_pokemon_id == pokemon.get('pokemon_id', 0): - exists = True - success = True - - # Also, if the IDs arent valid, override them (nearby_pokemon key names are game-bound!) with game values - if not pokemon.get('encounter_id') or not pokemon.get('spawn_point_id'): - pokemon['encounter_id'] = nearby_pokemon['encounter_id'] - pokemon['spawn_point_id'] = nearby_pokemon['spawn_point_id'] - break - - # If target exists, catch it, otherwise ignore - if exists: - self._log('Yay! There really is a wild {} nearby!'.format(pokemon.get('pokemon_name'))) - self._teleport_back_and_catch(last_position, pokemon) + # Have we already tried this pokemon? + if not hasattr(self.bot,'sniper_unique_pokemon'): + self.bot.sniper_unique_pokemon = [] + + # Check if already in list of pokemon we've tried + uniqueid = self._build_unique_id(pokemon) + if self._is_cached(uniqueid): + # Do nothing. Either we already got this, or it doesn't really exist + self._trace('{} was already handled! Skipping...'.format(pokemon['pokemon_name'])) else: - self._error('Damn! Its not here. Reasons: too far, caught, expired or fake data. Skipping...') - self._teleport_back(last_position) - - # Save target and unlock heartbeat calls - self._cache(pokemon) - self.bot.hb_locked = False + # Backup position before anything + last_position = self.bot.position[0:2] + + # Teleport, so that we can see nearby stuff + self.bot.hb_locked = True + self._teleport_to(pokemon) + + # If social is enabled and if no verification is needed, trust it. Otherwise, update IDs! + verify = not pokemon.get('encounter_id') or not pokemon.get('spawn_point_id') + exists = not verify and self.mode == SniperMode.SOCIAL + success = exists + + # If information verification have to be done, do so + if verify: + seconds_since_last_check = time.time() - self.last_cell_check_time + + # Wait a maximum of MIN_SECONDS_ALLOWED_FOR_CELL_CHECK seconds before requesting nearby cells + if (seconds_since_last_check < self.MIN_SECONDS_ALLOWED_FOR_CELL_CHECK): + time.sleep(self.MIN_SECONDS_ALLOWED_FOR_CELL_CHECK - seconds_since_last_check) + + nearby_pokemons = [] + nearby_stuff = self.bot.get_meta_cell() + self.last_cell_check_time = time.time() + + # Retrieve nearby pokemons for validation + nearby_pokemons.extend(nearby_stuff.get('wild_pokemons', [])) + nearby_pokemons.extend(nearby_stuff.get('catchable_pokemons', [])) + + # Make sure the target really/still exists (nearby_pokemon key names are game-bound!) + for nearby_pokemon in nearby_pokemons: + nearby_pokemon_id = nearby_pokemon.get('pokemon_data', {}).get('pokemon_id') or nearby_pokemon.get('pokemon_id') + + # If we found the target, it exists and will very likely be encountered/caught (success) + if nearby_pokemon_id == pokemon.get('pokemon_id', 0): + exists = True + success = True + + # Also, if the IDs arent valid, override them (nearby_pokemon key names are game-bound!) with game values + if not pokemon.get('encounter_id') or not pokemon.get('spawn_point_id'): + pokemon['encounter_id'] = nearby_pokemon['encounter_id'] + pokemon['spawn_point_id'] = nearby_pokemon['spawn_point_id'] + break + + # If target exists, catch it, otherwise ignore + if exists: + self._log('Yay! There really is a wild {} nearby!'.format(pokemon.get('pokemon_name'))) + self._teleport_back_and_catch(last_position, pokemon) + else: + self._error('Damn! Its not here. Reasons: too far, caught, expired or fake data. Skipping...') + self._teleport_back(last_position) + + # Save target and unlock heartbeat calls + self._cache(uniqueid) + self.bot.hb_locked = False return success @@ -372,7 +396,15 @@ def work(self): # Do nothing if this task was invalidated if self.disabled: self._error("Sniper was disabled for some reason. Scroll up to find out.") + + elif self.bot.catch_disabled: + if not hasattr(self.bot,"sniper_disabled_global_warning") or \ + (hasattr(self.bot,"sniper_disabled_global_warning") and not self.bot.sniper_disabled_global_warning): + self._log("All catching tasks are currently disabled until {}. Sniper will resume when catching tasks are re-enabled".format(self.bot.catch_resume_at.strftime("%H:%M:%S"))) + self.bot.sniper_disabled_global_warning = True + else: + self.bot.sniper_disabled_global_warning = False targets = [] # Retrieve the targets @@ -384,7 +416,6 @@ def work(self): if targets: # Order the targets (descending) targets = sorted(targets, key=itemgetter(*self.order), reverse=True) - shots = 0 # For as long as there are targets available, try to snipe untill we run out of bullets @@ -468,20 +499,25 @@ def _hash(self, pokemon): def _equals(self, pokemon_1, pokemon_2): return self._hash(pokemon_1) == self._hash(pokemon_2) - def _is_cached(self, pokemon): - for cached_pokemon in self.cache: - if self._equals(pokemon, cached_pokemon): - return True - + def _is_cached(self, uniqueid): + if uniqueid in self.bot.sniper_cache: + return True return False - def _cache(self, pokemon): - # Skip repeated items - if not self._is_cached(pokemon): + def _cache(self, uniqueid): + if not self._is_cached(uniqueid): # Free space if full and store it - if len(self.cache) >= self.MAX_CACHE_LIST_SIZE: - self.cache.pop(0) - self.cache.append(pokemon) + if len(self.bot.sniper_cache) >= self.MAX_CACHE_LIST_SIZE: + self.bot.sniper_cache.pop(0) + self.bot.sniper_cache.append(uniqueid) + + def _build_unique_id(self, pokemon): + # Build unique id for this pokemon from id, latitude, longitude and expiration + uniqueid = str(pokemon.get('pokemon_id','')) + str(pokemon.get('latitude','')) + str(pokemon.get('longitude','')) + str(pokemon.get('expiration','')) + md5str = hashlib.md5() + md5str.update(uniqueid) + uniqueid = str(md5str.hexdigest()) + return uniqueid def _log(self, message): self.emit_event('sniper_log', formatted='{message}', data={'message': message}) @@ -514,7 +550,7 @@ def _teleport_back(self, position_array): self._teleport(position_array[0], position_array[1], self.altitude) def _teleport_back_and_catch(self, position_array, pokemon): - catch_worker = PokemonCatchWorker(pokemon, self.bot, self.config) + catch_worker = PokemonCatchWorker(pokemon, self.bot) api_encounter_response = catch_worker.create_encounter_api_call() self._teleport_back(position_array) catch_worker.work(api_encounter_response) \ No newline at end of file diff --git a/pokemongo_bot/cell_workers/spin_fort.py b/pokemongo_bot/cell_workers/spin_fort.py index 2d0014dd0c..2e231f8cc2 100644 --- a/pokemongo_bot/cell_workers/spin_fort.py +++ b/pokemongo_bot/cell_workers/spin_fort.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import +from datetime import datetime, timedelta import sys import time @@ -27,9 +28,14 @@ def __init__(self, bot, config): super(SpinFort, self).__init__(bot, config) def initialize(self): + # 10 seconds from current time + self.next_update = datetime.now() + timedelta(0, 10) + self.ignore_item_count = self.config.get("ignore_item_count", False) self.spin_wait_min = self.config.get("spin_wait_min", 2) self.spin_wait_max = self.config.get("spin_wait_max", 3) + self.min_interval = int(self.config.get('min_interval', 120)) + self.exit_on_limit_reached = self.config.get("exit_on_limit_reached", True) def should_run(self): has_space_for_loot = inventory.Items.has_space_for_loot() @@ -40,9 +46,23 @@ def should_run(self): ) return self.ignore_item_count or has_space_for_loot + def work(self): forts = self.get_forts_in_range() + with self.bot.database as conn: + c = conn.cursor() + c.execute("SELECT DISTINCT COUNT(pokestop) FROM pokestop_log WHERE dated >= datetime('now','-1 day')") + if c.fetchone()[0] >= self.config.get('daily_spin_limit', 2000): + if self.exit_on_limit_reached: + self.emit_event('spin_limit', formatted='WARNING! You have reached your daily spin limit') + sys.exit(2) + + if datetime.now() >= self.next_update: + self.emit_event('spin_limit', formatted='WARNING! You have reached your daily spin limit') + self._compute_next_update() + return WorkerResult.SUCCESS + if not self.should_run() or len(forts) == 0: return WorkerResult.SUCCESS @@ -98,10 +118,6 @@ def work(self): c = conn.cursor() c.execute("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name='pokestop_log'") result = c.fetchone() - c.execute("SELECT DISTINCT COUNT(pokestop) FROM pokestop_log WHERE dated >= datetime('now','-1 day')") - if c.fetchone()[0] >= self.config.get('daily_spin_limit', 2000): - self.emit_event('spin_limit', formatted='WARNING! You have reached your daily spin limit') - sys.exit(2) while True: if result[0] == 1: conn.execute('''INSERT INTO pokestop_log (pokestop, exp, items) VALUES (?, ?, ?)''', (fort_name, str(experience_awarded), str(items_awarded))) @@ -229,3 +245,11 @@ def get_items_awarded_from_fort_spinned(self, response_dict): # TODO : Refactor this class, hide the inventory update right after the api call def _update_inventory(self, item_awarded): inventory.items().get(item_awarded['item_id']).add(item_awarded['item_count']) + + def _compute_next_update(self): + """ + Computes the next update datetime based on the minimum update interval. + :return: Nothing. + :rtype: None + """ + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) diff --git a/pokemongo_bot/event_handlers/chat_handler.py b/pokemongo_bot/event_handlers/chat_handler.py index 03fe959e18..196f8449c8 100644 --- a/pokemongo_bot/event_handlers/chat_handler.py +++ b/pokemongo_bot/event_handlers/chat_handler.py @@ -1,263 +1,204 @@ # -*- coding: utf-8 -*- import re from pokemongo_bot import inventory -import telegram -import time -DEBUG_ON = False +from pokemongo_bot import metrics +DEBUG_ON = False class ChatHandler: def __init__(self, bot, pokemons): self.bot = bot self.pokemons = pokemons - self._tbot = telegram.Bot(self.bot.config.telegram_token) + self.metrics = metrics.Metrics(bot) - def get_player_stats(self): - stats = inventory.player().player_stats - if stats: - with self.bot.database as conn: - cur = conn.cursor() - cur.execute( - "SELECT COUNT(DISTINCT encounter_id) FROM catch_log WHERE dated >= datetime('now','-1 day')") - catch_day = cur.fetchone()[0] - cur.execute("SELECT COUNT(pokestop) FROM pokestop_log WHERE dated >= datetime('now','-1 day')") - ps_day = cur.fetchone()[0] - res = ( - "*" + self.bot.config.username + "*", - "_Level:_ " + str(stats["level"]), - "_XP:_ " + str(stats["experience"]) + "/" + str(stats["next_level_xp"]), - "_Pokemons Captured:_ " + str(stats.get("pokemons_captured",0)) + " (" + str(catch_day) + " _last 24h_)", - "_Poke Stop Visits:_ " + str(stats.get("poke_stop_visits",0)) + " (" + str(ps_day) + " _last 24h_)", - "_KM Walked:_ " + str("%.2f" % stats.get("km_walked",0)) - ) - return (res) - else: - return ("Stats not loaded yet\n") - def get_event(self, event, formatted_msg, data): - msg = None - if event == 'level_up': - msg = "level up ({})".format(data["current_level"]) - if event == 'pokemon_caught': - trigger = None - if data["pokemon"] in self.pokemons: - trigger = self.pokemons[data["pokemon"]] - elif "all" in self.pokemons: - trigger = self.pokemons["all"] - if trigger: - if ((not "operator" in trigger or trigger["operator"] == "and") and data["cp"] >= trigger["cp"] and data["iv"] >= trigger["iv"]) or \ - ("operator" in trigger and trigger["operator"] == "or" and (data["cp"] >= trigger["cp"] or data["iv"] >= trigger["iv"])): - msg = "Caught {} CP: {}, IV: {}".format(data["pokemon"], data["cp"], data["iv"]) - if event == 'egg_hatched': - msg = "Egg hatched with a {} CP: {}, IV: {} (A/D/S {})".format(data["name"], data["cp"], data["iv_pct"], data["iv_ads"]) - if event == 'bot_sleep': - msg = "I am too tired, I will take a sleep till {}.".format(data["wake"]) - if event == 'catch_limit': - msg = "*You have reached your daily catch limit, quitting.*" - if event == 'spin_limit': - msg = "*You have reached your daily spin limit, quitting.*" - if msg == None: - return formatted_msg - else: - return msg - - def get_events(self, update): - cmd = update.message.text.split(" ", 1) - if len(cmd) > 1: - # we have a filter - event_filter = ".*{}.*".format(cmd[1]) - else: - # no filter - event_filter = ".*" - return sorted(filter(lambda k: re.match(event_filter, k), self.bot.event_manager._registered_events.keys())) - - def sendMessage(self, chat_id=None, parse_mode='Markdown', text=None): - try: - self._tbot.sendMessage(chat_id=chat_id, parse_mode=parse_mode, text=text) - except telegram.error.NetworkError: - time.sleep(1) - except telegram.error.TelegramError: - time.sleep(10) - except telegram.error.Unauthorized: - self.update_id += 1 - - def sendLocation(self, chat_id, latitude, longitude): - try: - self._tbot.send_location(chat_id=chat_id, latitude=latitude, longitude=longitude) - except telegram.error.NetworkError: - time.sleep(1) - except telegram.error.TelegramError: - time.sleep(10) - except telegram.error.Unauthorized: - self.update_id += 1 - - def send_player_stats_to_chat(self, chat_id): - stats = self.get_player_stats() - if stats: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="\n".join(stats)) - self.sendLocation(chat_id=chat_id, latitude=self.bot.api._position_lat, longitude=self.bot.api._position_lng) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="Stats not loaded yet\n") - - def showtop(self, chatid, num, order): + def get_evolved(self, num, order): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: + if order not in ["cp", "iv", "dated"]: order = "iv" - pkmns = sorted(inventory.pokemons().all(), key=lambda p: getattr(p, order), reverse=True)[:num] - - outMsg = "\n".join(["*{}* (_CP:_ {}) (_IV:_ {}) (Candy:{})".format(p.name, p.cp, p.iv, - inventory.candies().get(p.pokemon_id).quantity) for p - in pkmns]) - self.sendMessage(chat_id=chatid, parse_mode='Markdown', text=outMsg) - - def evolve(self, chatid, uid): - # TODO: here comes evolve logic (later) - self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Evolve logic not implemented yet") - return + with self.bot.database as conn: + cur = conn.cursor() + cur.execute("SELECT pokemon, cp, iv FROM catch_log ORDER BY " + order + " DESC LIMIT " + str(num)) + evolved = cur.fetchall() + return evolved - def upgrade(self, chatid, uid): - # TODO: here comes upgrade logic (later) - self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Upgrade logic not implemented yet") - return - def get_evolved(self, chat_id, num, order): + def get_softbans(self, num): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: - order = "iv" - with self.bot.database as conn: cur = conn.cursor() - cur.execute("SELECT * FROM evolve_log ORDER BY " + order + " DESC LIMIT " + str(num)) - evolved = cur.fetchall() - outMsg = '' - if evolved: - for x in evolved: - outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[2])) + ') (_IV:_ ' + str(x[1]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Evolutions Found.\n") + cur.execute("SELECT * FROM softban_log DESC LIMIT " + str(num)) + softbans = cur.fetchall() + return softbans - def get_softban(self, chat_id): - with self.bot.database as conn: - cur = conn.cursor() - cur.execute("SELECT * FROM softban_log") - softban = cur.fetchall() - outMsg = '' - if softban: - for x in softban: - outMsg += '*' + x[0] + '* ' + '(' + str(x[2]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Softbans found! Good job!\n") - - def get_hatched(self, chat_id, num, order): + def get_hatched(self, num, order): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: + if order not in ["cp", "iv", "dated"]: order = "iv" + with self.bot.database as conn: cur = conn.cursor() - cur.execute("SELECT * FROM eggs_hatched_log ORDER BY " + order + " DESC LIMIT " + str(num)) + cur.execute("SELECT pokemon, cp, iv From eggs_hatched_log ORDER BY " + order + " DESC LIMIT " + str(num)) hatched = cur.fetchall() - outMsg = '' - if hatched: - for x in hatched: - outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ') (_IV:_ ' + str(x[2]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Eggs Hatched Yet.\n") + return hatched - def get_caught(self, chat_id, num, order): + def get_caught(self, num, order): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: + if order not in ["cp", "iv", "dated"]: order = "iv" with self.bot.database as conn: cur = conn.cursor() cur.execute("SELECT pokemon, cp, iv FROM catch_log ORDER BY " + order + " DESC LIMIT " + str(num)) caught = cur.fetchall() - outMsg = '' - if caught: - for x in caught: - outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ') (_IV:_ ' + str(x[2]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Pokemon Caught Yet.\n") + return caught + + def get_pokestops(self, num): + if not num.isnumeric(): + num = 10 + else: + num = int(num) - def get_pokestops(self, chat_id, num): with self.bot.database as conn: cur = conn.cursor() - cur.execute("SELECT * FROM pokestop_log ORDER BY dated DESC LIMIT " + str(num)) - pokestop = cur.fetchall() - outMsg = '' - if pokestop: - for x in pokestop: - outMsg += '*' + x[0] + '* ' + '(_XP:_ ' + str(x[1]) + ') (_Items:_ ' + str(x[2]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Pokestops Encountered Yet.\n") + cur.execute("SELECT * FROM pokestop_log DESC LIMIT " + str(num)) + pokestops = cur.fetchall() + return pokestops - def get_released(self, chat_id, num, order): + def get_released(self, num, order): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: + if order not in ["cp", "iv", "dated"]: order = "iv" + with self.bot.database as conn: cur = conn.cursor() - cur.execute("SELECT * FROM transfer_log ORDER BY " + order + " DESC LIMIT " + str(num)) - transfer = cur.fetchall() - outMsg = '' - if transfer: - for x in transfer: - outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[2])) + ') (_IV:_ ' + str(x[1]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Pokemon Released Yet.\n") + cur.execute("SELECT pokemon, cp, iv FROM transfer_log ORDER BY " + order + " DESC LIMIT " + str(num)) + released = cur.fetchall() + return released - def get_vanished(self, chat_id, num, order): + def get_vanished(self, num, order): if not num.isnumeric(): num = 10 else: num = int(num) - if order not in ["cp", "iv"]: + if order not in ["cp", "iv", "dated"]: order = "iv" + with self.bot.database as conn: cur = conn.cursor() - cur.execute("SELECT * FROM vanish_log ORDER BY " + order + " DESC LIMIT " + str(num)) + cur.execute("SELECT pokemon, cp, iv FROM vanish_log ORDER BY " + order + " DESC LIMIT " + str(num)) vanished = cur.fetchall() - outMsg = '' - if vanished: - for x in vanished: - outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ') (_IV:_ ' + str(x[2]) + ')\n' - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="".join(str(outMsg))) - else: - self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="No Pokemon Vanished Yet.\n") + return vanished + + def get_player_stats(self): + stats = inventory.player().player_stats + dust = self.get_dust() + if stats: + with self.bot.database as conn: + cur = conn.cursor() + cur.execute( + "SELECT COUNT(DISTINCT encounter_id) FROM catch_log WHERE dated >= datetime('now','-1 day')") + catch_day = cur.fetchone()[0] + cur.execute("SELECT COUNT(pokestop) FROM pokestop_log WHERE dated >= datetime('now','-1 day')") + ps_day = cur.fetchone()[0] + res = ( + self.bot.config.username, + str(stats["level"]), + str(stats["experience"]), + str(stats["next_level_xp"]), + str(stats["pokemons_captured"]), + str(catch_day), + str(stats["poke_stop_visits"]), + str(ps_day), + str("%.2f" % stats["km_walked"]), + str(dust) + + ) + return (res) + + def get_events(self, update): + cmd = update.message.text.split(" ", 1) + if len(cmd) > 1: + # we have a filter + event_filter = ".*{}-*".format(cmd[1]) + else: + # no filter + event_filter = ".*" + events = filter(lambda k: re.match(event_filter, k), self.bot.event_manager._registered_events.keys()) + events = sorted(events) + return events + + def get_event(self, event, formatted_msg, data): + msg = None + trigger = None + if event == 'level_up': + msg = "level up ({})".format(data["current_level"]) + if event == 'pokemon_caught': + if data["pokemon"] in self.pokemons: + trigger = self.pokemons[data["pokemon"]] + elif "all" in self.pokemons: + trigger = self.pokemons["all"] + if trigger: + if ((not "operator" in trigger or trigger["operator"] == "and") and data["cp"] >= trigger["cp"] and + data["iv"] >= trigger["iv"]) or \ + ("operator" in trigger and trigger["operator"] == "or" and ( + data["cp"] >= trigger["cp"] or data["iv"] >= trigger["iv"])): + msg = "Captured {}! (CP: {} IV: {} A/D/S {} NCP: {}) Catch Limit: ({}/{}) +{} exp +{} stardust".format(data["pokemon"], data["cp"], data["iv"], data["iv_display"], data["ncp"], data["caught_last_24_hour"], data["daily_catch_limit"], data["exp"], data["stardust"]) + if event == 'egg_hatched': + msg = "Egg hatched with a {} CP: {}, IV: {} (A/D/S {})".format(data["name"], data["cp"], data["iv_pct"], + data["iv_ads"]) + if event == 'bot_sleep': + msg = "I am too tired, I will take a sleep till {}.".format(data["wake"]) + if event == 'catch_limit': + msg = "*You have reached your daily catch limit, quitting.*" + if event == 'spin_limit': + msg = "*You have reached your daily spin limit, quitting.*" + if msg is None: + return formatted_msg + else: + return msg - def evolve(self, chatid, uid): - # TODO: here comes evolve logic (later) - self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Evolve logic not implemented yet") - return + def get_top(self, num, order): + if not num.isnumeric(): + num = 10 + else: + num = int(num) - def upgrade(self, chatid, uid): - # TODO: here comes upgrade logic (later) - self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Upgrade logic not implemented yet") - return + if order not in ["cp", "iv", "dated"]: + order = "iv" + pkmns = sorted(inventory.pokemons().all(), key=lambda p: getattr(p, order), reverse=True)[:num] + res = [] + for p in pkmns: + res.append([ + p.name, + p.cp, + p.iv, + inventory.candies().get(p.pokemon_id).quantity + ]) + + return res + + def get_dust(self): + dust = metrics.Metrics.total_stardust(self.metrics) + return dust diff --git a/pokemongo_bot/event_handlers/logging_handler.py b/pokemongo_bot/event_handlers/logging_handler.py index b433b7e4b6..f12544d0e6 100644 --- a/pokemongo_bot/event_handlers/logging_handler.py +++ b/pokemongo_bot/event_handlers/logging_handler.py @@ -61,6 +61,7 @@ class LoggingHandler(EventHandler): 'pokemon_capture_failed': 'red', 'pokemon_caught': 'blue', 'pokemon_evolved': 'green', + 'pokemon_evolve_check': 'green', 'pokemon_fled': 'red', 'pokemon_inventory_full': 'red', 'pokemon_nickname_invalid': 'red', @@ -122,7 +123,9 @@ class LoggingHandler(EventHandler): 'spun_fort': 'none', 'threw_berry': 'none', 'threw_pokeball': 'none', - 'used_lucky_egg': 'none' + 'used_lucky_egg': 'none', + 'catch_limit_on': 'yellow', + 'catch_limit_off': 'green' } COLOR_CODE = { 'gray': '\033[90m', diff --git a/pokemongo_bot/event_handlers/telegram_handler.py b/pokemongo_bot/event_handlers/telegram_handler.py index a7d459db60..fdf8fcb060 100644 --- a/pokemongo_bot/event_handlers/telegram_handler.py +++ b/pokemongo_bot/event_handlers/telegram_handler.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- from pokemongo_bot.event_manager import EventHandler -from pokemongo_bot.base_dir import _base_dir import time import telegram import thread import re -import pprint -from pokemongo_bot.datastore import Datastore from telegram.utils import request from chat_handler import ChatHandler @@ -22,6 +19,7 @@ def __init__(self, bot, pokemons, config): self.config = config self.chat_handler = ChatHandler(self.bot, pokemons) self.master = self.config.get('master') + with self.bot.database as conn: # initialize the DB table if it does not exist yet initiator = TelegramDBInit(bot.database) @@ -39,9 +37,11 @@ def __init__(self, bot, pokemons, config): else: # uid not known yet self.bot.logger.info("Telegram master UID not in datastore yet") + self.pokemons = pokemons self._tbot = None self.config = config + self.master = None def connect(self): if DEBUG_ON: self.bot.logger.info("Not connected. Reconnecting") @@ -87,194 +87,358 @@ def deauthenticate(self, update): sql = "delete from telegram_logins where uid = {}".format(update.message.chat_id) cur.execute(sql) conn.commit() - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Logout completed") + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Logout completed") return def authenticate(self, update): args = update.message.text.split(' ') if len(args) != 2: - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Invalid password") return password = args[1] if password != self.config.get('password', None): - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Invalid password") else: with self.bot.database as conn: cur = conn.cursor() cur.execute("insert or replace into telegram_logins(uid) values(?)",[update.message.chat_id]) conn.commit() - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text="Authentication successful, you can now use all commands") return + def sendMessage(self, chat_id=None, parse_mode='Markdown', text=None): + try: + if self._tbot is None: + self.connect() + self._tbot.sendMessage(chat_id=chat_id, parse_mode=parse_mode, text=text) + except telegram.error.NetworkError: + time.sleep(1) + except telegram.error.TelegramError: + time.sleep(10) + except telegram.error.Unauthorized: + self.update_id += 1 + + def sendLocation(self, chat_id, latitude, longitude): + try: + self._tbot.send_location(chat_id=chat_id, latitude=latitude, longitude=longitude) + except telegram.error.NetworkError: + time.sleep(1) + except telegram.error.TelegramError: + time.sleep(10) + except telegram.error.Unauthorized: + self.update_id += 1 + + def send_player_stats_to_chat(self, chat_id): + stats = self.chat_handler.get_player_stats() + if stats: + self.sendMessage(chat_id=chat_id, + parse_mode='Markdown', + text="*{}* \n_Level:_ {} \n_XP:_ {}/{} \n_Pokemons Captured:_ {} ({} _last 24h_) \n_Poke Stop Visits:_ {} ({} _last 24h_) \n_KM Walked:_ {} \n_Stardust:_ {}".format( + stats[0], stats[1], stats[2], stats[3], stats[4], stats[5], stats[6], stats[7], stats[8], stats[9])) + self.sendLocation(chat_id=chat_id, latitude=self.bot.api._position_lat, + longitude=self.bot.api._position_lng) + else: + self.sendMessage(chat_id=chat_id, parse_mode='Markdown', text="Stats not loaded yet\n") + + def send_event(self, event, formatted_msg, data): + return self.chat_handler.get_event(event, formatted_msg, data) + + def send_events(self, update): + events = self.chat_handler.get_events(update) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', + text="\n".join(events)) + + def send_softbans(self, update, num): + softbans = self.chat_handler.get_softbans(num) + outMsg = '' + if softbans: + for x in softbans: + outMsg += '*' + x[0] + '* ' + '(' + str(x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Softbans found! Good job!\n") + + def send_subscription_updated(self, update): + self.chsub(update.message.text, update.message.chat_id) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', + text=("Subscriptions updated.")) + + def send_info(self, update): + self.send_player_stats_to_chat(update.message.chat_id) + + def send_logout(self, update): + self.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', text=("Logged out.")) + self.deauthenticate(update) + + def send_caught(self, update, num, order): + caught = self.chat_handler.get_caught(num, order) + outMsg = '' + if caught: + for x in caught: + outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ' _IV:_ ' + str( + x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Pokemon Caught Yet.\n") + + def send_evolved(self, update, num, order): + evolved = self.chat_handler.get_evolved(num, order) + outMsg = '' + if evolved: + for x in evolved: + outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ' _IV:_ ' + str( + x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Evolutions Found.\n") + + def send_pokestops(self, update, num): + pokestops = self.chat_handler.get_pokestops( num) + outMsg = '' + if pokestops: + for x in pokestops: + outMsg += '*' + x[0] + '* ' + '(_XP:_ ' + str(x[1]) + ' _Items:_ ' + str(x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Pokestops Encountered Yet.\n") + + def send_hatched(self, update, num, order): + hatched = self.chat_handler.get_hatched(num, order) + outMsg = '' + if hatched: + for x in hatched: + outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ' _IV:_ ' + str( + x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Eggs Hatched Yet.\n") + + def send_released(self, update, num, order): + released = self.chat_handler.get_released(num, order) + outMsg = '' + if released: + for x in released: + outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ' _IV:_ ' + str( + x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="".join(outMsg)) + else: + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No Pokemon Released Yet.\n") + + def send_vanished(self, update, num, order): + vanished = self.chat_handler.get_vanished( num, order) + outMsg = '' + if vanished: + for x in vanished: + outMsg += '*' + x[0] + '* ' + '(_CP:_ ' + str(int(x[1])) + ' _IV:_ ' + str( + x[2]) + ')\n' + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text=outMsg) + + + + def send_top(self, update, num, order): + top = self.chat_handler.get_top(num, order) + outMsg = '' + for x in top: + outMsg += "*{}* _CP:_ {} _IV:_ {} (Candy: {})\n".format(x[0], x[1], x[2], x[3]) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', text=outMsg) + + + + def showsubs(self, update): + subs = [] + with self.bot.database as conn: + for sub in conn.execute("select uid, event_type, parameters from telegram_subscriptions where uid = ?", + [update.message.chat_id]).fetchall(): + subs.append("{} -> {}".format(sub[1], sub[2])) + if subs is []: subs.append( + "No subscriptions found. Subscribe using /sub EVENTNAME. For a list of events, send /events") + self.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', text="\n{}".join(subs)) + + def chsub(self, msg, chatid): + (cmd, evt, params) = self.tokenize(msg, 3) + if cmd == "/sub": + sql = "replace into telegram_subscriptions(uid, event_type, parameters) values (?, ?, ?)" + else: + if evt == "everything": + sql = "delete from telegram_subscriptions where uid = ? and (event_type = ? or parameters = ? or 1 = 1)" # does not look very elegant, but makes unsub'ing everythign possible + else: + sql = "delete from telegram_subscriptions where uid = ? and event_type = ? and parameters = ?" + + with self.bot.database as conn: + conn.execute(sql, [chatid, evt, params]) + conn.commit() + return + + def send_start(self, update): + res = ( + "*Commands: *", + "/info - info about bot", + "/login - authenticate with the bot; once authenticated, your ID will be registered with the bot and survive bot restarts", + "/logout - remove your ID from the 'authenticated' list", + "/sub - subscribe to eventName, with optional parameters, event name=all will subscribe to ALL events (LOTS of output!)", + "/unsub - unsubscribe from eventName; parameters must match the /sub parameters", + "/unsub everything - will remove all subscriptions for this uid", + "/showsubs - show current subscriptions", + "/events - show available events, filtered by regular expression ", + "/top - show top X pokemons, sorted by CP, IV, or Date", + "/evolved - show top x pokemon evolved, sorted by CP, IV, or Date", + "/hatched - show top x pokemon hatched, sorted by CP, IV, or Date", + "/caught - show top x pokemon caught, sorted by CP, IV, or Date", + "/pokestops - show last x pokestops visited", + "/released - show top x released, sorted by CP, IV, or Date", + "/vanished - show top x vanished, sorted by CP, IV, or Date", + "/softbans - info about possible softbans" + ) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="\n".join(res)) + + def is_configured(self, update): + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="No password nor master configured in TelegramTask section, bot will not accept any commands") + + def is_master_numeric(self, update): + outMessage = "Telegram message received from correct user, but master is not numeric, updating datastore." + self.bot.logger.warn(outMessage) + # the "master" is not numeric, set self.master to update.message.chat_id and re-instantiate the handler + newconfig = self.config + newconfig['master'] = update.message.chat_id + # insert chat id into database + self.grab_uid(update) + # remove old handler + self.bot.event_manager._handlers = filter(lambda x: not isinstance(x, TelegramHandler), + self.bot.event_manager._handlers) + # add new handler (passing newconfig as parameter) + self.bot.event_manager.add_handler(TelegramHandler(self.bot, newconfig)) + + def is_known_sender(self, update): + # Reject message if sender does not match defined master in config + outMessage = "Telegram message received from unknown sender. Please either make sure your username or ID is in TelegramTask/master, or a password is set in TelegramTask section and /login is issued" + self.bot.logger.error(outMessage) + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="Please /login first") + + + + def tokenize(self, string, maxnum): + spl = string.split(' ', maxnum - 1) + while len(spl) < maxnum: + spl.append(" ") + return spl + + def evolve(self, chatid, uid): + # TODO: here comes evolve logic (later) + self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Evolve logic not implemented yet") + return + + def upgrade(self, chatid, uid): + # TODO: here comes upgrade logic (later) + self.sendMessage(chat_id=chatid, parse_mode='HTML', text="Upgrade logic not implemented yet") + return + def run(self): time.sleep(1) while True: if DEBUG_ON: self.bot.logger.info("Telegram loop running") if self._tbot is None: self.connect() - for update in self._tbot.getUpdates(offset=self.update_id, timeout=10): self.update_id = update.update_id + 1 if update.message: self.bot.logger.info("Telegram message from {} ({}): {}".format(update.message.from_user.username, update.message.from_user.id, update.message.text)) - if update.message.text == "/start" or update.message.text == "/help": - res = ( - "*Commands: *", - "/info - info about bot", - "/login - authenticate with the bot; once authenticated, your ID will be registered with the bot and survive bot restarts", - "/logout - remove your ID from the 'authenticated' list", - "/sub - subscribe to eventName, with optional parameters, event name=all will subscribe to ALL events (LOTS of output!)", - "/unsub - unsubscribe from eventName; parameters must match the /sub parameters", - "/unsub everything - will remove all subscriptions for this uid", - "/showsubs - show current subscriptions", - "/events - show available events, filtered by regular expression ", - "/top - show top X pokemons, sorted by CP, IV, or Date", - "/evolved - show top x pokemon evolved, sorted by CP, IV, or Date", - "/hatched - show top x pokemon hatched, sorted by CP, IV, or Date", - "/caught - show top x pokemon caught, sorted by CP, IV, or Date", - "/pokestops - show last x pokestops visited", - "/released - show top x released, sorted by CP, IV, or Date", - "/vanished - show top x vanished, sorted by CP, IV, or Date", - "/softbans - info about possible softbans" - ) - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', - text="\n".join(res)) - continue - if self.config.get('password', None) == None and ( - not hasattr(self, "master") or not self.config.get('master', - None)): # no auth provided in config - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', - text="No password nor master configured in TelegramTask section, bot will not accept any commands") - continue if re.match(r'^/login [^ ]+', update.message.text): self.authenticate(update) continue + if self.config.get('password', None) == None and ( + not hasattr(self, "master") or not self.config.get('master', None)):# no auth provided in config + self.is_configured(update) + continue if not self.isAuthenticated(update.message.from_user.id) and hasattr(self, "master") and self.master and not unicode(self.master).isnumeric() and \ unicode(self.master) == unicode(update.message.from_user.username): - outMessage = "Telegram message received from correct user, but master is not numeric, updating datastore." - self.bot.logger.warn(outMessage) - # the "master" is not numeric, set self.master to update.message.chat_id and re-instantiate the handler - newconfig = self.config - newconfig['master'] = update.message.chat_id - # insert chat id into database - self.grab_uid(update) - # remove old handler - self.bot.event_manager._handlers = filter(lambda x: not isinstance(x, TelegramHandler), - self.bot.event_manager._handlers) - # add new handler (passing newconfig as parameter) - self.bot.event_manager.add_handler(TelegramHandler(self.bot, newconfig)) + self.is_master_numeric(update) continue if not self.isAuthenticated(update.message.from_user.id): - # Reject message if sender does not match defined master in config - outMessage = "Telegram message received from unknown sender. Please either make sure your username or ID is in TelegramTask/master, or a password is set in TelegramTask section and /login is issued" - self.bot.logger.error(outMessage) - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', - text="Please /login first") + self.is_known_sender(update) continue # one way or another, the user is now authenticated # make sure uid is in database self.grab_uid(update) - if update.message.text == "/info": - self.chat_handler.send_player_stats_to_chat(update.message.chat_id) + if update.message.text == "/start" or update.message.text == "/help": + self.send_start(update) continue - if update.message.text == "/softbans": - self.chat_handler.get_softban(update.message.chat_id) + if update.message.text == "/info": + self.send_info(update) continue - if re.match("^/events", update.message.text): - events = self.chat_handler.get_events(update) - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', - text="\n".join(events)) - continue if update.message.text == "/logout": - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', - text=("Logged out.")) - self.deauthenticate(update) + self.send_logout(update) + continue + if re.match("^/events", update.message.text): + self.send_events(update) continue if re.match(r'^/sub ', update.message.text): - self.chsub(update.message.text, update.message.chat_id) - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', - text=("Subscriptions updated.")) + self.send_subscription_updated(update) continue if re.match(r'^/unsub ', update.message.text): - self.chsub(update.message.text, update.message.chat_id) - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='HTML', - text=("Subscriptions updated.")) + self.send_subscription_updated(update) continue if re.match(r'^/showsubs', update.message.text): - self.showsubs(update.message.chat_id) + self.showsubs(update) continue if re.match(r'^/top ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.showtop(update.message.chat_id, num, order) + self.send_top(update, num, order) continue if re.match(r'^/caught ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.get_caught(update.message.chat_id, num, order) + self.send_caught(update, num, order) continue if re.match(r'^/evolved ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.get_evolved(update.message.chat_id, num, order) + self.send_evolved(update, num, order) continue if re.match(r'^/pokestops ', update.message.text): (cmd, num) = self.tokenize(update.message.text, 2) - self.chat_handler.get_pokestops(update.message.chat_id, num) + self.send_pokestops(update, num) continue if re.match(r'^/hatched ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.get_hatched(update.message.chat_id, num, order) + self.send_hatched(update, num, order) continue if re.match(r'^/released ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.get_released(update.message.chat_id, num, order) + self.send_released(update, num, order) continue if re.match(r'^/vanished ', update.message.text): (cmd, num, order) = self.tokenize(update.message.text, 3) - self.chat_handler.get_vanished(update.message.chat_id, num, order) + self.send_vanished(update, num, order) continue - self.chat_handler.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', - text="Unrecognized command: {}".format(update.message.text)) - - def showsubs(self, chatid): - subs = [] - with self.bot.database as conn: - for sub in conn.execute("select uid, event_type, parameters from telegram_subscriptions where uid = ?", - [chatid]).fetchall(): - if "{}".format(sub[2]) not in ["", " "]: - subs.append("{} -> .{}.".format(sub[1], sub[2])) - else: - subs.append("{}".format(sub[1])) - if subs == []: subs.append( - "No subscriptions found. Subscribe using /sub EVENTNAME. For a list of events, send /events") - self.chat_handler.sendMessage(chat_id=chatid, parse_mode='HTML', text="\n".join(subs)) - - def chsub(self, msg, chatid): - (cmd, evt, params) = self.tokenize(msg, 3) - if cmd == "/sub": - sql = "replace into telegram_subscriptions(uid, event_type, parameters) values (?, ?, ?)" - else: - if evt == "everything": - sql = "delete from telegram_subscriptions where uid = ? and (event_type = ? or parameters = ? or 1 = 1)" # does not look very elegant, but makes unsub'ing everythign possible - else: - sql = "delete from telegram_subscriptions where uid = ? and event_type = ? and parameters = ?" - - with self.bot.database as conn: - conn.execute(sql, [chatid, evt, params]) - conn.commit() - return - - def tokenize(self, string, maxnum): - spl = string.split(' ', maxnum - 1) - while len(spl) < maxnum: - spl.append(" ") - return spl + if re.match(r'^/softbans ', update.message.text): + (cmd, num) = self.tokenize(update.message.text, 2) + self.send_softbans(update, num) + continue + + self.sendMessage(chat_id=update.message.chat_id, parse_mode='Markdown', + text="Unrecognized command: {}".format(update.message.text)) class TelegramDBInit: @@ -319,9 +483,11 @@ def __init__(self, bot, config): self._connect() def _connect(self): - self.bot.logger.info("Telegram bot not running. Starting") - self.tbot = TelegramClass(self.bot, self.pokemons, self.config) - thread.start_new_thread(self.tbot.run) + if self.tbot is None: + self.bot.logger.info("Telegram bot not running. Starting") + self.tbot = TelegramClass(self.bot, self.pokemons, self.config) + thread.start_new_thread(self.tbot.run) + return self.tbot def catch_notify(self, pokemon, cp, iv, params): if params == " ": @@ -376,4 +542,4 @@ def handle_event(self, event, sender, level, formatted_msg, data): if self.tbot is not None: # tbot should be running, but just in case it hasn't started yet - self.chat_handler.sendMessage(chat_id=uid, parse_mode='Markdown', text=msg) + self.tbot.sendMessage(chat_id=uid, parse_mode='Markdown', text=msg) diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py index d01d9b649f..9e007c9f6e 100644 --- a/pokemongo_bot/metrics.py +++ b/pokemongo_bot/metrics.py @@ -73,7 +73,10 @@ def num_evolutions(self): def earned_dust(self): return self.dust['latest'] - self.dust['start'] - + + def total_stardust(self): + return self.bot.stardust + def stardust_per_hour(self): return self.earned_dust()/(time.time() - self.start_time)*3600