├── .gitattributes ├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── accounts.example.csv ├── config.example.py ├── gyms.py ├── manifest.in ├── monocle ├── __init__.py ├── altitudes.py ├── avatar.py ├── bounds.py ├── db.py ├── db_proc.py ├── landmarks.py ├── names.py ├── notification.py ├── overseer.py ├── sanitized.py ├── shared.py ├── spawns.py ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── leaflet.css │ │ └── main.css │ ├── demo │ │ └── gyms.png │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── img │ │ ├── discord.png │ │ ├── facebook.png │ │ ├── layers-2x.png │ │ ├── layers.png │ │ ├── marker-icon-2x.png │ │ ├── marker-icon.png │ │ ├── marker-shadow.png │ │ ├── my-location.png │ │ ├── settings.png │ │ ├── telegram.png │ │ └── twitter.png │ └── js │ │ ├── bootstrap.min.js │ │ ├── jquery-3.2.1.min.js │ │ ├── leaflet.js │ │ └── main.js ├── templates │ ├── gyms.html │ ├── newmap.html │ ├── report.html │ ├── report_single.html │ └── workersmap.html ├── utils.py ├── web_utils.py └── worker.py ├── optional-requirements.txt ├── requirements.txt ├── scan.py ├── scripts ├── create_db.py ├── export_accounts_csv.py ├── pickle_landmarks.example.py ├── print_accounts.py ├── print_levels.py ├── print_spawns.py └── test_notifications.py ├── setup.py ├── solve_captchas.py ├── sql ├── 0.7.0.sql ├── add_spawnpoint_failures.sql ├── mysql-doubles.sql ├── remove_altitude.sql ├── remove_normalized_timestamp.sql ├── spawn_id_integer.sql ├── v0.5.0.sql └── v0.5.1.sql ├── web.py └── web_sanic.py /.gitattributes: -------------------------------------------------------------------------------- 1 | monocle/static/js/bootstrap.min.js linguist-vendored 2 | monocle/static/js/jquery-3.2.1.min.js linguist-vendored 3 | monocle/static/js/leaflet.js linguist-vendored 4 | monocle/static/css/bootstrap.min.css linguist-vendored 5 | monocle/static/css/leaflet.css linguist-vendored 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | *.dll 10 | *.dylib 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv/ 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # configuration 96 | config.py 97 | accounts*.csv 98 | banned.csv 99 | templates/footer.html 100 | templates/custom.html 101 | static/css/custom.css 102 | static/js/custom.js 103 | 104 | # script 105 | *pickle_landmarks.py 106 | 107 | # storage and logging 108 | *.sqlite 109 | *.log 110 | scan.log.* 111 | *.pickle 112 | *.xz 113 | pickles/ 114 | ignore/ 115 | 116 | # socket 117 | *.sock 118 | 119 | # generated images 120 | notification-images/ 121 | 122 | ### macOS ### 123 | *.DS_Store 124 | .AppleDouble 125 | .LSOverride 126 | 127 | ### Windows ### 128 | Thumbs.db 129 | ehthumbs.db 130 | Desktop.ini 131 | 132 | ### Images ### 133 | monocle/static/monocle-icons 134 | 135 | ### IntelliJ IDEA ### 136 | .idea 137 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "monocle-icons"] 2 | path = monocle/static/monocle-icons 3 | url = https://github.com/Noctem/monocle-icons.git 4 | ignore = all 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | directories: 3 | - "$HOME/.cache/pip" 4 | - "$HOME/.pyenv" 5 | 6 | matrix: 7 | include: 8 | - os: linux 9 | python: 3.5 10 | language: python 11 | - os: linux 12 | dist: trusty 13 | python: 3.6 14 | language: python 15 | - os: osx 16 | osx_image: xcode7.3 17 | language: generic 18 | - os: osx 19 | osx_image: xcode8.2 20 | language: generic 21 | 22 | before_install: 23 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update && brew install python3; fi 24 | 25 | install: 26 | - pip3 install -r requirements.txt 27 | - python3 setup.py install 28 | 29 | script: 30 | - cp accounts.example.csv accounts.csv 31 | - python3 scripts/create_db.py 32 | - python3 -c 'from monocle import avatar, bounds, db_proc, db, names, notification, overseer, sanitized, shared, spawns, utils, web_utils, worker' 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright for portions of Monocle are held by Tomasz Modrzyński (2016), all others are held by David Christenson (2017). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monocle 2 | 3 | [![Build Status](https://travis-ci.org/Noctem/Monocle.svg?branch=develop)](https://travis-ci.org/Noctem/Monocle) 4 | 5 | Monocle is the distinguished Pokémon Go scanner capable of scanning large areas for spawns. Features spawnpoint scanning, Twitter and PushBullet notifications, accurate expiration times and estimates based on historical data, pokestop and gym collection, a CAPTCHA solving script, and more. 6 | 7 | A [demonstration of the Twitter notifications can be viewed here](https://twitter.com/SLCPokemon). 8 | 9 | [![Distinguished Pokemon](https://i.imgur.com/9vud1wo.jpg)](https://darkestnight.deviantart.com/art/A-Distinguished-Pokeman-208009200) 10 | 11 | 12 | ## How does it work? 13 | 14 | It uses a database table of spawnpoints and expiration times to visit points soon after Pokemon spawn. For each point it determines which eligible worker can reach the point with the lowest speed, or tries again if all workers would be over the configurable speed limit. This method scans very efficiently and finds Pokemon very soon after they spawn, and also leads to unpredictable worker movements that look less robotic. The spawnpoint database continually expands as Pokemon are discovered. If you don't have enough accounts to keep up with the number of spawns in your database, it will automatically skip points that are unreachable within the speed limit or points that it has already seen spawn that cycle from other nearby points. 15 | 16 | If you don't have an existing database of spawn points it will spread your workers out over the area you specify in config and collect the locations of spawn points from GetMapObjects requests. It will then visit those points whenever it doesn't have a known spawn (with its expiration time) to visit soon. So it will gradually learn the expiration times of more and more spawn points as you use it. 17 | 18 | There's also a simple interface that displays active Pokemon on a map, and can generate nice-looking reports. 19 | 20 | Since it uses [Leaflet](http://leafletjs.com/) for mapping, the appearance and data source can easily be configured to match [any of these](https://leaflet-extras.github.io/leaflet-providers/preview/) with the `MAP_PROVIDER_URL` config option. 21 | 22 | ## Features 23 | 24 | - accurate timestamp information whenever possible with historical data 25 | - Twitter, PushBullet, and Telegram notifications 26 | - references nearest landmark from your own list 27 | - IV/moves detection, storage, notification, and display 28 | - produces nice image of Pokémon with stats for Twitter 29 | - can configure to get IVs for all Pokémon or only those eligible for notification 30 | - stores Pokémon, gyms, and pokestops in database 31 | - spawnpoint scanning with or without an existing database of spawns 32 | - automatic account swapping for CAPTCHAs and other problems 33 | - pickle storage to improve speed and reduce database queries 34 | - manual CAPTCHA solving that instantly puts accounts back in rotation 35 | - closely emulates the client to reduce CAPTCHAs and bans 36 | - automatic device_info generation and retention 37 | - aims at being very stable for long-term runs 38 | - able to map entire city (or larger area) 39 | - reports for gathered data 40 | - asyncio coroutines 41 | - support for Bossland's hashing server 42 | - displays key usage stats in real time 43 | 44 | ## Setting up 45 | 1. Install Python 3.5 or later (3.6 is recommended) 46 | 2. `git clone --recursive https://github.com/Noctem/Monocle.git` 47 | * Optionally install a custom icon package from elsewhere 48 | 3. Copy *config.example.py* to *monocle/config.py* and customize it with your location, database information, and any other relevant settings. The comments in the config example provide some information about the options. 49 | 4. Fill in *accounts.example.csv* with your own accounts and save it as *accounts.csv*. 50 | * You only need to fill in the usernames and passwords, the other columns will be generated for you if left blank. 51 | 5. `pip3 install -r requirements.txt` 52 | * Optionally `pip3 install` additional packages listed in optional-requirements 53 | * *asyncpushbullet* is required for PushBullet notifications 54 | * *peony-twitter* is required for Twitter notifications 55 | * *gpsoauth* is required for logging in to Google accounts 56 | * *shapely* is required for landmarks or boundary polygons 57 | * *selenium* (and [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)) are required for manually solving CAPTCHAs 58 | * *uvloop* provides better event loop performance 59 | * *pycairo* is required for generating IV/move images 60 | * *mysqlclient* is required for using a MySQL database 61 | * *psycopg2* is required for using a PostgreSQL database 62 | * *aiosocks* is required for using SOCKS proxies 63 | * *cchardet* and *aiodns* provide better performance with aiohttp 64 | * *sanic* and *asyncpg* (and a Postgres DB) are required for web_sanic 65 | * *ujson* for better JSON encoding and decoding performance 66 | 6. Run `python3 scripts/create_db.py` from the command line 67 | 7. Run `python3 scan.py` 68 | * Optionally run the live map interface and reporting system: `python3 web.py` 69 | 70 | 71 | **Note**: Monocle works with Python 3.5 or later only. Python 2.7 is **not supported** and is not compatible at all since I moved from threads to coroutines. Seriously, it's 2017, Python 2.7 hasn't been developed for 6 years, why don't you upgrade already? 72 | 73 | Note that if you want more than 10 workers simultaneously running, SQLite is likely not the best choice. I personally use and recommend PostgreSQL, but MySQL and SQLite should also work. 74 | 75 | 76 | ## Reports 77 | 78 | There are three reports, all available as web pages on the same server as the live map: 79 | 80 | 1. Overall report, available at `/report` 81 | 2. Single species report, available at `/report/` 82 | 3. Gym statistics page, available by running `gyms.py` 83 | 84 | The workers' live locations and stats can be viewed from the main map by enabling the workers layer, or at `/workers` (communicates directly with the worker process and requires no DB queries). 85 | 86 | The gyms statistics server is in a separate file, because it's intended to be shared publicly as a webpage. 87 | 88 | [![gyms](https://i.imgur.com/MWpHAEWm.jpg)](monocle/static/demo/gyms.png) 89 | 90 | ## Getting Started Tips & FAQs 91 | 92 | Visit our [Wiki](https://github.com/Noctem/Monocle/wiki) for more info. 93 | 94 | ## License 95 | 96 | See [LICENSE](LICENSE). 97 | 98 | This project is based on the coroutines branch of [pokeminer](https://github.com/modrzew/pokeminer/tree/coroutines) (now discontinued). *Pokeminer* was originally based on an early version of [PokemonGo-Map](https://github.com/AHAAAAAAA/PokemonGo-Map), but no longer shares any code with it. It uses [aiopogo](https://github.com/Noctem/aiopogo), my fork of pgoapi which uses *aiohttp* for asynchronous network requests. 99 | 100 | The [excellent image](https://darkestnight.deviantart.com/art/A-Distinguished-Pokeman-208009200) near the top of this README was painted by [darkestnight](https://darkestnight.deviantart.com/). 101 | -------------------------------------------------------------------------------- /accounts.example.csv: -------------------------------------------------------------------------------- 1 | username,password,provider,model,iOS,id 2 | ash_ketchum,pik4chu,ptc,"iPhone9,1",10.2.1,2fcfb8c40e244be284aa42313c1d8dee 3 | misty,t0g3p1,ptc,,, 4 | brock,0n1x,google,,, 5 | jessie,w0bbuff3t,,,, -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | ### All lines that are commented out (and some that aren't) are optional ### 2 | 3 | DB_ENGINE = 'sqlite:///db.sqlite' 4 | #DB_ENGINE = 'mysql://user:pass@localhost/monocle' 5 | #DB_ENGINE = 'postgresql://user:pass@localhost/monocle 6 | 7 | AREA_NAME = 'SLC' # the city or region you are scanning 8 | LANGUAGE = 'EN' # ISO 639-1 codes EN, DE, ES, FR, IT, JA, KO, PT, or ZH for Pokémon/move names 9 | MAX_CAPTCHAS = 100 # stop launching new visits if this many CAPTCHAs are pending 10 | SCAN_DELAY = 10 # wait at least this many seconds before scanning with the same account 11 | SPEED_UNIT = 'miles' # valid options are 'miles', 'kilometers', 'meters' 12 | SPEED_LIMIT = 19.5 # limit worker speed to this many SPEED_UNITs per hour 13 | 14 | # The number of simultaneous workers will be these two numbers multiplied. 15 | # On the initial run, workers will arrange themselves in a grid across the 16 | # rectangle you defined with MAP_START and MAP_END. 17 | # The rows/columns will also be used for the dot grid in the console output. 18 | # Provide more accounts than the product of your grid to allow swapping. 19 | GRID = (4, 4) # rows, columns 20 | 21 | # the corner points of a rectangle for your workers to spread out over before 22 | # any spawn points have been discovered 23 | MAP_START = (40.7913, -111.9398) 24 | MAP_END = (40.7143, -111.8046) 25 | 26 | # do not visit spawn points outside of your MAP_START and MAP_END rectangle 27 | # the boundaries will be the rectangle created by MAP_START and MAP_END, unless 28 | STAY_WITHIN_MAP = True 29 | 30 | # ensure that you visit within this many meters of every part of your map during bootstrap 31 | # lower values are more thorough but will take longer 32 | BOOTSTRAP_RADIUS = 120 33 | 34 | GIVE_UP_KNOWN = 75 # try to find a worker for a known spawn for this many seconds before giving up 35 | GIVE_UP_UNKNOWN = 60 # try to find a worker for an unknown point for this many seconds before giving up 36 | SKIP_SPAWN = 90 # don't even try to find a worker for a spawn if the spawn time was more than this many seconds ago 37 | 38 | # How often should the mystery queue be reloaded (default 90s) 39 | # this will reduce the grouping of workers around the last few mysteries 40 | #RESCAN_UNKNOWN = 90 41 | 42 | # filename of accounts CSV 43 | ACCOUNTS_CSV = 'accounts.csv' 44 | 45 | # the directory that the pickles folder, socket, CSV, etc. will go in 46 | # defaults to working directory if not set 47 | #DIRECTORY = None 48 | 49 | # Limit the number of simultaneous logins to this many at a time. 50 | # Lower numbers will increase the amount of time it takes for all workers to 51 | # get started but are recommended to avoid suddenly flooding the servers with 52 | # accounts and arousing suspicion. 53 | SIMULTANEOUS_LOGINS = 4 54 | 55 | # Limit the number of workers simulating the app startup process simultaneously. 56 | SIMULTANEOUS_SIMULATION = 10 57 | 58 | # Immediately select workers whose speed are below (SPEED_UNIT)p/h instead of 59 | # continuing to try to find the worker with the lowest speed. 60 | # May increase clustering if you have a high density of workers. 61 | GOOD_ENOUGH = 0.1 62 | 63 | # Seconds to sleep after failing to find an eligible worker before trying again. 64 | SEARCH_SLEEP = 2.5 65 | 66 | ## alternatively define a Polygon to use as boundaries (requires shapely) 67 | ## if BOUNDARIES is set, STAY_WITHIN_MAP will be ignored 68 | ## more information available in the shapely manual: 69 | ## http://toblerity.org/shapely/manual.html#polygons 70 | #from shapely.geometry import Polygon 71 | #BOUNDARIES = Polygon(((40.799609, -111.948556), (40.792749, -111.887341), (40.779264, -111.838078), (40.761410, -111.817908), (40.728636, -111.805293), (40.688833, -111.785564), (40.689768, -111.919389), (40.750461, -111.949938))) 72 | 73 | # key for Bossland's hashing server, otherwise the old hashing lib will be used 74 | #HASH_KEY = '9d87af14461b93cb3605' # this key is fake 75 | 76 | # Skip PokéStop spinning and egg incubation if your request rate is too high 77 | # for your hashing subscription. 78 | # e.g. 79 | # 75/150 hashes available 35/60 seconds passed => fine 80 | # 70/150 hashes available 30/60 seconds passed => throttle (only scan) 81 | # value: how many requests to keep as spare (0.1 = 10%), False to disable 82 | #SMART_THROTTLE = 0.1 83 | 84 | # Swap the worker that has seen the fewest Pokémon every x seconds 85 | # Defaults to whatever will allow every worker to be swapped within 6 hours 86 | #SWAP_OLDEST = 300 # 5 minutes 87 | # Only swap if it's been active for more than x minutes 88 | #MINIMUM_RUNTIME = 10 89 | 90 | ### these next 6 options use more requests but look more like the real client 91 | APP_SIMULATION = True # mimic the actual app's login requests 92 | COMPLETE_TUTORIAL = True # complete the tutorial process and configure avatar for all accounts that haven't yet 93 | INCUBATE_EGGS = True # incubate eggs if available 94 | 95 | ## encounter Pokémon to store IVs. 96 | ## valid options: 97 | # 'all' will encounter every Pokémon that hasn't been already been encountered 98 | # 'some' will encounter Pokémon if they are in ENCOUNTER_IDS or eligible for notification 99 | # 'notifying' will encounter Pokémon that are eligible for notifications 100 | # None will never encounter Pokémon 101 | ENCOUNTER = None 102 | #ENCOUNTER_IDS = (3, 6, 9, 45, 62, 71, 80, 85, 87, 89, 91, 94, 114, 130, 131, 134) 103 | 104 | # PokéStops 105 | SPIN_POKESTOPS = True # spin all PokéStops that are within range 106 | SPIN_COOLDOWN = 300 # spin only one PokéStop every n seconds (default 300) 107 | 108 | # minimum number of each item to keep if the bag is cleaned 109 | # bag cleaning is disabled if this is not present or is commented out 110 | ''' # triple quotes are comments, remove them to use this ITEM_LIMITS example 111 | ITEM_LIMITS = { 112 | 1: 20, # Poké Ball 113 | 2: 50, # Great Ball 114 | 3: 100, # Ultra Ball 115 | 101: 0, # Potion 116 | 102: 0, # Super Potion 117 | 103: 0, # Hyper Potion 118 | 104: 40, # Max Potion 119 | 201: 0, # Revive 120 | 202: 40, # Max Revive 121 | 701: 20, # Razz Berry 122 | 702: 20, # Bluk Berry 123 | 703: 20, # Nanab Berry 124 | 704: 20, # Wepar Berry 125 | 705: 20, # Pinap Berry 126 | } 127 | ''' 128 | 129 | # Update the console output every x seconds 130 | REFRESH_RATE = 0.75 # 750ms 131 | # Update the seen/speed/visit/speed stats every x seconds 132 | STAT_REFRESH = 5 133 | 134 | # sent with GET_PLAYER requests, should match your region 135 | PLAYER_LOCALE = {'country': 'US', 'language': 'en', 'timezone': 'America/Denver'} 136 | 137 | # retry a request after failure this many times before giving up 138 | MAX_RETRIES = 3 139 | 140 | # number of seconds before timing out on a login request 141 | LOGIN_TIMEOUT = 2.5 142 | 143 | # add spawn points reported in cell_ids to the unknown spawns list 144 | #MORE_POINTS = False 145 | 146 | # Set to True to kill the scanner when a newer version is forced 147 | #FORCED_KILL = False 148 | 149 | # exclude these Pokémon from the map by default (only visible in trash layer) 150 | TRASH_IDS = ( 151 | 16, 19, 21, 29, 32, 41, 46, 48, 50, 52, 56, 74, 77, 96, 111, 133, 152 | 161, 163, 167, 177, 183, 191, 194 153 | ) 154 | 155 | # include these Pokémon on the "rare" report 156 | RARE_IDS = (3, 6, 9, 45, 62, 71, 80, 85, 87, 89, 91, 94, 114, 130, 131, 134) 157 | 158 | from datetime import datetime 159 | REPORT_SINCE = datetime(2017, 2, 17) # base reports on data from after this date 160 | 161 | # used for altitude queries and maps in reports 162 | #GOOGLE_MAPS_KEY = 'OYOgW1wryrp2RKJ81u7BLvHfYUA6aArIyuQCXu4' # this key is fake 163 | REPORT_MAPS = True # Show maps on reports 164 | #ALT_RANGE = (1250, 1450) # Fall back to altitudes in this range if Google query fails 165 | 166 | ## Round altitude coordinates to this many decimal places 167 | ## More precision will lead to larger caches and more Google API calls 168 | ## Maximum distance from coords to rounded coords for precisions (at Lat40): 169 | ## 1: 7KM, 2: 700M, 3: 70M, 4: 7M 170 | #ALT_PRECISION = 2 171 | 172 | ## Automatically resolve captchas using 2Captcha key. 173 | #CAPTCHA_KEY = '1abc234de56fab7c89012d34e56fa7b8' 174 | ## the number of CAPTCHAs an account is allowed to receive before being swapped out 175 | #CAPTCHAS_ALLOWED = 3 176 | ## Get new accounts from the CAPTCHA queue first if it's not empty 177 | #FAVOR_CAPTCHA = True 178 | 179 | # allow displaying the live location of workers on the map 180 | MAP_WORKERS = True 181 | # filter these Pokemon from the map to reduce traffic and browser load 182 | #MAP_FILTER_IDS = [161, 165, 16, 19, 167] 183 | 184 | # unix timestamp of last spawn point migration, spawn times learned before this will be ignored 185 | LAST_MIGRATION = 1481932800 # Dec. 17th, 2016 186 | 187 | # Treat a spawn point's expiration time as unknown if nothing is seen at it on more than x consecutive visits 188 | FAILURES_ALLOWED = 2 189 | 190 | ## Map data provider and appearance, previews available at: 191 | ## https://leaflet-extras.github.io/leaflet-providers/preview/ 192 | #MAP_PROVIDER_URL = '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 193 | #MAP_PROVIDER_ATTRIBUTION = '© OpenStreetMap contributors' 194 | 195 | # set of proxy addresses and ports 196 | # SOCKS requires aiosocks to be installed 197 | #PROXIES = {'http://127.0.0.1:8080', 'https://127.0.0.1:8443', 'socks5://127.0.0.1:1080'} 198 | 199 | # convert spawn_id to integer for more efficient DB storage, set to False if 200 | # using an old database since the data types are incompatible. 201 | #SPAWN_ID_INT = True 202 | 203 | # Bytestring key to authenticate with manager for inter-process communication 204 | #AUTHKEY = b'm3wtw0' 205 | # Address to use for manager, leave commented if you're not sure. 206 | #MANAGER_ADDRESS = r'\\.\pipe\monocle' # must be in this format for Windows 207 | #MANAGER_ADDRESS = 'monocle.sock' # the socket name for Unix systems 208 | #MANAGER_ADDRESS = ('127.0.0.1', 5002) # could be used for CAPTCHA solving and live worker maps on remote systems 209 | 210 | # Store the cell IDs so that they don't have to be recalculated every visit. 211 | # Enabling will (potentially drastically) increase memory usage. 212 | #CACHE_CELLS = False 213 | 214 | # Only for use with web_sanic (requires PostgreSQL) 215 | #DB = {'host': '127.0.0.1', 'user': 'monocle_role', 'password': 'pik4chu', 'port': '5432', 'database': 'monocle'} 216 | 217 | # Disable to use Python's event loop even if uvloop is installed 218 | #UVLOOP = True 219 | 220 | # The number of coroutines that are allowed to run simultaneously. 221 | #COROUTINES_LIMIT = GRID[0] * GRID[1] 222 | 223 | ### FRONTEND CONFIGURATION 224 | LOAD_CUSTOM_HTML_FILE = False # File path MUST be 'templates/custom.html' 225 | LOAD_CUSTOM_CSS_FILE = False # File path MUST be 'static/css/custom.css' 226 | LOAD_CUSTOM_JS_FILE = False # File path MUST be 'static/js/custom.js' 227 | 228 | #FB_PAGE_ID = None 229 | #TWITTER_SCREEN_NAME = None # Username withouth '@' char 230 | #DISCORD_INVITE_ID = None 231 | #TELEGRAM_USERNAME = None # Username withouth '@' char 232 | 233 | ## Variables below will be used as default values on frontend 234 | FIXED_OPACITY = False # Make marker opacity independent of remaining time 235 | SHOW_TIMER = False # Show remaining time on a label under each pokemon marker 236 | 237 | ### OPTIONS BELOW THIS POINT ARE ONLY NECESSARY FOR NOTIFICATIONS ### 238 | NOTIFY = False # enable notifications 239 | 240 | # create images with Pokémon image and optionally include IVs and moves 241 | # requires cairo and ENCOUNTER = 'notifying' or 'all' 242 | TWEET_IMAGES = True 243 | # IVs and moves are now dependant on level, so this is probably not useful 244 | IMAGE_STATS = False 245 | 246 | # As many hashtags as can fit will be included in your tweets, these will 247 | # be combined with landmark-specific hashtags (if applicable). 248 | HASHTAGS = {AREA_NAME, 'Monocle', 'PokemonGO'} 249 | #TZ_OFFSET = 0 # UTC offset in hours (if different from system time) 250 | 251 | # the required number of seconds remaining to notify about a Pokémon 252 | TIME_REQUIRED = 600 # 10 minutes 253 | 254 | ### Only set either the NOTIFY_RANKING or NOTIFY_IDS, not both! 255 | # The (x) rarest Pokémon will be eligible for notification. Whether a 256 | # notification is sent or not depends on its score, as explained below. 257 | NOTIFY_RANKING = 90 258 | 259 | # Pokémon to potentially notify about, in order of preference. 260 | # The first in the list will have a rarity score of 1, the last will be 0. 261 | #NOTIFY_IDS = (130, 89, 131, 3, 9, 134, 62, 94, 91, 87, 71, 45, 85, 114, 80, 6) 262 | 263 | # Sightings of the top (x) will always be notified about, even if below TIME_REQUIRED 264 | # (ignored if using NOTIFY_IDS instead of NOTIFY_RANKING) 265 | ALWAYS_NOTIFY = 14 266 | 267 | # Always notify about the following Pokémon even if their time remaining or scores are not high enough 268 | #ALWAYS_NOTIFY_IDS = {89, 130, 144, 145, 146, 150, 151} 269 | 270 | # Never notify about the following Pokémon, even if they would otherwise be eligible 271 | #NEVER_NOTIFY_IDS = TRASH_IDS 272 | 273 | # Override the rarity score for particular Pokémon 274 | # format is: {pokemon_id: rarity_score} 275 | #RARITY_OVERRIDE = {148: 0.6, 149: 0.9} 276 | 277 | # Ignore IV score and only base decision on rarity score (default if IVs not known) 278 | #IGNORE_IVS = False 279 | 280 | # Ignore rarity score and only base decision on IV score 281 | #IGNORE_RARITY = False 282 | 283 | # The Pokémon score required to notify goes on a sliding scale from INITIAL_SCORE 284 | # to MINIMUM_SCORE over the course of FULL_TIME seconds following a notification 285 | # Pokémon scores are an average of the Pokémon's rarity score and IV score (from 0 to 1) 286 | # If NOTIFY_RANKING is 90, the 90th most common Pokémon will have a rarity of score 0, the rarest will be 1. 287 | # IV score is the IV sum divided by 45 (perfect IVs). 288 | FULL_TIME = 1800 # the number of seconds after a notification when only MINIMUM_SCORE will be required 289 | INITIAL_SCORE = 0.7 # the required score immediately after a notification 290 | MINIMUM_SCORE = 0.4 # the required score after FULL_TIME seconds have passed 291 | 292 | ### The following values are fake, replace them with your own keys to enable 293 | ### notifications, otherwise exclude them from your config 294 | ### You must provide keys for at least one service to use notifications. 295 | 296 | #PB_API_KEY = 'o.9187cb7d5b857c97bfcaa8d63eaa8494' 297 | #PB_CHANNEL = 0 # set to the integer of your channel, or to None to push privately 298 | 299 | #TWITTER_CONSUMER_KEY = '53d997264eb7f6452b7bf101d' 300 | #TWITTER_CONSUMER_SECRET = '64b9ebf618829a51f8c0535b56cebc808eb3e80d3d18bf9e00' 301 | #TWITTER_ACCESS_KEY = '1dfb143d4f29-6b007a5917df2b23d0f6db951c4227cdf768b' 302 | #TWITTER_ACCESS_SECRET = 'e743ed1353b6e9a45589f061f7d08374db32229ec4a61' 303 | 304 | ## Telegram bot token is the one Botfather sends to you after completing bot creation. 305 | ## Chat ID can be two different values: 306 | ## 1) '@channel_name' for channels 307 | ## 2) Your chat_id if you will use your own account. To retrieve your ID, write to your bot and check this URL: 308 | ## https://api.telegram.org/bot/getUpdates 309 | #TELEGRAM_BOT_TOKEN = '123456789:AA12345qT6QDd12345RekXSQeoZBXVt-AAA' 310 | #TELEGRAM_CHAT_ID = '@your_channel' 311 | 312 | #WEBHOOKS = {'http://127.0.0.1:4000'} 313 | 314 | 315 | ##### Referencing landmarks in your tweets/notifications 316 | 317 | #### It is recommended to store the LANDMARKS object in a pickle to reduce startup 318 | #### time if you are using queries. An example script for this is in: 319 | #### scripts/pickle_landmarks.example.py 320 | #from pickle import load 321 | #with open('pickles/landmarks.pickle', 'rb') as f: 322 | # LANDMARKS = load(f) 323 | 324 | ### if you do pickle it, just load the pickle and omit everything below this point 325 | 326 | #from monocle.landmarks import Landmarks 327 | #LANDMARKS = Landmarks(query_suffix=AREA_NAME) 328 | 329 | # Landmarks to reference when Pokémon are nearby 330 | # If no points are specified then it will query OpenStreetMap for the coordinates 331 | # If 1 point is provided then it will use those coordinates but not create a shape 332 | # If 2 points are provided it will create a rectangle with its corners at those points 333 | # If 3 or more points are provided it will create a polygon with vertices at each point 334 | # You can specify the string to search for on OpenStreetMap with the query parameter 335 | # If no query or points is provided it will query with the name of the landmark (and query_suffix) 336 | # Optionally provide a set of hashtags to be used for tweets about this landmark 337 | # Use is_area for neighborhoods, regions, etc. 338 | # When selecting a landmark, non-areas will be chosen first if any are close enough 339 | # the default phrase is 'in' for areas and 'at' for non-areas, but can be overriden for either. 340 | 341 | ### replace these with well-known places in your area 342 | 343 | ## since no points or query is provided, the names provided will be queried and suffixed with AREA_NAME 344 | #LANDMARKS.add('Rice Eccles Stadium', shortname='Rice Eccles', hashtags={'Utes'}) 345 | #LANDMARKS.add('the Salt Lake Temple', shortname='the temple', hashtags={'TempleSquare'}) 346 | 347 | ## provide two corner points to create a square for this area 348 | #LANDMARKS.add('City Creek Center', points=((40.769210, -111.893901), (40.767231, -111.888275)), hashtags={'CityCreek'}) 349 | 350 | ## provide a query that is different from the landmark name so that OpenStreetMap finds the correct one 351 | #LANDMARKS.add('the State Capitol', shortname='the Capitol', query='Utah State Capitol Building') 352 | 353 | ### area examples ### 354 | ## query using name, override the default area phrase so that it says 'at (name)' instead of 'in' 355 | #LANDMARKS.add('the University of Utah', shortname='the U of U', hashtags={'Utes'}, phrase='at', is_area=True) 356 | ## provide corner points to create a polygon of the area since OpenStreetMap does not have a shape for it 357 | #LANDMARKS.add('Yalecrest', points=((40.750263, -111.836502), (40.750377, -111.851108), (40.751515, -111.853833), (40.741212, -111.853909), (40.741188, -111.836519)), is_area=True) 358 | -------------------------------------------------------------------------------- /gyms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime, timedelta 4 | from pkg_resources import resource_filename 5 | 6 | import time 7 | import argparse 8 | 9 | from flask import Flask, render_template 10 | 11 | from monocle import db, sanitized as conf 12 | from monocle.names import POKEMON 13 | from monocle.web_utils import get_args 14 | from monocle.bounds import area 15 | 16 | 17 | app = Flask(__name__, template_folder=resource_filename('monocle', 'templates')) 18 | 19 | CACHE = { 20 | 'data': None, 21 | 'generated_at': None, 22 | } 23 | 24 | 25 | def get_stats(): 26 | cache_valid = ( 27 | CACHE['data'] and 28 | CACHE['generated_at'] > datetime.now() - timedelta(minutes=15) 29 | ) 30 | if cache_valid: 31 | return CACHE['data'] 32 | with db.session_scope() as session: 33 | forts = db.get_forts(session) 34 | count = {t.value: 0 for t in db.Team} 35 | strongest = {t.value: None for t in db.Team} 36 | guardians = {t.value: {} for t in db.Team} 37 | top_guardians = {t.value: None for t in db.Team} 38 | prestige = {t.value: 0 for t in db.Team} 39 | percentages = {} 40 | prestige_percent = {} 41 | total_prestige = 0 42 | last_date = 0 43 | pokemon_names = POKEMON 44 | for fort in forts: 45 | if fort['last_modified'] > last_date: 46 | last_date = fort['last_modified'] 47 | team = fort['team'] 48 | count[team] += 1 49 | if team != 0: 50 | # Strongest gym 51 | existing = strongest[team] 52 | should_replace = ( 53 | existing is not None and 54 | fort['prestige'] > existing[0] or 55 | existing is None 56 | ) 57 | pokemon_id = fort['guard_pokemon_id'] 58 | if should_replace: 59 | strongest[team] = ( 60 | fort['prestige'], 61 | pokemon_id, 62 | pokemon_names[pokemon_id], 63 | ) 64 | # Guardians 65 | guardian_value = guardians[team].get(pokemon_id, 0) 66 | guardians[team][pokemon_id] = guardian_value + 1 67 | # Prestige 68 | prestige[team] += fort['prestige'] 69 | total_prestige = sum(prestige.values()) 70 | for team in db.Team: 71 | percentages[team.value] = ( 72 | count.get(team.value) / len(forts) * 100 73 | ) 74 | prestige_percent[team.value] = ( 75 | prestige.get(team.value) / total_prestige * 100 76 | ) 77 | if guardians[team.value]: 78 | pokemon_id = sorted( 79 | guardians[team.value], 80 | key=guardians[team.value].__getitem__, 81 | reverse=True 82 | )[0] 83 | top_guardians[team.value] = pokemon_names[pokemon_id] 84 | CACHE['generated_at'] = datetime.now() 85 | CACHE['data'] = { 86 | 'order': sorted(count, key=count.__getitem__, reverse=True), 87 | 'count': count, 88 | 'total_count': len(forts), 89 | 'strongest': strongest, 90 | 'prestige': prestige, 91 | 'prestige_percent': prestige_percent, 92 | 'percentages': percentages, 93 | 'last_date': last_date, 94 | 'top_guardians': top_guardians, 95 | 'generated_at': CACHE['generated_at'], 96 | } 97 | return CACHE['data'] 98 | 99 | 100 | @app.route('/') 101 | def index(): 102 | stats = get_stats() 103 | team_names = {k.value: k.name.title() for k in db.Team} 104 | styles = {1: 'primary', 2: 'danger', 3: 'warning'} 105 | return render_template( 106 | 'gyms.html', 107 | area_name=conf.AREA_NAME, 108 | area_size=area, 109 | minutes_ago=int((datetime.now() - stats['generated_at']).seconds / 60), 110 | last_date_minutes_ago=int((time.time() - stats['last_date']) / 60), 111 | team_names=team_names, 112 | styles=styles, 113 | **stats 114 | ) 115 | 116 | 117 | if __name__ == '__main__': 118 | args = get_args() 119 | app.run(debug=args.debug, host=args.host, port=args.port) 120 | -------------------------------------------------------------------------------- /manifest.in: -------------------------------------------------------------------------------- 1 | recursive-include monocle/static *.png 2 | recursive-include monocle/static/css *.css 3 | recursive-include monocle/static/js *.js 4 | recursive-include monocle/templates *.html 5 | -------------------------------------------------------------------------------- /monocle/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'monocle' 2 | __version__ = '0.8b1' 3 | __author__ = 'David Christenson' 4 | __license__ = 'MIT License' 5 | __copyright__ = 'Copyright (c) 2017 David Christenson ' 6 | -------------------------------------------------------------------------------- /monocle/altitudes.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from asyncio import gather, CancelledError 4 | from statistics import mean 5 | 6 | from aiohttp import ClientSession 7 | from polyline import encode as polyencode 8 | from aiopogo import json_loads 9 | from cyrandom import uniform 10 | 11 | from . import bounds, sanitized as conf 12 | from .shared import get_logger, LOOP, run_threaded 13 | from .utils import dump_pickle, float_range, load_pickle, round_coords 14 | 15 | 16 | class Altitudes: 17 | """Manage altitudes""" 18 | __slots__ = ('altitudes', 'changed', 'fallback', 'log', 'mean') 19 | 20 | def __init__(self): 21 | self.log = get_logger('altitudes') 22 | self.changed = False 23 | self.load() 24 | if len(self.altitudes) > 5: 25 | self.fallback = self.average 26 | else: 27 | self.fallback = self.random 28 | 29 | async def get_all(self): 30 | self.log.info('Fetching all altitudes') 31 | 32 | coords = self.get_coords() 33 | 34 | async with ClientSession(loop=LOOP) as session: 35 | if len(coords) < 300: 36 | await self.fetch_alts(coords, session) 37 | else: 38 | tasks = [self.fetch_alts(chunk, session) 39 | for chunk in self.chunks(coords)] 40 | await gather(*tasks, loop=LOOP) 41 | self.changed = True 42 | LOOP.create_task(run_threaded(self.pickle)) 43 | 44 | async def fetch_alts(self, coords, session, precision=conf.ALT_PRECISION): 45 | try: 46 | async with session.get( 47 | 'https://maps.googleapis.com/maps/api/elevation/json', 48 | params={'locations': 'enc:' + polyencode(coords), 49 | 'key': conf.GOOGLE_MAPS_KEY}, 50 | timeout=10) as resp: 51 | response = await resp.json(loads=json_loads) 52 | for r in response['results']: 53 | coords = round_coords((r['location']['lat'], r['location']['lng']), precision) 54 | self.altitudes[coords] = r['elevation'] 55 | if not self.altitudes: 56 | self.log.error(response['error_message']) 57 | except Exception: 58 | self.log.exception('Error fetching altitudes.') 59 | 60 | def get(self, point, randomize=uniform): 61 | point = round_coords(point, conf.ALT_PRECISION) 62 | alt = self.altitudes[point] 63 | return randomize(alt - 2.5, alt + 2.5) 64 | 65 | async def fetch(self, point, key=conf.GOOGLE_MAPS_KEY): 66 | if not key: 67 | return self.fallback() 68 | try: 69 | async with ClientSession(loop=LOOP) as session: 70 | async with session.get( 71 | 'https://maps.googleapis.com/maps/api/elevation/json', 72 | params={'locations': '{0[0]},{0[1]}'.format(point), 73 | 'key': key}, 74 | timeout=10) as resp: 75 | response = await resp.json(loads=json_loads) 76 | altitude = response['results'][0]['elevation'] 77 | self.altitudes[point] = altitude 78 | self.changed = True 79 | return altitude 80 | except CancelledError: 81 | raise 82 | except Exception: 83 | try: 84 | self.log.error(response['error_message']) 85 | except (KeyError, NameError): 86 | self.log.error('Error fetching altitude for {}.', point) 87 | return self.fallback() 88 | 89 | def average(self, randomize=uniform): 90 | self.log.info('Fell back to average altitude.') 91 | try: 92 | return randomize(self.mean - 15.0, self.mean + 15.0) 93 | except AttributeError: 94 | self.mean = mean(self.altitudes.values()) 95 | return self.average() 96 | 97 | def random(self, alt_range=conf.ALT_RANGE, randomize=uniform): 98 | self.log.info('Fell back to random altitude.') 99 | return randomize(*conf.ALT_RANGE) 100 | 101 | def load(self): 102 | try: 103 | state = load_pickle('altitudes', raise_exception=True) 104 | except FileNotFoundError: 105 | self.log.info('No altitudes pickle found.') 106 | self.altitudes = {} 107 | LOOP.run_until_complete(self.get_all()) 108 | return 109 | 110 | if state['bounds_hash'] == hash(bounds): 111 | if state['precision'] == conf.ALT_PRECISION and state['altitudes']: 112 | self.altitudes = state['altitudes'] 113 | return 114 | elif state['precision'] < conf.ALT_PRECISION: 115 | self.altitudes = state['altitudes'] 116 | LOOP.run_until_complete(self.get_all()) 117 | return 118 | elif state['precision'] <= conf.ALT_PRECISION: 119 | pickled_alts = state['altitudes'] 120 | 121 | to_remove = [] 122 | for coords in pickled_alts.keys(): 123 | if coords not in bounds: 124 | to_remove.append(coords) 125 | for key in to_remove: 126 | del pickled_alts[key] 127 | 128 | self.altitudes = pickled_alts 129 | LOOP.run_until_complete(self.get_all()) 130 | return 131 | self.altitudes = {} 132 | LOOP.run_until_complete(self.get_all()) 133 | 134 | def pickle(self): 135 | if self.changed: 136 | state = { 137 | 'altitudes': self.altitudes, 138 | 'precision': conf.ALT_PRECISION, 139 | 'bounds_hash': hash(bounds) 140 | } 141 | dump_pickle('altitudes', state) 142 | self.changed = False 143 | 144 | def get_coords(self, bounds=bounds, precision=conf.ALT_PRECISION): 145 | coords = [] 146 | if bounds.multi: 147 | for b in bounds.polygons: 148 | coords.extend(self.get_coords(b)) 149 | return coords 150 | step = 1 / (10 ** precision) 151 | west, east = bounds.west, bounds.east 152 | existing = self.altitudes.keys() if self.altitudes else False 153 | for lat in float_range(bounds.south, bounds.north, step): 154 | for lon in float_range(west, east, step): 155 | point = lat, lon 156 | if not existing or point not in existing: 157 | coords.append(round_coords(point, precision)) 158 | return coords 159 | 160 | @staticmethod 161 | def chunks(l, n=300): 162 | """Yield successive n-sized chunks from l.""" 163 | for i in range(0, len(l), n): 164 | yield l[i:i + n] 165 | 166 | 167 | sys.modules[__name__] = Altitudes() 168 | -------------------------------------------------------------------------------- /monocle/avatar.py: -------------------------------------------------------------------------------- 1 | from cyrandom import choice, randint 2 | 3 | class MaleAvatar: 4 | hats = ( 5 | "AVATAR_m_hat_default_0", 6 | "AVATAR_m_hat_default_1", 7 | "AVATAR_m_hat_default_2", 8 | "AVATAR_m_hat_default_3", 9 | "AVATAR_m_hat_default_4", 10 | "AVATAR_m_hat_default_5", 11 | "AVATAR_m_hat_empty") 12 | shirts = ( 13 | "AVATAR_m_shirt_default_0", 14 | "AVATAR_m_shirt_default_1", 15 | "AVATAR_m_shirt_default_2", 16 | "AVATAR_m_shirt_default_3", 17 | "AVATAR_m_shirt_default_4", 18 | "AVATAR_m_shirt_default_5", 19 | "AVATAR_m_shirt_default_6", 20 | "AVATAR_m_shirt_default_7", 21 | "AVATAR_m_shirt_default_8", 22 | "AVATAR_m_shirt_default_2B") 23 | bags = ( 24 | "AVATAR_m_backpack_default_0", 25 | "AVATAR_m_backpack_default_1", 26 | "AVATAR_m_backpack_default_2", 27 | "AVATAR_m_backpack_default_3", 28 | "AVATAR_m_backpack_default_4", 29 | "AVATAR_m_backpack_default_5", 30 | "AVATAR_m_backpack_empty") 31 | gloves = ( 32 | "AVATAR_m_gloves_default_0", 33 | "AVATAR_m_gloves_default_1", 34 | "AVATAR_m_gloves_default_2", 35 | "AVATAR_m_gloves_default_3", 36 | "AVATAR_m_gloves_empty") 37 | socks = ( 38 | "AVATAR_m_socks_default_0", 39 | "AVATAR_m_socks_default_1", 40 | "AVATAR_m_socks_default_2", 41 | "AVATAR_m_socks_default_3", 42 | "AVATAR_m_socks_empty") 43 | footwear = ( 44 | "AVATAR_m_shoes_default_0", 45 | "AVATAR_m_shoes_default_1", 46 | "AVATAR_m_shoes_default_2", 47 | "AVATAR_m_shoes_default_3", 48 | "AVATAR_m_shoes_default_4", 49 | "AVATAR_m_shoes_default_5", 50 | "AVATAR_m_shoes_default_6", 51 | "AVATAR_m_shoes_empty") 52 | 53 | def __init__(self): 54 | self.avatar = 0 55 | self.avatar_hair = 'AVATAR_m_hair_default_{}'.format(randint(0, 5)) 56 | self.avatar_eyes = 'AVATAR_m_eyes_{}'.format(randint(0, 4)) 57 | self.skin = randint(0, 3) 58 | self.avatar_hat = choice(self.hats) 59 | self.avatar_shirt = choice(self.shirts) 60 | self.avatar_backpack = choice(self.bags) 61 | self.avatar_gloves = choice(self.gloves) 62 | self.avatar_pants = "AVATAR_m_pants_default_0" 63 | self.avatar_socks = choice(self.socks) 64 | self.avatar_shoes = choice(self.footwear) 65 | self.avatar_glasses = "AVATAR_m_glasses_empty" 66 | 67 | 68 | class FemaleAvatar: 69 | hats = ( 70 | "AVATAR_f_hat_default_A_0", 71 | "AVATAR_f_hat_default_A_1", 72 | "AVATAR_f_hat_default_A_2", 73 | "AVATAR_f_hat_default_A_3", 74 | "AVATAR_f_hat_default_A_4", 75 | "AVATAR_f_hat_default_A_5", 76 | "AVATAR_f_hat_default_B_0", 77 | "AVATAR_f_hat_default_B_1", 78 | "AVATAR_f_hat_empty") 79 | necklaces = ( 80 | "AVATAR_f_necklace_heart_0", 81 | "AVATAR_f_necklace_star_0", 82 | "AVATAR_f_necklace_default_0", 83 | "AVATAR_f_necklace_default_1", 84 | "AVATAR_f_necklace_empty") 85 | bags = ( 86 | "AVATAR_f_backpack_default_0", 87 | "AVATAR_f_backpack_default_1", 88 | "AVATAR_f_backpack_default_2", 89 | "AVATAR_f_backpack_default_3", 90 | "AVATAR_f_backpack_empty") 91 | gloves = ( 92 | "AVATAR_f_gloves_default_0", 93 | "AVATAR_f_gloves_default_1", 94 | "AVATAR_f_gloves_default_2", 95 | "AVATAR_f_gloves_default_3", 96 | "AVATAR_f_gloves_empty") 97 | belts = ( 98 | "AVATAR_f_belt_default_0", 99 | "AVATAR_f_belt_default_1", 100 | "AVATAR_f_belt_default_2", 101 | "AVATAR_f_belt_default_3", 102 | "AVATAR_f_belt_default_4", 103 | "AVATAR_f_belt_default_5", 104 | "AVATAR_f_belt_default_6", 105 | "AVATAR_f_belt_default_7", 106 | "AVATAR_f_belt_default_8", 107 | "AVATAR_f_belt_empty") 108 | bottoms = ( 109 | "AVATAR_f_pants_miniskirt_wave_0", 110 | "AVATAR_f_pants_miniskirt_wave_1", 111 | "AVATAR_f_pants_miniskirt_wave_2", 112 | "AVATAR_f_pants_default_0", 113 | "AVATAR_f_pants_default_1", 114 | "AVATAR_f_pants_default_2", 115 | "AVATAR_f_pants_default_3", 116 | "AVATAR_f_pants_default_4", 117 | "AVATAR_f_pants_default_5") 118 | socks = ( 119 | "AVATAR_f_socks_thighhighs_0", 120 | "AVATAR_f_socks_default_0", 121 | "AVATAR_f_socks_default_1", 122 | "AVATAR_f_socks_default_2", 123 | "AVATAR_f_socks_empty") 124 | footwear = ( 125 | "AVATAR_f_shoes_default_0", 126 | "AVATAR_f_shoes_default_1", 127 | "AVATAR_f_shoes_default_2", 128 | "AVATAR_f_shoes_default_3", 129 | "AVATAR_f_shoes_default_4", 130 | "AVATAR_f_shoes_default_5", 131 | "AVATAR_f_shoes_default_6", 132 | "AVATAR_f_shoes_empty") 133 | 134 | def __init__(self): 135 | self.avatar = 1 136 | self.avatar_hair = 'AVATAR_f_hair_default_{}'.format(randint(0, 5)) 137 | self.avatar_eyes = 'AVATAR_f_eyes_{}'.format(randint(0, 4)) 138 | self.skin = randint(0, 3) 139 | self.avatar_hat = choice(self.hats) 140 | self.avatar_necklace = choice(self.necklaces) 141 | self.avatar_shirt = 'AVATAR_f_shirt_default_{}'.format(randint(0,8)) 142 | self.avatar_backpack = choice(self.bags) 143 | self.avatar_gloves = choice(self.gloves) 144 | self.avatar_belt = choice(self.belts) 145 | self.avatar_pants = choice(self.bottoms) 146 | self.avatar_socks = choice(self.socks) 147 | self.avatar_shoes = choice(self.footwear) 148 | self.avatar_glasses = "AVATAR_f_glasses_empty" 149 | 150 | def new(): 151 | NewAvatar = choice((FemaleAvatar, MaleAvatar)) 152 | return vars(NewAvatar()) 153 | -------------------------------------------------------------------------------- /monocle/bounds.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from . import sanitized as conf 4 | from .utils import get_distance 5 | 6 | 7 | class Bounds: 8 | def __init__(self): 9 | self.north = max(conf.MAP_START[0], conf.MAP_END[0]) 10 | self.south = min(conf.MAP_START[0], conf.MAP_END[0]) 11 | self.east = max(conf.MAP_START[1], conf.MAP_END[1]) 12 | self.west = min(conf.MAP_START[1], conf.MAP_END[1]) 13 | self.center = ((self.north + self.south) / 2, 14 | (self.west + self.east) / 2) 15 | self.multi = False 16 | 17 | def __bool__(self): 18 | """Are boundaries a polygon?""" 19 | return False 20 | 21 | def __contains__(self, p): 22 | return True 23 | 24 | def __hash__(self): 25 | return 0 26 | 27 | @property 28 | def area(self): 29 | """Returns the square kilometers for configured scan area""" 30 | width = get_distance((self.center[0], self.west), (self.center[0], self.east), 2) 31 | height = get_distance((self.south, 0), (self.north, 0), 2) 32 | return round(width * height) 33 | 34 | 35 | class PolyBounds(Bounds): 36 | def __init__(self, polygon=conf.BOUNDARIES): 37 | self.boundaries = prep(polygon) 38 | self.south, self.west, self.north, self.east = polygon.bounds 39 | self.center = polygon.centroid.coords[0] 40 | self.multi = False 41 | self.polygon = polygon 42 | 43 | def __bool__(self): 44 | """Are boundaries a polygon?""" 45 | return True 46 | 47 | def __contains__(self, p): 48 | return self.boundaries.contains(Point(p)) 49 | 50 | def __hash__(self): 51 | return hash((self.south, self.west, self.north, self.east)) 52 | 53 | 54 | class MultiPolyBounds(PolyBounds): 55 | def __init__(self): 56 | super().__init__() 57 | self.multi = True 58 | self.polygons = [PolyBounds(polygon) for polygon in self.polygon] 59 | 60 | def __hash__(self): 61 | return hash(tuple(hash(x) for x in self.polygons)) 62 | 63 | @property 64 | def area(self): 65 | return sum(x.area for x in self.polygons) 66 | 67 | 68 | class RectBounds(Bounds): 69 | def __contains__(self, p): 70 | lat, lon = p 71 | return (self.south <= lat <= self.north and 72 | self.west <= lon <= self.east) 73 | 74 | def __hash__(self): 75 | return hash((self.north, self.east, self.south, self.west)) 76 | 77 | 78 | if conf.BOUNDARIES: 79 | try: 80 | from shapely.geometry import MultiPolygon, Point, Polygon 81 | from shapely.prepared import prep 82 | except ImportError as e: 83 | raise ImportError('BOUNDARIES is set but shapely is not available.') from e 84 | 85 | if isinstance(conf.BOUNDARIES, Polygon): 86 | sys.modules[__name__] = PolyBounds() 87 | elif isinstance(conf.BOUNDARIES, MultiPolygon): 88 | sys.modules[__name__] = MultiPolyBounds() 89 | else: 90 | raise TypeError('BOUNDARIES must be a shapely Polygon.') 91 | elif conf.STAY_WITHIN_MAP: 92 | sys.modules[__name__] = RectBounds() 93 | else: 94 | sys.modules[__name__] = Bounds() 95 | -------------------------------------------------------------------------------- /monocle/db_proc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from queue import Queue 4 | from threading import Thread 5 | from time import sleep 6 | 7 | from . import db 8 | from .shared import get_logger, LOOP 9 | 10 | class DatabaseProcessor(Thread): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.queue = Queue() 15 | self.log = get_logger('dbprocessor') 16 | self.running = True 17 | self.count = 0 18 | self._commit = False 19 | 20 | def __len__(self): 21 | return self.queue.qsize() 22 | 23 | def stop(self): 24 | self.update_mysteries() 25 | self.running = False 26 | self.queue.put({'type': False}) 27 | 28 | def add(self, obj): 29 | self.queue.put(obj) 30 | 31 | def run(self): 32 | session = db.Session() 33 | LOOP.call_soon_threadsafe(self.commit) 34 | 35 | while self.running or not self.queue.empty(): 36 | try: 37 | item = self.queue.get() 38 | item_type = item['type'] 39 | 40 | if item_type == 'pokemon': 41 | db.add_sighting(session, item) 42 | self.count += 1 43 | if not item['inferred']: 44 | db.add_spawnpoint(session, item) 45 | elif item_type == 'mystery': 46 | db.add_mystery(session, item) 47 | self.count += 1 48 | elif item_type == 'fort': 49 | db.add_fort_sighting(session, item) 50 | elif item_type == 'pokestop': 51 | db.add_pokestop(session, item) 52 | elif item_type == 'target': 53 | db.update_failures(session, item['spawn_id'], item['seen']) 54 | elif item_type == 'mystery-update': 55 | db.update_mystery(session, item) 56 | elif item_type is False: 57 | break 58 | self.log.debug('Item saved to db') 59 | if self._commit: 60 | session.commit() 61 | self._commit = False 62 | except Exception as e: 63 | session.rollback() 64 | sleep(5.0) 65 | self.log.exception('A wild {} appeared in the DB processor!', e.__class__.__name__) 66 | try: 67 | session.commit() 68 | except Exception: 69 | pass 70 | session.close() 71 | 72 | def commit(self): 73 | self._commit = True 74 | if self.running: 75 | LOOP.call_later(5, self.commit) 76 | 77 | def update_mysteries(self): 78 | for key, times in db.MYSTERY_CACHE.items(): 79 | first, last = times 80 | if last != first: 81 | encounter_id, spawn_id = key 82 | mystery = { 83 | 'type': 'mystery-update', 84 | 'spawn': spawn_id, 85 | 'encounter': encounter_id, 86 | 'first': first, 87 | 'last': last 88 | } 89 | self.add(mystery) 90 | 91 | sys.modules[__name__] = DatabaseProcessor() 92 | -------------------------------------------------------------------------------- /monocle/landmarks.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | from shapely.geometry import Point, Polygon, shape, box, LineString 4 | from shapely import speedups 5 | from geopy import Nominatim 6 | from pogeo import get_distance 7 | 8 | if speedups.available: 9 | speedups.enable() 10 | 11 | 12 | class FailedQuery(Exception): 13 | """Raised when no location is found.""" 14 | 15 | 16 | class Landmark: 17 | ''' Contains information about user-defined landmarks.''' 18 | log = getLogger('landmarks') 19 | 20 | def __init__(self, name, shortname=None, points=None, query=None, 21 | hashtags=None, phrase=None, is_area=False, query_suffix=None): 22 | self.name = name 23 | self.shortname = shortname 24 | self.is_area = is_area 25 | 26 | if not points and not query: 27 | query = name.lstrip('the ') 28 | 29 | if ((query_suffix and query) and 30 | query_suffix.lower() not in query.lower()): 31 | query = '{} {}'.format(query, query_suffix) 32 | 33 | self.location = None 34 | if query: 35 | self.query_location(query) 36 | elif points: 37 | try: 38 | length = len(points) 39 | if length > 2: 40 | self.location = Polygon(points) 41 | elif length == 2: 42 | self.location = box(points[0][0], points[0][1], 43 | points[1][0], points[1][1]) 44 | elif length == 1: 45 | self.location = Point(*points[0]) 46 | except TypeError: 47 | raise ValueError('points must be a list/tuple of lists/tuples' 48 | ' containing 2 coordinates each') 49 | 50 | if not self.location: 51 | raise ValueError('No location provided for {}. Must provide' 52 | ' either points, or query.'.format(self.name)) 53 | elif not isinstance(self.location, (Point, Polygon, LineString)): 54 | raise NotImplementedError('{} is a {} which is not supported' 55 | .format(self.name, self.location.type)) 56 | self.south, self.west, self.north, self.east = self.location.bounds 57 | 58 | # very imprecise conversion to square meters 59 | self.size = self.location.area * 12100000000 60 | 61 | if phrase: 62 | self.phrase = phrase 63 | elif is_area: 64 | self.phrase = 'in' 65 | else: 66 | self.phrase = 'at' 67 | 68 | self.hashtags = hashtags 69 | 70 | def __contains__(self, coordinates): 71 | """determine if a point is within this object range""" 72 | lat, lon = coordinates 73 | if (self.south <= lat <= self.north and 74 | self.west <= lon <= self.east): 75 | return self.location.contains(Point(lat, lon)) 76 | return False 77 | 78 | def query_location(self, query): 79 | def swap_coords(geojson): 80 | out = [] 81 | for x in geojson: 82 | if isinstance(x, list): 83 | out.append(swap_coords(x)) 84 | else: 85 | return geojson[1], geojson[0] 86 | return out 87 | 88 | nom = Nominatim() 89 | try: 90 | geo = nom.geocode(query=query, geometry='geojson', timeout=3).raw 91 | geojson = geo['geojson'] 92 | except (AttributeError, KeyError): 93 | raise FailedQuery('Query for {} did not return results.'.format(query)) 94 | self.log.info('Nominatim returned {} for {}'.format(geo['display_name'], query)) 95 | geojson['coordinates'] = swap_coords(geojson['coordinates']) 96 | self.location = shape(geojson) 97 | 98 | def get_coordinates(self): 99 | if isinstance(self.location, Polygon): 100 | return tuple(self.location.exterior.coordinates) 101 | else: 102 | return self.location.coords[0] 103 | 104 | def generate_string(self, coordinates): 105 | if coordinates in self: 106 | return '{} {}'.format(self.phrase, self.name) 107 | distance = self.distance_from_point(coordinates) 108 | if distance < 50 or (self.is_area and distance < 100): 109 | return '{} {}'.format(self.phrase, self.name) 110 | else: 111 | return '{:.0f} meters from {}'.format(distance, self.name) 112 | 113 | def distance_from_point(self, coordinates): 114 | point = Point(*coordinates) 115 | if isinstance(self.location, Point): 116 | nearest = self.location 117 | else: 118 | nearest = self.nearest_point(point) 119 | return get_distance(coordinates, nearest.coords[0]) 120 | 121 | def nearest_point(self, point): 122 | '''Find nearest point in geometry, measured from given point.''' 123 | if isinstance(self.location, Polygon): 124 | segs = self.pairs(self.location.exterior.coords) 125 | elif isinstance(self.location, LineString): 126 | segs = self.pairs(self.location.coords) 127 | else: 128 | raise NotImplementedError('project_point_to_object not implemented' 129 | "for geometry type '{}'.".format( 130 | self.location.type)) 131 | 132 | nearest_point = None 133 | min_dist = float("inf") 134 | 135 | for seg_start, seg_end in segs: 136 | line_start = Point(seg_start) 137 | line_end = Point(seg_end) 138 | 139 | intersection_point = self.project_point_to_line( 140 | point, line_start, line_end) 141 | cur_dist = point.distance(intersection_point) 142 | 143 | if cur_dist < min_dist: 144 | min_dist = cur_dist 145 | nearest_point = intersection_point 146 | return nearest_point 147 | 148 | @staticmethod 149 | def pairs(lst): 150 | """Iterate over a list in overlapping pairs.""" 151 | i = iter(lst) 152 | prev = next(i) 153 | for item in i: 154 | yield prev, item 155 | prev = item 156 | 157 | @staticmethod 158 | def project_point_to_line(point, line_start, line_end): 159 | '''Find nearest point on a straight line, 160 | measured from given point.''' 161 | line_magnitude = line_start.distance(line_end) 162 | 163 | u = (((point.x - line_start.x) * (line_end.x - line_start.x) + 164 | (point.y - line_start.y) * (line_end.y - line_start.y)) 165 | / (line_magnitude ** 2)) 166 | 167 | # closest point does not fall within the line segment, 168 | # take the shorter distance to an endpoint 169 | if u < 0.00001 or u > 1: 170 | ix = point.distance(line_start) 171 | iy = point.distance(line_end) 172 | if ix > iy: 173 | return line_end 174 | else: 175 | return line_start 176 | else: 177 | ix = line_start.x + u * (line_end.x - line_start.x) 178 | iy = line_start.y + u * (line_end.y - line_start.y) 179 | return Point([ix, iy]) 180 | 181 | 182 | class Landmarks: 183 | 184 | def __init__(self, query_suffix=None): 185 | self.points_of_interest = set() 186 | self.areas = set() 187 | self.query_suffix = query_suffix 188 | 189 | def add(self, *args, **kwargs): 190 | if ('query_suffix' not in kwargs) and self.query_suffix and ( 191 | 'query' not in kwargs): 192 | kwargs['query_suffix'] = self.query_suffix 193 | landmark = Landmark(*args, **kwargs) 194 | if landmark.is_area: 195 | self.areas.add(landmark) 196 | else: 197 | self.points_of_interest.add(landmark) 198 | if landmark.size < 1: 199 | print(landmark.name, type(landmark.location), '\n') 200 | else: 201 | print(landmark.name, landmark.size, type(landmark.location), '\n') 202 | 203 | def find_landmark(self, coords, max_distance=750): 204 | landmark = find_within(self.points_of_interest, coords) 205 | if landmark: 206 | return landmark 207 | landmark, distance = find_closest(self.points_of_interest, coords) 208 | try: 209 | if distance < max_distance: 210 | return landmark 211 | except TypeError: 212 | pass 213 | 214 | area = find_within(self.areas, coords) 215 | if area: 216 | return area 217 | 218 | area, area_distance = find_closest(self.areas, coords) 219 | 220 | try: 221 | if area and area_distance < distance: 222 | return area 223 | else: 224 | return landmark 225 | except TypeError: 226 | return area 227 | 228 | 229 | def find_within(landmarks, coordinates): 230 | within = [landmark for landmark in landmarks if coordinates in landmark] 231 | found = len(within) 232 | if found == 1: 233 | return within[0] 234 | if found: 235 | landmarks = iter(within) 236 | smallest = next(landmarks) 237 | smallest_size = landmark.size 238 | for landmark in landmarks: 239 | if landmark.size < smallest_size: 240 | smallest = landmark 241 | smallest_size = landmark.size 242 | return smallest 243 | return None 244 | 245 | 246 | def find_closest(landmarks, coordinates): 247 | landmarks = iter(landmarks) 248 | try: 249 | closest_landmark = next(landmarks) 250 | except StopIteration: 251 | return None, None 252 | shortest_distance = closest_landmark.distance_from_point(coordinates) 253 | for landmark in landmarks: 254 | distance = landmark.distance_from_point(coordinates) 255 | if distance <= shortest_distance: 256 | if (distance == shortest_distance 257 | and landmark.size > closest_landmark.size): 258 | continue 259 | shortest_distance = distance 260 | closest_landmark = landmark 261 | return closest_landmark, shortest_distance 262 | -------------------------------------------------------------------------------- /monocle/sanitized.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from numbers import Number 4 | from pathlib import Path 5 | from datetime import datetime 6 | from logging import getLogger 7 | 8 | try: 9 | from . import config 10 | except ImportError as e: 11 | raise ImportError('Please copy config.example.py to config.py and customize it.') from e 12 | 13 | sequence = (tuple, list) 14 | path = (str, Path) 15 | set_sequence = (tuple, list, set, frozenset) 16 | set_sequence_range = (tuple, list, range, set, frozenset) 17 | 18 | worker_count = config.GRID[0] * config.GRID[1] 19 | 20 | _valid_types = { 21 | 'ACCOUNTS': set_sequence, 22 | 'ACCOUNTS_CSV': path, 23 | 'ALT_PRECISION': int, 24 | 'ALT_RANGE': sequence, 25 | 'ALWAYS_NOTIFY': int, 26 | 'ALWAYS_NOTIFY_IDS': set_sequence_range, 27 | 'APP_SIMULATION': bool, 28 | 'AREA_NAME': str, 29 | 'AUTHKEY': bytes, 30 | 'BOOTSTRAP_RADIUS': Number, 31 | 'BOUNDARIES': object, 32 | 'CACHE_CELLS': bool, 33 | 'CAPTCHAS_ALLOWED': int, 34 | 'CAPTCHA_KEY': str, 35 | 'COMPLETE_TUTORIAL': bool, 36 | 'COROUTINES_LIMIT': int, 37 | 'DB': dict, 38 | 'DB_ENGINE': str, 39 | 'DIRECTORY': path, 40 | 'DISCORD_INVITE_ID': str, 41 | 'ENCOUNTER': str, 42 | 'ENCOUNTER_IDS': set_sequence_range, 43 | 'FAILURES_ALLOWED': int, 44 | 'FAVOR_CAPTCHA': bool, 45 | 'FB_PAGE_ID': str, 46 | 'FIXED_OPACITY': bool, 47 | 'FORCED_KILL': bool, 48 | 'FULL_TIME': Number, 49 | 'GIVE_UP_KNOWN': Number, 50 | 'GIVE_UP_UNKNOWN': Number, 51 | 'GOOD_ENOUGH': Number, 52 | 'GOOGLE_MAPS_KEY': str, 53 | 'GRID': sequence, 54 | 'HASHTAGS': set_sequence, 55 | 'HASH_KEY': (str,) + set_sequence, 56 | 'HEATMAP': bool, 57 | 'IGNORE_IVS': bool, 58 | 'IGNORE_RARITY': bool, 59 | 'IMAGE_STATS': bool, 60 | 'INCUBATE_EGGS': bool, 61 | 'INITIAL_SCORE': Number, 62 | 'ITEM_LIMITS': dict, 63 | 'IV_FONT': str, 64 | 'LANDMARKS': object, 65 | 'LANGUAGE': str, 66 | 'LAST_MIGRATION': Number, 67 | 'LOAD_CUSTOM_CSS_FILE': bool, 68 | 'LOAD_CUSTOM_HTML_FILE': bool, 69 | 'LOAD_CUSTOM_JS_FILE': bool, 70 | 'LOGIN_TIMEOUT': Number, 71 | 'MANAGER_ADDRESS': (str, tuple, list), 72 | 'MAP_END': sequence, 73 | 'MAP_FILTER_IDS': sequence, 74 | 'MAP_PROVIDER_ATTRIBUTION': str, 75 | 'MAP_PROVIDER_URL': str, 76 | 'MAP_START': sequence, 77 | 'MAP_WORKERS': bool, 78 | 'MAX_CAPTCHAS': int, 79 | 'MAX_RETRIES': int, 80 | 'MINIMUM_RUNTIME': Number, 81 | 'MINIMUM_SCORE': Number, 82 | 'MORE_POINTS': bool, 83 | 'MOVE_FONT': str, 84 | 'NAME_FONT': str, 85 | 'NEVER_NOTIFY_IDS': set_sequence_range, 86 | 'NOTIFY': bool, 87 | 'NOTIFY_IDS': set_sequence_range, 88 | 'NOTIFY_RANKING': int, 89 | 'PASS': str, 90 | 'PB_API_KEY': str, 91 | 'PB_CHANNEL': int, 92 | 'PLAYER_LOCALE': dict, 93 | 'PROVIDER': str, 94 | 'PROXIES': set_sequence, 95 | 'RARE_IDS': set_sequence_range, 96 | 'RARITY_OVERRIDE': dict, 97 | 'REFRESH_RATE': Number, 98 | 'REPORT_MAPS': bool, 99 | 'REPORT_SINCE': datetime, 100 | 'RESCAN_UNKNOWN': Number, 101 | 'SCAN_DELAY': Number, 102 | 'SEARCH_SLEEP': Number, 103 | 'SHOW_TIMER': bool, 104 | 'SIMULTANEOUS_LOGINS': int, 105 | 'SIMULTANEOUS_SIMULATION': int, 106 | 'SKIP_SPAWN': Number, 107 | 'SMART_THROTTLE': Number, 108 | 'SPAWN_ID_INT': bool, 109 | 'SPEED_LIMIT': Number, 110 | 'SPEED_UNIT': str, 111 | 'SPIN_COOLDOWN': Number, 112 | 'SPIN_POKESTOPS': bool, 113 | 'STAT_REFRESH': Number, 114 | 'STAY_WITHIN_MAP': bool, 115 | 'SWAP_OLDEST': Number, 116 | 'TELEGRAM_BOT_TOKEN': str, 117 | 'TELEGRAM_CHAT_ID': str, 118 | 'TELEGRAM_USERNAME': str, 119 | 'TIME_REQUIRED': Number, 120 | 'TRASH_IDS': set_sequence_range, 121 | 'TWEET_IMAGES': bool, 122 | 'TWITTER_ACCESS_KEY': str, 123 | 'TWITTER_ACCESS_SECRET': str, 124 | 'TWITTER_CONSUMER_KEY': str, 125 | 'TWITTER_CONSUMER_SECRET': str, 126 | 'TWITTER_SCREEN_NAME': str, 127 | 'TZ_OFFSET': Number, 128 | 'UVLOOP': bool, 129 | 'WEBHOOKS': set_sequence 130 | } 131 | 132 | _defaults = { 133 | 'ACCOUNTS': None, 134 | 'ACCOUNTS_CSV': None, 135 | 'ALT_PRECISION': 2, 136 | 'ALT_RANGE': (300, 400), 137 | 'ALWAYS_NOTIFY': 0, 138 | 'ALWAYS_NOTIFY_IDS': set(), 139 | 'APP_SIMULATION': True, 140 | 'AREA_NAME': 'Area', 141 | 'AUTHKEY': b'm3wtw0', 142 | 'BOOTSTRAP_RADIUS': 120, 143 | 'BOUNDARIES': None, 144 | 'CACHE_CELLS': False, 145 | 'CAPTCHAS_ALLOWED': 3, 146 | 'CAPTCHA_KEY': None, 147 | 'COMPLETE_TUTORIAL': False, 148 | 'CONTROL_SOCKS': None, 149 | 'COROUTINES_LIMIT': worker_count, 150 | 'DIRECTORY': '.', 151 | 'DISCORD_INVITE_ID': None, 152 | 'ENCOUNTER': None, 153 | 'ENCOUNTER_IDS': None, 154 | 'FAVOR_CAPTCHA': True, 155 | 'FAILURES_ALLOWED': 2, 156 | 'FB_PAGE_ID': None, 157 | 'FIXED_OPACITY': False, 158 | 'FORCED_KILL': None, 159 | 'FULL_TIME': 1800, 160 | 'GIVE_UP_KNOWN': 75, 161 | 'GIVE_UP_UNKNOWN': 60, 162 | 'GOOD_ENOUGH': 0.1, 163 | 'GOOGLE_MAPS_KEY': '', 164 | 'HASHTAGS': None, 165 | 'IGNORE_IVS': False, 166 | 'IGNORE_RARITY': False, 167 | 'IMAGE_STATS': False, 168 | 'INCUBATE_EGGS': True, 169 | 'INITIAL_RANKING': None, 170 | 'ITEM_LIMITS': None, 171 | 'IV_FONT': 'monospace', 172 | 'LANDMARKS': None, 173 | 'LANGUAGE': 'EN', 174 | 'LAST_MIGRATION': 1481932800, 175 | 'LOAD_CUSTOM_CSS_FILE': False, 176 | 'LOAD_CUSTOM_HTML_FILE': False, 177 | 'LOAD_CUSTOM_JS_FILE': False, 178 | 'LOGIN_TIMEOUT': 2.5, 179 | 'MANAGER_ADDRESS': None, 180 | 'MAP_FILTER_IDS': None, 181 | 'MAP_PROVIDER_URL': '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 182 | 'MAP_PROVIDER_ATTRIBUTION': '© OpenStreetMap contributors', 183 | 'MAP_WORKERS': True, 184 | 'MAX_CAPTCHAS': 0, 185 | 'MAX_RETRIES': 3, 186 | 'MINIMUM_RUNTIME': 10, 187 | 'MORE_POINTS': False, 188 | 'MOVE_FONT': 'sans-serif', 189 | 'NAME_FONT': 'sans-serif', 190 | 'NEVER_NOTIFY_IDS': (), 191 | 'NOTIFY': False, 192 | 'NOTIFY_IDS': None, 193 | 'NOTIFY_RANKING': None, 194 | 'PASS': None, 195 | 'PB_API_KEY': None, 196 | 'PB_CHANNEL': None, 197 | 'PLAYER_LOCALE': {'country': 'US', 'language': 'en', 'timezone': 'America/Denver'}, 198 | 'PROVIDER': None, 199 | 'PROXIES': None, 200 | 'RARE_IDS': (), 201 | 'RARITY_OVERRIDE': {}, 202 | 'REFRESH_RATE': 0.6, 203 | 'REPORT_MAPS': True, 204 | 'REPORT_SINCE': None, 205 | 'RESCAN_UNKNOWN': 90, 206 | 'SCAN_DELAY': 10, 207 | 'SEARCH_SLEEP': 2.5, 208 | 'SHOW_TIMER': False, 209 | 'SIMULTANEOUS_LOGINS': 2, 210 | 'SIMULTANEOUS_SIMULATION': 4, 211 | 'SKIP_SPAWN': 90, 212 | 'SMART_THROTTLE': False, 213 | 'SPAWN_ID_INT': True, 214 | 'SPEED_LIMIT': 19.5, 215 | 'SPEED_UNIT': 'miles', 216 | 'SPIN_COOLDOWN': 300, 217 | 'SPIN_POKESTOPS': True, 218 | 'STAT_REFRESH': 5, 219 | 'STAY_WITHIN_MAP': True, 220 | 'SWAP_OLDEST': 21600 / worker_count, 221 | 'TELEGRAM_BOT_TOKEN': None, 222 | 'TELEGRAM_CHAT_ID': None, 223 | 'TELEGRAM_USERNAME': None, 224 | 'TIME_REQUIRED': 300, 225 | 'TRASH_IDS': (), 226 | 'TWEET_IMAGES': False, 227 | 'TWITTER_ACCESS_KEY': None, 228 | 'TWITTER_ACCESS_SECRET': None, 229 | 'TWITTER_CONSUMER_KEY': None, 230 | 'TWITTER_CONSUMER_SECRET': None, 231 | 'TWITTER_SCREEN_NAME': None, 232 | 'TZ_OFFSET': None, 233 | 'UVLOOP': True, 234 | 'WEBHOOKS': None 235 | } 236 | 237 | 238 | class Config: 239 | __spec__ = __spec__ 240 | __slots__ = tuple(_valid_types.keys()) + ('log',) 241 | 242 | def __init__(self): 243 | self.log = getLogger('sanitizer') 244 | for key, value in (x for x in vars(config).items() if x[0].isupper()): 245 | try: 246 | if isinstance(value, _valid_types[key]): 247 | setattr(self, key, value) 248 | if key in _defaults: 249 | del _defaults[key] 250 | elif key in _defaults and value is _defaults[key]: 251 | setattr(self, key, _defaults.pop(key)) 252 | else: 253 | valid = _valid_types[key] 254 | actual = type(value).__name__ 255 | if isinstance(valid, type): 256 | err = '{} must be {}. Yours is: {}.'.format( 257 | key, valid.__name__, actual) 258 | else: 259 | types = ', '.join((x.__name__ for x in valid)) 260 | err = '{} must be one of {}. Yours is: {}'.format( 261 | key, types, actual) 262 | raise TypeError(err) 263 | except KeyError: 264 | self.log.warning('{} is not a valid config option'.format(key)) 265 | 266 | def __getattr__(self, name): 267 | try: 268 | default = _defaults.pop(name) 269 | setattr(self, name, default) 270 | return default 271 | except KeyError: 272 | if name == '__path__': 273 | return 274 | err = '{} not in config, and no default has been set.'.format(name) 275 | self.log.error(err) 276 | raise AttributeError(err) 277 | 278 | sys.modules[__name__] = Config() 279 | 280 | del _valid_types, config 281 | -------------------------------------------------------------------------------- /monocle/shared.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger, LoggerAdapter 2 | from concurrent.futures import ThreadPoolExecutor 3 | from time import time 4 | from asyncio import get_event_loop 5 | 6 | from aiohttp import ClientSession 7 | from aiopogo import json_dumps 8 | from aiopogo.session import SESSIONS 9 | 10 | from .utils import load_accounts 11 | 12 | 13 | LOOP = get_event_loop() 14 | ACCOUNTS = load_accounts() 15 | 16 | 17 | class SessionManager: 18 | @classmethod 19 | def get(cls): 20 | try: 21 | return cls._session 22 | except AttributeError: 23 | cls._session = ClientSession(connector=SESSIONS.get_connector(False), 24 | loop=LOOP, 25 | conn_timeout=5.0, 26 | read_timeout=30.0, 27 | connector_owner=False, 28 | raise_for_status=True, 29 | json_serialize=json_dumps) 30 | return cls._session 31 | 32 | @classmethod 33 | def close(cls): 34 | try: 35 | cls._session.close() 36 | except Exception: 37 | pass 38 | 39 | 40 | class Message: 41 | def __init__(self, fmt, args): 42 | self.fmt = fmt 43 | self.args = args 44 | 45 | def __str__(self): 46 | return self.fmt.format(*self.args) 47 | 48 | 49 | class StyleAdapter(LoggerAdapter): 50 | def __init__(self, logger, extra=None): 51 | super(StyleAdapter, self).__init__(logger, extra or {}) 52 | 53 | def log(self, level, msg, *args, **kwargs): 54 | if self.isEnabledFor(level): 55 | msg, kwargs = self.process(msg, kwargs) 56 | self.logger._log(level, Message(msg, args), (), **kwargs) 57 | 58 | 59 | def get_logger(name=None): 60 | return StyleAdapter(getLogger(name)) 61 | 62 | 63 | def call_later(delay, cb, *args): 64 | """Thread-safe wrapper for call_later""" 65 | try: 66 | return LOOP.call_soon_threadsafe(LOOP.call_later, delay, cb, *args) 67 | except RuntimeError: 68 | if not LOOP.is_closed(): 69 | raise 70 | 71 | 72 | def call_at(when, cb, *args): 73 | """Run call back at the unix time given""" 74 | delay = when - time() 75 | return call_later(delay, cb, *args) 76 | 77 | 78 | async def run_threaded(cb, *args): 79 | with ThreadPoolExecutor(max_workers=1) as x: 80 | return await LOOP.run_in_executor(x, cb, *args) 81 | -------------------------------------------------------------------------------- /monocle/spawns.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from collections import deque, OrderedDict 4 | from time import time 5 | from itertools import chain 6 | from hashlib import sha256 7 | 8 | from . import bounds, db, sanitized as conf 9 | from .shared import get_logger 10 | from .utils import dump_pickle, load_pickle, get_current_hour, time_until_time 11 | 12 | 13 | class BaseSpawns: 14 | """Manage spawn points and times""" 15 | def __init__(self): 16 | ## Spawns with known times 17 | # {(lat, lon): (spawn_id, spawn_seconds)} 18 | self.known = OrderedDict() 19 | # {spawn_id: despawn_seconds} 20 | self.despawn_times = {} 21 | 22 | ## Spawns with unknown times 23 | # {(lat, lon)} 24 | self.unknown = set() 25 | 26 | self.class_version = 3 27 | self.db_hash = sha256(conf.DB_ENGINE.encode()).digest() 28 | self.log = get_logger('spawns') 29 | 30 | def __len__(self): 31 | return len(self.despawn_times) 32 | 33 | def __bool__(self): 34 | return len(self.despawn_times) > 0 35 | 36 | def update(self): 37 | bound = bool(bounds) 38 | last_migration = conf.LAST_MIGRATION 39 | 40 | with db.session_scope() as session: 41 | query = session.query(db.Spawnpoint) 42 | if bound or conf.STAY_WITHIN_MAP: 43 | query = query.filter(db.Spawnpoint.lat >= bounds.south, 44 | db.Spawnpoint.lat <= bounds.north, 45 | db.Spawnpoint.lon >= bounds.west, 46 | db.Spawnpoint.lon <= bounds.east) 47 | known = {} 48 | for spawn in query: 49 | point = spawn.lat, spawn.lon 50 | 51 | # skip if point is not within boundaries (if applicable) 52 | if bound and point not in bounds: 53 | continue 54 | 55 | if not spawn.updated or spawn.updated <= last_migration: 56 | self.unknown.add(point) 57 | continue 58 | 59 | if spawn.duration == 60: 60 | spawn_time = spawn.despawn_time 61 | else: 62 | spawn_time = (spawn.despawn_time + 1800) % 3600 63 | 64 | self.despawn_times[spawn.spawn_id] = spawn.despawn_time 65 | known[point] = spawn.spawn_id, spawn_time 66 | self.known = OrderedDict(sorted(known.items(), key=lambda k: k[1][1])) 67 | 68 | def after_last(self): 69 | try: 70 | k = next(reversed(self.known)) 71 | seconds = self.known[k][1] 72 | return time() % 3600 > seconds 73 | except (StopIteration, KeyError, TypeError): 74 | return False 75 | 76 | def get_despawn_time(self, spawn_id, seen): 77 | hour = get_current_hour(now=seen) 78 | try: 79 | despawn_time = self.despawn_times[spawn_id] + hour 80 | if seen > despawn_time: 81 | despawn_time += 3600 82 | return despawn_time 83 | except KeyError: 84 | return None 85 | 86 | def unpickle(self): 87 | try: 88 | state = load_pickle('spawns', raise_exception=True) 89 | if all((state['class_version'] == self.class_version, 90 | state['db_hash'] == self.db_hash, 91 | state['bounds_hash'] == hash(bounds), 92 | state['last_migration'] == conf.LAST_MIGRATION)): 93 | self.__dict__.update(state) 94 | return True 95 | else: 96 | self.log.warning('Configuration changed, reloading spawns from DB.') 97 | except FileNotFoundError: 98 | self.log.warning('No spawns pickle found, will create one.') 99 | except (TypeError, KeyError): 100 | self.log.warning('Obsolete or invalid spawns pickle type, reloading from DB.') 101 | return False 102 | 103 | def pickle(self): 104 | state = self.__dict__.copy() 105 | del state['log'] 106 | state.pop('cells_count', None) 107 | state['bounds_hash'] = hash(bounds) 108 | state['last_migration'] = conf.LAST_MIGRATION 109 | dump_pickle('spawns', state) 110 | 111 | @property 112 | def total_length(self): 113 | return len(self.despawn_times) + len(self.unknown) + self.cells_count 114 | 115 | 116 | class Spawns(BaseSpawns): 117 | def __init__(self): 118 | super().__init__() 119 | self.cells_count = 0 120 | 121 | def items(self): 122 | return self.known.items() 123 | 124 | def add_known(self, spawn_id, despawn_time, point): 125 | self.despawn_times[spawn_id] = despawn_time 126 | self.unknown.discard(point) 127 | 128 | def add_unknown(self, point): 129 | self.unknown.add(point) 130 | 131 | def unpickle(self): 132 | result = super().unpickle() 133 | try: 134 | del self.cell_points 135 | except AttributeError: 136 | pass 137 | return result 138 | 139 | def mystery_gen(self): 140 | for mystery in self.unknown.copy(): 141 | yield mystery 142 | 143 | 144 | class MoreSpawns(BaseSpawns): 145 | def __init__(self): 146 | super().__init__() 147 | 148 | ## Coordinates mentioned as "spawn_points" in GetMapObjects response 149 | ## May or may not be actual spawn points, more research is needed. 150 | # {(lat, lon)} 151 | self.cell_points = set() 152 | 153 | def items(self): 154 | # return a copy since it may be modified 155 | return self.known.copy().items() 156 | 157 | def add_known(self, spawn_id, despawn_time, point): 158 | self.despawn_times[spawn_id] = despawn_time 159 | # add so that have_point() will be up to date 160 | self.known[point] = None 161 | self.unknown.discard(point) 162 | self.cell_points.discard(point) 163 | 164 | def add_unknown(self, point): 165 | self.unknown.add(point) 166 | self.cell_points.discard(point) 167 | 168 | def have_point(self, point): 169 | return point in chain(self.cell_points, self.known, self.unknown) 170 | 171 | def mystery_gen(self): 172 | for mystery in chain(self.unknown.copy(), self.cell_points.copy()): 173 | yield mystery 174 | 175 | @property 176 | def cells_count(self): 177 | return len(self.cell_points) 178 | 179 | sys.modules[__name__] = MoreSpawns() if conf.MORE_POINTS else Spawns() 180 | -------------------------------------------------------------------------------- /monocle/static/css/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-container, 8 | .leaflet-pane > svg, 9 | .leaflet-pane > canvas, 10 | .leaflet-zoom-box, 11 | .leaflet-image-layer, 12 | .leaflet-layer { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .leaflet-container { 18 | overflow: hidden; 19 | } 20 | .leaflet-tile, 21 | .leaflet-marker-icon, 22 | .leaflet-marker-shadow { 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ 29 | .leaflet-safari .leaflet-tile { 30 | image-rendering: -webkit-optimize-contrast; 31 | } 32 | /* hack that prevents hw layers "stretching" when loading new tiles */ 33 | .leaflet-safari .leaflet-tile-container { 34 | width: 1600px; 35 | height: 1600px; 36 | -webkit-transform-origin: 0 0; 37 | } 38 | .leaflet-marker-icon, 39 | .leaflet-marker-shadow { 40 | display: block; 41 | } 42 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ 43 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ 44 | .leaflet-container .leaflet-overlay-pane svg, 45 | .leaflet-container .leaflet-marker-pane img, 46 | .leaflet-container .leaflet-shadow-pane img, 47 | .leaflet-container .leaflet-tile-pane img, 48 | .leaflet-container img.leaflet-image-layer { 49 | max-width: none !important; 50 | } 51 | 52 | .leaflet-container.leaflet-touch-zoom { 53 | -ms-touch-action: pan-x pan-y; 54 | touch-action: pan-x pan-y; 55 | } 56 | .leaflet-container.leaflet-touch-drag { 57 | -ms-touch-action: pinch-zoom; 58 | } 59 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { 60 | -ms-touch-action: none; 61 | touch-action: none; 62 | } 63 | .leaflet-container { 64 | -webkit-tap-highlight-color: transparent; 65 | } 66 | .leaflet-container a { 67 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); 68 | } 69 | .leaflet-tile { 70 | filter: inherit; 71 | visibility: hidden; 72 | } 73 | .leaflet-tile-loaded { 74 | visibility: inherit; 75 | } 76 | .leaflet-zoom-box { 77 | width: 0; 78 | height: 0; 79 | -moz-box-sizing: border-box; 80 | box-sizing: border-box; 81 | z-index: 800; 82 | } 83 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 84 | .leaflet-overlay-pane svg { 85 | -moz-user-select: none; 86 | } 87 | 88 | .leaflet-pane { z-index: 400; } 89 | 90 | .leaflet-tile-pane { z-index: 200; } 91 | .leaflet-overlay-pane { z-index: 400; } 92 | .leaflet-shadow-pane { z-index: 500; } 93 | .leaflet-marker-pane { z-index: 600; } 94 | .leaflet-tooltip-pane { z-index: 650; } 95 | .leaflet-popup-pane { z-index: 700; } 96 | 97 | .leaflet-map-pane canvas { z-index: 100; } 98 | .leaflet-map-pane svg { z-index: 200; } 99 | 100 | .leaflet-vml-shape { 101 | width: 1px; 102 | height: 1px; 103 | } 104 | .lvml { 105 | behavior: url(#default#VML); 106 | display: inline-block; 107 | position: absolute; 108 | } 109 | 110 | 111 | /* control positioning */ 112 | 113 | .leaflet-control { 114 | position: relative; 115 | z-index: 800; 116 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 117 | pointer-events: auto; 118 | } 119 | .leaflet-top, 120 | .leaflet-bottom { 121 | position: absolute; 122 | z-index: 1000; 123 | pointer-events: none; 124 | } 125 | .leaflet-top { 126 | top: 0; 127 | } 128 | .leaflet-right { 129 | right: 0; 130 | } 131 | .leaflet-bottom { 132 | bottom: 0; 133 | } 134 | .leaflet-left { 135 | left: 0; 136 | } 137 | .leaflet-control { 138 | float: left; 139 | clear: both; 140 | } 141 | .leaflet-right .leaflet-control { 142 | float: right; 143 | } 144 | .leaflet-top .leaflet-control { 145 | margin-top: 10px; 146 | } 147 | .leaflet-bottom .leaflet-control { 148 | margin-bottom: 10px; 149 | } 150 | .leaflet-left .leaflet-control { 151 | margin-left: 10px; 152 | } 153 | .leaflet-right .leaflet-control { 154 | margin-right: 10px; 155 | } 156 | 157 | 158 | /* zoom and fade animations */ 159 | 160 | .leaflet-fade-anim .leaflet-tile { 161 | will-change: opacity; 162 | } 163 | .leaflet-fade-anim .leaflet-popup { 164 | opacity: 0; 165 | -webkit-transition: opacity 0.2s linear; 166 | -moz-transition: opacity 0.2s linear; 167 | -o-transition: opacity 0.2s linear; 168 | transition: opacity 0.2s linear; 169 | } 170 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 171 | opacity: 1; 172 | } 173 | .leaflet-zoom-animated { 174 | -webkit-transform-origin: 0 0; 175 | -ms-transform-origin: 0 0; 176 | transform-origin: 0 0; 177 | } 178 | .leaflet-zoom-anim .leaflet-zoom-animated { 179 | will-change: transform; 180 | } 181 | .leaflet-zoom-anim .leaflet-zoom-animated { 182 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 183 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 184 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); 185 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 186 | } 187 | .leaflet-zoom-anim .leaflet-tile, 188 | .leaflet-pan-anim .leaflet-tile { 189 | -webkit-transition: none; 190 | -moz-transition: none; 191 | -o-transition: none; 192 | transition: none; 193 | } 194 | 195 | .leaflet-zoom-anim .leaflet-zoom-hide { 196 | visibility: hidden; 197 | } 198 | 199 | 200 | /* cursors */ 201 | 202 | .leaflet-interactive { 203 | cursor: pointer; 204 | } 205 | .leaflet-grab { 206 | cursor: -webkit-grab; 207 | cursor: -moz-grab; 208 | } 209 | .leaflet-crosshair, 210 | .leaflet-crosshair .leaflet-interactive { 211 | cursor: crosshair; 212 | } 213 | .leaflet-popup-pane, 214 | .leaflet-control { 215 | cursor: auto; 216 | } 217 | .leaflet-dragging .leaflet-grab, 218 | .leaflet-dragging .leaflet-grab .leaflet-interactive, 219 | .leaflet-dragging .leaflet-marker-draggable { 220 | cursor: move; 221 | cursor: -webkit-grabbing; 222 | cursor: -moz-grabbing; 223 | } 224 | 225 | /* marker & overlays interactivity */ 226 | .leaflet-marker-icon, 227 | .leaflet-marker-shadow, 228 | .leaflet-image-layer, 229 | .leaflet-pane > svg path, 230 | .leaflet-tile-container { 231 | pointer-events: none; 232 | } 233 | 234 | .leaflet-marker-icon.leaflet-interactive, 235 | .leaflet-image-layer.leaflet-interactive, 236 | .leaflet-pane > svg path.leaflet-interactive { 237 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 238 | pointer-events: auto; 239 | } 240 | 241 | /* visual tweaks */ 242 | 243 | .leaflet-container { 244 | background: #ddd; 245 | outline: 0; 246 | } 247 | .leaflet-container a { 248 | color: #0078A8; 249 | } 250 | .leaflet-container a.leaflet-active { 251 | outline: 2px solid orange; 252 | } 253 | .leaflet-zoom-box { 254 | border: 2px dotted #38f; 255 | background: rgba(255,255,255,0.5); 256 | } 257 | 258 | 259 | /* general typography */ 260 | .leaflet-container { 261 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 262 | } 263 | 264 | 265 | /* general toolbar styles */ 266 | 267 | .leaflet-bar { 268 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 269 | border-radius: 4px; 270 | } 271 | .leaflet-bar a, 272 | .leaflet-bar a:hover { 273 | background-color: #fff; 274 | border-bottom: 1px solid #ccc; 275 | width: 26px; 276 | height: 26px; 277 | line-height: 26px; 278 | display: block; 279 | text-align: center; 280 | text-decoration: none; 281 | color: black; 282 | } 283 | .leaflet-bar a, 284 | .leaflet-control-layers-toggle { 285 | background-position: 50% 50%; 286 | background-repeat: no-repeat; 287 | display: block; 288 | } 289 | .leaflet-bar a:hover { 290 | background-color: #f4f4f4; 291 | } 292 | .leaflet-bar a:first-child { 293 | border-top-left-radius: 4px; 294 | border-top-right-radius: 4px; 295 | } 296 | .leaflet-bar a:last-child { 297 | border-bottom-left-radius: 4px; 298 | border-bottom-right-radius: 4px; 299 | border-bottom: none; 300 | } 301 | .leaflet-bar a.leaflet-disabled { 302 | cursor: default; 303 | background-color: #f4f4f4; 304 | color: #bbb; 305 | } 306 | 307 | .leaflet-touch .leaflet-bar a { 308 | width: 30px; 309 | height: 30px; 310 | line-height: 30px; 311 | } 312 | .leaflet-touch .leaflet-bar a:first-child { 313 | border-top-left-radius: 2px; 314 | border-top-right-radius: 2px; 315 | } 316 | .leaflet-touch .leaflet-bar a:last-child { 317 | border-bottom-left-radius: 2px; 318 | border-bottom-right-radius: 2px; 319 | } 320 | 321 | /* zoom control */ 322 | 323 | .leaflet-control-zoom-in, 324 | .leaflet-control-zoom-out { 325 | font: bold 18px 'Lucida Console', Monaco, monospace; 326 | text-indent: 1px; 327 | } 328 | 329 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { 330 | font-size: 22px; 331 | } 332 | 333 | 334 | /* layers control */ 335 | 336 | .leaflet-control-layers { 337 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 338 | background: #fff; 339 | border-radius: 5px; 340 | } 341 | .leaflet-control-layers-toggle { 342 | background-image: url(../img/layers.png); 343 | width: 36px; 344 | height: 36px; 345 | } 346 | .leaflet-retina .leaflet-control-layers-toggle { 347 | background-image: url(../img/layers-2x.png); 348 | background-size: 26px 26px; 349 | } 350 | .leaflet-touch .leaflet-control-layers-toggle { 351 | width: 44px; 352 | height: 44px; 353 | } 354 | .leaflet-control-layers .leaflet-control-layers-list, 355 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 356 | display: none; 357 | } 358 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 359 | display: block; 360 | position: relative; 361 | } 362 | .leaflet-control-layers-expanded { 363 | padding: 6px 10px 6px 6px; 364 | color: #333; 365 | background: #fff; 366 | } 367 | .leaflet-control-layers-scrollbar { 368 | overflow-y: scroll; 369 | padding-right: 5px; 370 | } 371 | .leaflet-control-layers-selector { 372 | margin-top: 2px; 373 | position: relative; 374 | top: 1px; 375 | } 376 | .leaflet-control-layers label { 377 | display: block; 378 | } 379 | .leaflet-control-layers-separator { 380 | height: 0; 381 | border-top: 1px solid #ddd; 382 | margin: 5px -10px 5px -6px; 383 | } 384 | 385 | /* Default icon URLs */ 386 | .leaflet-default-icon-path { 387 | background-image: url(../img/marker-icon.png); 388 | } 389 | 390 | 391 | /* attribution and scale controls */ 392 | 393 | .leaflet-container .leaflet-control-attribution { 394 | background: #fff; 395 | background: rgba(255, 255, 255, 0.7); 396 | margin: 0; 397 | } 398 | .leaflet-control-attribution, 399 | .leaflet-control-scale-line { 400 | padding: 0 5px; 401 | color: #333; 402 | } 403 | .leaflet-control-attribution a { 404 | text-decoration: none; 405 | } 406 | .leaflet-control-attribution a:hover { 407 | text-decoration: underline; 408 | } 409 | .leaflet-container .leaflet-control-attribution, 410 | .leaflet-container .leaflet-control-scale { 411 | font-size: 11px; 412 | } 413 | .leaflet-left .leaflet-control-scale { 414 | margin-left: 5px; 415 | } 416 | .leaflet-bottom .leaflet-control-scale { 417 | margin-bottom: 5px; 418 | } 419 | .leaflet-control-scale-line { 420 | border: 2px solid #777; 421 | border-top: none; 422 | line-height: 1.1; 423 | padding: 2px 5px 1px; 424 | font-size: 11px; 425 | white-space: nowrap; 426 | overflow: hidden; 427 | -moz-box-sizing: border-box; 428 | box-sizing: border-box; 429 | 430 | background: #fff; 431 | background: rgba(255, 255, 255, 0.5); 432 | } 433 | .leaflet-control-scale-line:not(:first-child) { 434 | border-top: 2px solid #777; 435 | border-bottom: none; 436 | margin-top: -2px; 437 | } 438 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 439 | border-bottom: 2px solid #777; 440 | } 441 | 442 | .leaflet-touch .leaflet-control-attribution, 443 | .leaflet-touch .leaflet-control-layers, 444 | .leaflet-touch .leaflet-bar { 445 | box-shadow: none; 446 | } 447 | .leaflet-touch .leaflet-control-layers, 448 | .leaflet-touch .leaflet-bar { 449 | border: 2px solid rgba(0,0,0,0.2); 450 | background-clip: padding-box; 451 | } 452 | 453 | 454 | /* popup */ 455 | 456 | .leaflet-popup { 457 | position: absolute; 458 | text-align: center; 459 | margin-bottom: 20px; 460 | } 461 | .leaflet-popup-content-wrapper { 462 | padding: 1px; 463 | text-align: left; 464 | border-radius: 12px; 465 | } 466 | .leaflet-popup-content { 467 | margin: 13px 19px; 468 | line-height: 1.4; 469 | } 470 | .leaflet-popup-content p { 471 | margin: 18px 0; 472 | } 473 | .leaflet-popup-tip-container { 474 | width: 40px; 475 | height: 20px; 476 | position: absolute; 477 | left: 50%; 478 | margin-left: -20px; 479 | overflow: hidden; 480 | pointer-events: none; 481 | } 482 | .leaflet-popup-tip { 483 | width: 17px; 484 | height: 17px; 485 | padding: 1px; 486 | 487 | margin: -10px auto 0; 488 | 489 | -webkit-transform: rotate(45deg); 490 | -moz-transform: rotate(45deg); 491 | -ms-transform: rotate(45deg); 492 | -o-transform: rotate(45deg); 493 | transform: rotate(45deg); 494 | } 495 | .leaflet-popup-content-wrapper, 496 | .leaflet-popup-tip { 497 | background: white; 498 | color: #333; 499 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 500 | } 501 | .leaflet-container a.leaflet-popup-close-button { 502 | position: absolute; 503 | top: 0; 504 | right: 0; 505 | padding: 4px 4px 0 0; 506 | border: none; 507 | text-align: center; 508 | width: 18px; 509 | height: 14px; 510 | font: 16px/14px Tahoma, Verdana, sans-serif; 511 | color: #c3c3c3; 512 | text-decoration: none; 513 | font-weight: bold; 514 | background: transparent; 515 | } 516 | .leaflet-container a.leaflet-popup-close-button:hover { 517 | color: #999; 518 | } 519 | .leaflet-popup-scrolled { 520 | overflow: auto; 521 | border-bottom: 1px solid #ddd; 522 | border-top: 1px solid #ddd; 523 | } 524 | 525 | .leaflet-oldie .leaflet-popup-content-wrapper { 526 | zoom: 1; 527 | } 528 | .leaflet-oldie .leaflet-popup-tip { 529 | width: 24px; 530 | margin: 0 auto; 531 | 532 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 533 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 534 | } 535 | .leaflet-oldie .leaflet-popup-tip-container { 536 | margin-top: -1px; 537 | } 538 | 539 | .leaflet-oldie .leaflet-control-zoom, 540 | .leaflet-oldie .leaflet-control-layers, 541 | .leaflet-oldie .leaflet-popup-content-wrapper, 542 | .leaflet-oldie .leaflet-popup-tip { 543 | border: 1px solid #999; 544 | } 545 | 546 | 547 | /* div icon */ 548 | 549 | .leaflet-div-icon { 550 | background: #fff; 551 | border: 1px solid #666; 552 | } 553 | 554 | 555 | /* Tooltip */ 556 | /* Base styles for the element that has a tooltip */ 557 | .leaflet-tooltip { 558 | position: absolute; 559 | padding: 6px; 560 | background-color: #fff; 561 | border: 1px solid #fff; 562 | border-radius: 3px; 563 | color: #222; 564 | white-space: nowrap; 565 | -webkit-user-select: none; 566 | -moz-user-select: none; 567 | -ms-user-select: none; 568 | user-select: none; 569 | pointer-events: none; 570 | box-shadow: 0 1px 3px rgba(0,0,0,0.4); 571 | } 572 | .leaflet-tooltip.leaflet-clickable { 573 | cursor: pointer; 574 | pointer-events: auto; 575 | } 576 | .leaflet-tooltip-top:before, 577 | .leaflet-tooltip-bottom:before, 578 | .leaflet-tooltip-left:before, 579 | .leaflet-tooltip-right:before { 580 | position: absolute; 581 | pointer-events: none; 582 | border: 6px solid transparent; 583 | background: transparent; 584 | content: ""; 585 | } 586 | 587 | /* Directions */ 588 | 589 | .leaflet-tooltip-bottom { 590 | margin-top: 6px; 591 | } 592 | .leaflet-tooltip-top { 593 | margin-top: -6px; 594 | } 595 | .leaflet-tooltip-bottom:before, 596 | .leaflet-tooltip-top:before { 597 | left: 50%; 598 | margin-left: -6px; 599 | } 600 | .leaflet-tooltip-top:before { 601 | bottom: 0; 602 | margin-bottom: -12px; 603 | border-top-color: #fff; 604 | } 605 | .leaflet-tooltip-bottom:before { 606 | top: 0; 607 | margin-top: -12px; 608 | margin-left: -6px; 609 | border-bottom-color: #fff; 610 | } 611 | .leaflet-tooltip-left { 612 | margin-left: -6px; 613 | } 614 | .leaflet-tooltip-right { 615 | margin-left: 6px; 616 | } 617 | .leaflet-tooltip-left:before, 618 | .leaflet-tooltip-right:before { 619 | top: 50%; 620 | margin-top: -6px; 621 | } 622 | .leaflet-tooltip-left:before { 623 | right: 0; 624 | margin-right: -12px; 625 | border-left-color: #fff; 626 | } 627 | .leaflet-tooltip-right:before { 628 | left: 0; 629 | margin-left: -12px; 630 | border-right-color: #fff; 631 | } 632 | -------------------------------------------------------------------------------- /monocle/static/css/main.css: -------------------------------------------------------------------------------- 1 | .map { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | top: 0; 6 | left: 0; 7 | z-index: 100; 8 | } 9 | 10 | .map_btn { 11 | z-index: 1000; 12 | position: absolute; 13 | bottom: 30px; 14 | left: 10px; 15 | padding: 10px; 16 | box-sizing: border-box; 17 | background-color: #fff; 18 | background-size: 24px 24px; 19 | background-position: 4px 4px; 20 | background-repeat: no-repeat; 21 | text-align: center; 22 | width: 32px; 23 | height: 32px; 24 | border-radius: 4px; 25 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 26 | cursor: pointer; 27 | } 28 | 29 | .map_btn:hover { 30 | background-color: #f4f4f4; 31 | } 32 | 33 | .map_btn + .map_btn { 34 | bottom: 70px; 35 | } 36 | 37 | .map_btn + .map_btn + .map_btn { 38 | bottom: 110px; 39 | } 40 | 41 | .map_btn + .map_btn + .map_btn + .map_btn { 42 | bottom: 150px; 43 | } 44 | 45 | .map_btn + .map_btn + .map_btn + .map_btn + .map_btn { 46 | bottom: 190px; 47 | } 48 | 49 | .map_btn + .map_btn + .map_btn + .map_btn + .map_btn + .map_btn { 50 | bottom: 230px; 51 | } 52 | 53 | .fort-icon { 54 | border-radius: 12px; 55 | padding: 2px; 56 | background: #fff; 57 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); 58 | } 59 | 60 | .leaflet-control-layers-toggle { 61 | background-image: url('../img/layers.png'); 62 | } 63 | 64 | .my-location { 65 | background-image: url('../img/my-location.png'); 66 | } 67 | 68 | .facebook-icon { 69 | background-image: url('../img/facebook.png'); 70 | } 71 | 72 | .twitter-icon { 73 | background-image: url('../img/twitter.png'); 74 | } 75 | 76 | .discord-icon { 77 | background-image: url('../img/discord.png'); 78 | } 79 | 80 | .telegram-icon { 81 | background-image: url('../img/telegram.png'); 82 | } 83 | #settings{ 84 | position: absolute; 85 | top: 0; 86 | left: 0; 87 | width: 100%; 88 | height: 100%; 89 | z-index: 1500; 90 | display: none; 91 | background: rgba(255, 255, 255, 1); 92 | opacity: 0; 93 | } 94 | 95 | .scroll-up { 96 | width: 40px; 97 | height: 40px; 98 | position: fixed; 99 | top: 20px; 100 | right: 20px; 101 | display: none; 102 | font-size: 38px; 103 | } 104 | 105 | @media screen and (min-width: 481px) { 106 | #settings { 107 | width: 450px; 108 | transform: translate(-50%, -50%); 109 | left: 50%; 110 | top: 50%; 111 | border-radius: 5px; 112 | box-shadow: 0px 0px 10px 2px rgba(0, 0, 0, 0.55); 113 | height: 525px; 114 | } 115 | 116 | .settings-panel>.panel-body{ 117 | height: 348px; 118 | overflow: auto; 119 | } 120 | 121 | .nav>li>a { 122 | position: relative; 123 | display: block; 124 | padding: 5px 10px; 125 | margin-bottom: 0; 126 | } 127 | 128 | .nav li { 129 | display:table-cell !important; 130 | width: 1%; 131 | } 132 | } 133 | .remaining_text { 134 | border-radius: 25px; 135 | background-color: black; 136 | text-align: center; 137 | background-color: rgba(100, 100, 100, 0.7); 138 | font-size: 10px; 139 | margin: 32px 0 0 0; 140 | padding: 0; 141 | color: white; 142 | width: 100%; 143 | visibility: hidden; 144 | } 145 | 146 | .pokemarker { 147 | position: absolute; 148 | left: -16px; 149 | top: -20px; 150 | width: 32px; 151 | text-align: center; 152 | margin: 0; 153 | padding: 0; 154 | } 155 | 156 | #settings>.page-header{ 157 | padding-bottom: 5px; 158 | margin: 20px 0 10px; 159 | } 160 | 161 | #settings_close_btn{ 162 | font-size: 38px; 163 | } 164 | 165 | .settings-panel{ 166 | display: none; 167 | } 168 | 169 | .settings-panel.active{ 170 | display: block; 171 | } 172 | 173 | .nav-settings{ 174 | margin-bottom: 10px; 175 | } 176 | 177 | .my-settings { 178 | background-image: url('../img/settings.png'); 179 | } 180 | 181 | .leaflet-control-layers-overlays>label{ 182 | margin-bottom: 0; 183 | font-weight: initial; 184 | } 185 | -------------------------------------------------------------------------------- /monocle/static/demo/gyms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/demo/gyms.png -------------------------------------------------------------------------------- /monocle/static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /monocle/static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /monocle/static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /monocle/static/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /monocle/static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /monocle/static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /monocle/static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/favicon.ico -------------------------------------------------------------------------------- /monocle/static/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monocle", 3 | "icons": [ 4 | { 5 | "src": "/static/favicon/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/static/favicon/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /monocle/static/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /monocle/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /monocle/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /monocle/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /monocle/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /monocle/static/img/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/discord.png -------------------------------------------------------------------------------- /monocle/static/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/facebook.png -------------------------------------------------------------------------------- /monocle/static/img/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/layers-2x.png -------------------------------------------------------------------------------- /monocle/static/img/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/layers.png -------------------------------------------------------------------------------- /monocle/static/img/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/marker-icon-2x.png -------------------------------------------------------------------------------- /monocle/static/img/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/marker-icon.png -------------------------------------------------------------------------------- /monocle/static/img/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/marker-shadow.png -------------------------------------------------------------------------------- /monocle/static/img/my-location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/my-location.png -------------------------------------------------------------------------------- /monocle/static/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/settings.png -------------------------------------------------------------------------------- /monocle/static/img/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/telegram.png -------------------------------------------------------------------------------- /monocle/static/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Noctem/Monocle/f6855e9e8a412f791b78085f45545e026da7d6fc/monocle/static/img/twitter.png -------------------------------------------------------------------------------- /monocle/static/js/main.js: -------------------------------------------------------------------------------- 1 | var _last_pokemon_id = 0; 2 | var _pokemon_count = 251; 3 | var _WorkerIconUrl = 'static/monocle-icons/assets/ball.png'; 4 | var _PokestopIconUrl = 'static/monocle-icons/assets/stop.png'; 5 | 6 | var PokemonIcon = L.Icon.extend({ 7 | options: { 8 | popupAnchor: [0, -15] 9 | }, 10 | createIcon: function() { 11 | var div = document.createElement('div'); 12 | div.innerHTML = 13 | '
' + 14 | '
' + 15 | '' + 16 | '
' + 17 | '
' + calculateRemainingTime(this.options.expires_at) + '
' + 18 | '
'; 19 | return div; 20 | } 21 | }); 22 | 23 | var FortIcon = L.Icon.extend({ 24 | options: { 25 | iconSize: [20, 20], 26 | popupAnchor: [0, -10], 27 | className: 'fort-icon' 28 | } 29 | }); 30 | var WorkerIcon = L.Icon.extend({ 31 | options: { 32 | iconSize: [20, 20], 33 | className: 'worker-icon', 34 | iconUrl: _WorkerIconUrl 35 | } 36 | }); 37 | var PokestopIcon = L.Icon.extend({ 38 | options: { 39 | iconSize: [10, 20], 40 | className: 'pokestop-icon', 41 | iconUrl: _PokestopIconUrl 42 | } 43 | }); 44 | 45 | var markers = {}; 46 | var overlays = { 47 | Pokemon: L.layerGroup([]), 48 | Trash: L.layerGroup([]), 49 | Gyms: L.layerGroup([]), 50 | Pokestops: L.layerGroup([]), 51 | Workers: L.layerGroup([]), 52 | Spawns: L.layerGroup([]), 53 | ScanArea: L.layerGroup([]) 54 | }; 55 | 56 | function unsetHidden (event) { 57 | event.target.hidden = false; 58 | } 59 | 60 | function setHidden (event) { 61 | event.target.hidden = true; 62 | } 63 | 64 | function monitor (group, initial) { 65 | group.hidden = initial; 66 | group.on('add', unsetHidden); 67 | group.on('remove', setHidden); 68 | } 69 | 70 | monitor(overlays.Pokemon, false) 71 | monitor(overlays.Trash, true) 72 | monitor(overlays.Gyms, true) 73 | monitor(overlays.Workers, false) 74 | 75 | function getPopupContent (item) { 76 | var diff = (item.expires_at - new Date().getTime() / 1000); 77 | var minutes = parseInt(diff / 60); 78 | var seconds = parseInt(diff - (minutes * 60)); 79 | var expires_at = minutes + 'm ' + seconds + 's'; 80 | var content = '' + item.name + ' - #' + item.pokemon_id + ''; 81 | if(item.atk != undefined){ 82 | var totaliv = 100 * (item.atk + item.def + item.sta) / 45; 83 | content += ' - ' + totaliv.toFixed(2) + '%
'; 84 | content += 'Disappears in: ' + expires_at + '
'; 85 | content += 'Move 1: ' + item.move1 + ' ( ' + item.damage1 + ' dps )
'; 86 | content += 'Move 2: ' + item.move2 + ' ( ' + item.damage2 + ' dps )
'; 87 | content += 'IV: ' + item.atk + ' atk, ' + item.def + ' def, ' + item.sta + ' sta
' 88 | } else { 89 | content += '
Disappears in: ' + expires_at + '
'; 90 | } 91 | content += 'Hide'; 92 | content += '  |  '; 93 | 94 | var userPref = getPreference('filter-'+item.pokemon_id); 95 | if (userPref == 'trash'){ 96 | content += 'Move to Pokemon'; 97 | }else{ 98 | content += 'Move to Trash'; 99 | } 100 | content += '
=> Get directions'; 101 | return content; 102 | } 103 | 104 | function getOpacity (diff) { 105 | if (diff > 300 || getPreference('FIXED_OPACITY') === "1") { 106 | return 1; 107 | } 108 | return 0.5 + diff / 600; 109 | } 110 | 111 | function PokemonMarker (raw) { 112 | var icon = new PokemonIcon({iconUrl: '/static/monocle-icons/icons/' + raw.pokemon_id + '.png', expires_at: raw.expires_at}); 113 | var marker = L.marker([raw.lat, raw.lon], {icon: icon, opacity: 1}); 114 | 115 | var intId = parseInt(raw.id.split('-')[1]); 116 | if (_last_pokemon_id < intId){ 117 | _last_pokemon_id = intId; 118 | } 119 | 120 | if (raw.trash) { 121 | marker.overlay = 'Trash'; 122 | } else { 123 | marker.overlay = 'Pokemon'; 124 | } 125 | var userPreference = getPreference('filter-'+raw.pokemon_id); 126 | if (userPreference === 'pokemon'){ 127 | marker.overlay = 'Pokemon'; 128 | }else if (userPreference === 'trash'){ 129 | marker.overlay = 'Trash'; 130 | }else if (userPreference === 'hidden'){ 131 | marker.overlay = 'Hidden'; 132 | } 133 | marker.raw = raw; 134 | markers[raw.id] = marker; 135 | marker.on('popupopen',function popupopen (event) { 136 | event.popup.options.autoPan = true; // Pan into view once 137 | event.popup.setContent(getPopupContent(event.target.raw)); 138 | event.target.popupInterval = setInterval(function () { 139 | event.popup.setContent(getPopupContent(event.target.raw)); 140 | event.popup.options.autoPan = false; // Don't fight user panning 141 | }, 1000); 142 | }); 143 | marker.on('popupclose', function (event) { 144 | clearInterval(event.target.popupInterval); 145 | }); 146 | marker.setOpacity(getOpacity(marker.raw)); 147 | marker.opacityInterval = setInterval(function () { 148 | if (marker.overlay === "Hidden" || overlays[marker.overlay].hidden) { 149 | return; 150 | } 151 | var diff = marker.raw.expires_at - new Date().getTime() / 1000; 152 | if (diff > 0) { 153 | marker.setOpacity(getOpacity(diff)); 154 | } else { 155 | marker.removeFrom(overlays[marker.overlay]); 156 | markers[marker.raw.id] = undefined; 157 | clearInterval(marker.opacityInterval); 158 | } 159 | }, 2500); 160 | marker.bindPopup(); 161 | return marker; 162 | } 163 | 164 | function FortMarker (raw) { 165 | var icon = new FortIcon({iconUrl: '/static/monocle-icons/forts/' + raw.team + '.png'}); 166 | var marker = L.marker([raw.lat, raw.lon], {icon: icon, opacity: 1}); 167 | marker.raw = raw; 168 | markers[raw.id] = marker; 169 | marker.on('popupopen',function popupopen (event) { 170 | var content = '' 171 | if (raw.team === 0) { 172 | content = 'An empty Gym!' 173 | } 174 | else { 175 | if (raw.team === 1 ) { 176 | content = 'Team Mystic' 177 | } 178 | else if (raw.team === 2 ) { 179 | content = 'Team Valor' 180 | } 181 | else if (raw.team === 3 ) { 182 | content = 'Team Instinct' 183 | } 184 | content += '
Prestige: ' + raw.prestige + 185 | '
Guarding Pokemon: ' + raw.pokemon_name + ' (#' + raw.pokemon_id + ')'; 186 | } 187 | content += '
=> Get directions'; 188 | event.popup.setContent(content); 189 | }); 190 | marker.bindPopup(); 191 | return marker; 192 | } 193 | 194 | function WorkerMarker (raw) { 195 | var icon = new WorkerIcon(); 196 | var marker = L.marker([raw.lat, raw.lon], {icon: icon}); 197 | var circle = L.circle([raw.lat, raw.lon], 70, {weight: 2}); 198 | var group = L.featureGroup([marker, circle]) 199 | .bindPopup('Worker ' + raw.worker_no + '
time: ' + raw.time + '
speed: ' + raw.speed + '
total seen: ' + raw.total_seen + '
visits: ' + raw.visits + '
seen here: ' + raw.seen_here); 200 | return group; 201 | } 202 | 203 | function addPokemonToMap (data, map) { 204 | data.forEach(function (item) { 205 | // Already placed? No need to do anything, then 206 | if (item.id in markers) { 207 | return; 208 | } 209 | var marker = PokemonMarker(item); 210 | if (marker.overlay !== "Hidden"){ 211 | marker.addTo(overlays[marker.overlay]) 212 | } 213 | }); 214 | updateTime(); 215 | if (_updateTimeInterval === null){ 216 | _updateTimeInterval = setInterval(updateTime, 1000); 217 | } 218 | } 219 | 220 | function addGymsToMap (data, map) { 221 | data.forEach(function (item) { 222 | // No change since last time? Then don't do anything 223 | var existing = markers[item.id]; 224 | if (typeof existing !== 'undefined') { 225 | if (existing.raw.sighting_id === item.sighting_id) { 226 | return; 227 | } 228 | existing.removeFrom(overlays.Gyms); 229 | markers[item.id] = undefined; 230 | } 231 | marker = FortMarker(item); 232 | marker.addTo(overlays.Gyms); 233 | }); 234 | } 235 | 236 | function addSpawnsToMap (data, map) { 237 | data.forEach(function (item) { 238 | var circle = L.circle([item.lat, item.lon], 5, {weight: 2}); 239 | var time = '??'; 240 | if (item.despawn_time != null) { 241 | time = '' + Math.floor(item.despawn_time/60) + 'min ' + 242 | (item.despawn_time%60) + 'sec'; 243 | } 244 | else { 245 | circle.setStyle({color: '#f03'}) 246 | } 247 | circle.bindPopup('Spawn ' + item.spawn_id + '' + 248 | '
despawn: ' + time + 249 | '
duration: '+ (item.duration == null ? '30mn' : item.duration + 'mn') + 250 | '
=> Get directions'); 251 | circle.addTo(overlays.Spawns); 252 | }); 253 | } 254 | 255 | function addPokestopsToMap (data, map) { 256 | data.forEach(function (item) { 257 | var icon = new PokestopIcon(); 258 | var marker = L.marker([item.lat, item.lon], {icon: icon}); 259 | marker.raw = item; 260 | marker.bindPopup('Pokestop: ' + item.external_id + '' + 261 | '
=> Get directions'); 262 | marker.addTo(overlays.Pokestops); 263 | }); 264 | } 265 | 266 | function addScanAreaToMap (data, map) { 267 | data.forEach(function (item) { 268 | if (item.type === 'scanarea'){ 269 | L.polyline(item.coords).addTo(overlays.ScanArea); 270 | } else if (item.type === 'scanblacklist'){ 271 | L.polyline(item.coords, {'color':'red'}).addTo(overlays.ScanArea); 272 | } 273 | }); 274 | } 275 | 276 | function addWorkersToMap (data, map) { 277 | overlays.Workers.clearLayers() 278 | data.forEach(function (item) { 279 | marker = WorkerMarker(item); 280 | marker.addTo(overlays.Workers); 281 | }); 282 | } 283 | 284 | function getPokemon () { 285 | if (overlays.Pokemon.hidden && overlays.Trash.hidden) { 286 | return; 287 | } 288 | new Promise(function (resolve, reject) { 289 | $.get('/data?last_id='+_last_pokemon_id, function (response) { 290 | resolve(response); 291 | }); 292 | }).then(function (data) { 293 | addPokemonToMap(data, map); 294 | }); 295 | } 296 | 297 | function getGyms () { 298 | if (overlays.Gyms.hidden) { 299 | return; 300 | } 301 | new Promise(function (resolve, reject) { 302 | $.get('/gym_data', function (response) { 303 | resolve(response); 304 | }); 305 | }).then(function (data) { 306 | addGymsToMap(data, map); 307 | }); 308 | } 309 | 310 | function getSpawnPoints() { 311 | new Promise(function (resolve, reject) { 312 | $.get('/spawnpoints', function (response) { 313 | resolve(response); 314 | }); 315 | }).then(function (data) { 316 | addSpawnsToMap(data, map); 317 | }); 318 | } 319 | 320 | function getPokestops() { 321 | new Promise(function (resolve, reject) { 322 | $.get('/pokestops', function (response) { 323 | resolve(response); 324 | }); 325 | }).then(function (data) { 326 | addPokestopsToMap(data, map); 327 | }); 328 | } 329 | 330 | function getScanAreaCoords() { 331 | new Promise(function (resolve, reject) { 332 | $.get('/scan_coords', function (response) { 333 | resolve(response); 334 | }); 335 | }).then(function (data) { 336 | addScanAreaToMap(data, map); 337 | }); 338 | } 339 | 340 | function getWorkers() { 341 | if (overlays.Workers.hidden) { 342 | return; 343 | } 344 | new Promise(function (resolve, reject) { 345 | $.get('/workers_data', function (response) { 346 | resolve(response); 347 | }); 348 | }).then(function (data) { 349 | addWorkersToMap(data, map); 350 | }); 351 | } 352 | 353 | var map = L.map('main-map', {preferCanvas: true}).setView(_MapCoords, 13); 354 | 355 | overlays.Pokemon.addTo(map); 356 | overlays.ScanArea.addTo(map); 357 | 358 | var control = L.control.layers(null, overlays).addTo(map); 359 | L.tileLayer(_MapProviderUrl, { 360 | opacity: 0.75, 361 | attribution: _MapProviderAttribution 362 | }).addTo(map); 363 | map.whenReady(function () { 364 | $('.my-location').on('click', function () { 365 | map.locate({ enableHighAccurracy: true, setView: true }); 366 | }); 367 | overlays.Gyms.once('add', function(e) { 368 | getGyms(); 369 | }) 370 | overlays.Spawns.once('add', function(e) { 371 | getSpawnPoints(); 372 | }) 373 | overlays.Pokestops.once('add', function(e) { 374 | getPokestops(); 375 | }) 376 | getScanAreaCoords(); 377 | getWorkers(); 378 | overlays.Workers.hidden = true; 379 | setInterval(getWorkers, 14000); 380 | getPokemon(); 381 | setInterval(getPokemon, 30000); 382 | setInterval(getGyms, 110000) 383 | }); 384 | 385 | $("#settings>ul.nav>li>a").on('click', function(){ 386 | // Click handler for each tab button. 387 | $(this).parent().parent().children("li").removeClass('active'); 388 | $(this).parent().addClass('active'); 389 | var panel = $(this).data('panel'); 390 | var item = $("#settings>.settings-panel").removeClass('active') 391 | .filter("[data-panel='"+panel+"']").addClass('active'); 392 | }); 393 | 394 | $("#settings_close_btn").on('click', function(){ 395 | // 'X' button on Settings panel 396 | $("#settings").animate({ 397 | opacity: 0 398 | }, 250, function(){ $(this).hide(); }); 399 | }); 400 | 401 | $('.my-settings').on('click', function () { 402 | // Settings button on bottom-left corner 403 | $("#settings").show().animate({ 404 | opacity: 1 405 | }, 250); 406 | }); 407 | 408 | $('#reset_btn').on('click', function () { 409 | // Reset button in Settings>More 410 | if (confirm("This will reset all your preferences. Are you sure?")){ 411 | localStorage.clear(); 412 | location.reload(); 413 | } 414 | }); 415 | 416 | $('body').on('click', '.popup_filter_link', function () { 417 | var id = $(this).data("pokeid"); 418 | var layer = $(this).data("newlayer").toLowerCase(); 419 | moveToLayer(id, layer); 420 | var item = $("#settings button[data-id='"+id+"']"); 421 | item.removeClass("active").filter("[data-value='"+layer+"']").addClass("active"); 422 | }); 423 | 424 | $('#settings').on('click', '.settings-panel button', function () { 425 | //Handler for each button in every settings-panel. 426 | var item = $(this); 427 | if (item.hasClass('active')){ 428 | return; 429 | } 430 | var id = item.data('id'); 431 | var key = item.parent().data('group'); 432 | var value = item.data('value'); 433 | 434 | item.parent().children("button").removeClass("active"); 435 | item.addClass("active"); 436 | 437 | if (key.indexOf('filter-') > -1){ 438 | // This is a pokemon's filter button 439 | moveToLayer(id, value); 440 | }else{ 441 | setPreference(key, value); 442 | } 443 | }); 444 | 445 | function moveToLayer(id, layer){ 446 | setPreference("filter-"+id, layer); 447 | layer = layer.toLowerCase(); 448 | for(var k in markers) { 449 | var m = markers[k]; 450 | if ((k.indexOf("pokemon-") > -1) && (m !== undefined) && (m.raw.pokemon_id === id)){ 451 | m.removeFrom(overlays[m.overlay]); 452 | if (layer === 'pokemon'){ 453 | m.overlay = "Pokemon"; 454 | m.addTo(overlays.Pokemon); 455 | }else if (layer === 'trash') { 456 | m.overlay = "Trash"; 457 | m.addTo(overlays.Trash); 458 | } 459 | } 460 | } 461 | } 462 | 463 | function populateSettingsPanels(){ 464 | var container = $('.settings-panel[data-panel="filters"]').children('.panel-body'); 465 | var newHtml = ''; 466 | for (var i = 1; i <= _pokemon_count; i++){ 467 | var partHtml = `
468 | 469 |
470 | 471 | 472 | 473 |
474 |
475 | `; 476 | 477 | newHtml += partHtml 478 | } 479 | newHtml += ''; 480 | container.html(newHtml); 481 | } 482 | 483 | function setSettingsDefaults(){ 484 | for (var i = 1; i <= _pokemon_count; i++){ 485 | _defaultSettings['filter-'+i] = (_defaultSettings['TRASH_IDS'].indexOf(i) > -1) ? "trash" : "pokemon"; 486 | }; 487 | 488 | $("#settings div.btn-group").each(function(){ 489 | var item = $(this); 490 | var key = item.data('group'); 491 | var value = getPreference(key); 492 | if (value === false) 493 | value = "0"; 494 | else if (value === true) 495 | value = "1"; 496 | item.children("button").removeClass("active").filter("[data-value='"+value+"']").addClass("active"); 497 | }); 498 | } 499 | populateSettingsPanels(); 500 | setSettingsDefaults(); 501 | 502 | function getPreference(key, ret){ 503 | return localStorage.getItem(key) ? localStorage.getItem(key) : (key in _defaultSettings ? _defaultSettings[key] : ret); 504 | } 505 | 506 | function setPreference(key, val){ 507 | localStorage.setItem(key, val); 508 | } 509 | 510 | $(window).scroll(function () { 511 | if ($(this).scrollTop() > 100) { 512 | $('.scroll-up').fadeIn(); 513 | } else { 514 | $('.scroll-up').fadeOut(); 515 | } 516 | }); 517 | 518 | $("#settings").scroll(function () { 519 | if ($(this).scrollTop() > 100) { 520 | $('.scroll-up').fadeIn(); 521 | } else { 522 | $('.scroll-up').fadeOut(); 523 | } 524 | }); 525 | 526 | $('.scroll-up').click(function () { 527 | $("html, body, #settings").animate({ 528 | scrollTop: 0 529 | }, 500); 530 | return false; 531 | }); 532 | 533 | function calculateRemainingTime(expire_at_timestamp) { 534 | var diff = (expire_at_timestamp - new Date().getTime() / 1000); 535 | var minutes = parseInt(diff / 60); 536 | var seconds = parseInt(diff - (minutes * 60)); 537 | return minutes + ':' + (seconds > 9 ? "" + seconds: "0" + seconds); 538 | } 539 | 540 | function updateTime() { 541 | if (getPreference("SHOW_TIMER") === "1"){ 542 | $(".remaining_text").each(function() { 543 | $(this).css('visibility', 'visible'); 544 | this.innerHTML = calculateRemainingTime($(this).data('expire')); 545 | }); 546 | }else{ 547 | $(".remaining_text").each(function() { 548 | $(this).css('visibility', 'hidden'); 549 | }); 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /monocle/templates/gyms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Pokemon Go {{ area_name }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Congrats, {{ team_names[order[0]] }}! 19 |

20 |

You literally own {{ area_size }} km² of {{ area_name }}.

21 |
22 |
23 | {% for team in order if team != 0%} 24 |
25 | {% endfor %} 26 |
27 |

Out of {{ total_count }} gyms in {{ area_name }}, {{ team_names[order[0]] }} owns {{ count[order[0]] }} ({{ '%0.1f' % percentages[order[0]] }}%) of them. 29 | They have {{ prestige[order[0]] }} ({{ '%0.1f' % 30 | prestige_percent[order[0]] }}%) prestige amassed right now.

31 |

Next in line is {{ team_names[order[1]] }}, which owns {{ 32 | count[order[1]] }} ({{ '%0.1f' % percentages[order[1]] }}%) Gyms right now. They accumulated {{ prestige[order[1]] }} ({{ '%0.1f' % prestige_percent[order[1]] }}%) 34 | prestige.

35 |

And the loser is {{ team_names[order[2]] }} with just {{ 36 | count[order[2]] }} ({{ '%0.1f' % percentages[order[2]] }}%) Gyms in possesion, with {{ prestige[order[2]] }} ({{ '%0.1f' % prestige_percent[order[2]] }}%) 38 | prestige total.

39 |

By the way, {{ count[0] }} ({{ '%0.1f' % percentages[0] }}%) Gyms are 40 | empty.

41 |
42 |

{{ team_names[order[0]] }}'s strongest Gym has {{ 43 | strongest[order[0]][0] }} prestige and is guarded by {{ 44 | strongest[order[0]][2] }}.

45 |

On the other hand, runner up ({{ team_names[order[1]] }}) can boast with their {{ strongest[order[1]][0] }} prestige as the strongest Gym and 47 | {{ strongest[order[1]][2] }} sitting there.

48 |

Last but not least, {{ strongest[order[2]][0] }} 49 | prestige guarded by {{ strongest[order[2]][2] }} is 50 | the stronghold for {{ team_names[order[2]] }}.

51 |
52 |

The most common guardians (strongest Pokemon residing in Gym) are {{ top_guardians[order[0]] }}, {{ top_guardians[order[1]] }} and {{ top_guardians[order[2]] }}.

56 |
57 |

Generated {{ minutes_ago }} minutes ago using data from {{ last_date_minutes_ago }} minutes ago 58 | gathered by Monocle

59 |
60 | {% include 'footer.html' ignore missing %} 61 | 62 | 63 | -------------------------------------------------------------------------------- /monocle/templates/newmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monocle - {{ area_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

Monocle is initializing, please wait.

19 | 26 |
27 | 28 | 29 | {{ social_links }} 30 |
31 | 34 | 42 |
43 |
44 | Filters Settings 45 |
46 |
47 | Panel Content 48 |
49 |
50 |
51 |
52 | More Settings 53 |
54 |
55 |
Fixed markers opacity
56 |
57 | 58 | 59 |
60 |
61 |
Show timer under markers
62 |
63 | 64 | 65 |
66 |
67 |
Reset all preferences
68 | 69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 | {{ extra_css_js }} 78 | 79 | 80 | -------------------------------------------------------------------------------- /monocle/templates/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monocle Report - {{ area_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 104 | 105 | 106 |
107 |

Monocle Report

108 |

Generated on {{ current_date.strftime('%Y-%m-%d %H:%M:%S') }}

109 |

Disclaimer: data may be incomplete due to various issues that might have happened (bugs, unstable 110 | servers, bugs on the servers etc.). If there is data about a sighting of a Pokemon, that spawn almost certainly 111 | happened. On the other hand, there is no guarantee that the database contains all spawns, so there may be 112 | Pokemon missing from this report. Your mileage may vary.

113 |

This report contains statistics about data gathered from scanning {{ area_name }}.

114 |

During that session, {{ total_spawn_count }} Pokemon have been seen on an area of about {{ area_size 115 | }} square km. Data gathering started on {{ session_start.strftime('%Y-%m-%d %H:%M:%S') }} and ended on 116 | {{ session_end.strftime('%Y-%m-%d %H:%M:%S') }}, lasting {{ session_length_hours }} hours. There were 117 | {{ spawns_per_hour }} spawns per hour on average.

118 |

Below chart shows number of spawns seen per 5 minutes blocks:

119 |
{% if google_maps_key %} 120 |

Heatmap

121 |

All noticed spawn locations. The redder the point is, more Pokemon spawn there.

122 |

(will slow down browser!)

123 |
{% endif %} 124 |

Most & least frequently spawning species

125 |

Top 30 that spawned the most number of times during above period:

126 |
127 | {% for icon in icons.top30 %} 128 | 129 | {% if loop.index > 0 and loop.index % 10 == 0 %} 130 |
131 | {% endif %} 132 | {% endfor %} 133 |
134 |
135 |

Bottom 30 that spawned the least number of times during above period, and all of their spawning 136 | places:

137 |
138 | {% for icon in icons.bottom30 %} 139 | 140 | {% if loop.index > 0 and loop.index % 10 == 0 %} 141 |
142 | {% endif %} 143 | {% endfor %} 144 |
145 |
146 |

Evolutions and rare Pokemon

147 |

Stage 2 evolutions and Pokemon subjectively considered "rare" by author of this report, together with 148 | their spawning places:

149 |
150 | {% for icon in icons.rare %} 151 | 152 | {% if loop.index > 0 and loop.index % 10 == 0 %} 153 |
154 | {% endif %} 155 | {% endfor %} 156 |
157 |
158 | {% if google_maps_key %} 159 |
160 | {% endif %} 161 |

Nonexistent species

162 |

Those Pokemon didn't spawn during data gathering period:

163 |
164 | {% for icon in icons.nonexistent %} 165 | 166 | {% if loop.index > 0 and loop.index % 10 == 0 %} 167 |
168 | {% endif %} 169 | {% endfor %} 170 |
171 |

Footnotes

172 |

This report was generated using Monocle, a tool for gathering data about Pokemon Go.

173 |

Check out Monocle on GitHub for more info.

174 |

This report is available under Creative Commons CC-BY-4.0 license: https://creativecommons.org/licenses/by/4.0/.

176 |
177 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /monocle/templates/report_single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monocle Report: {{ pokemon_name }} in {{ area_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | 89 | 90 | 91 |
92 |

Monocle Report: #{{ pokemon_id }} {{ pokemon_name }}

93 |

Generated on {{ current_date.strftime('%Y-%m-%d %H:%M:%S') }}

94 |

Disclaimer: data may be incomplete due to various issues that might have happened (bugs, unstable 95 | servers, bugs on the servers etc.). If there is data about a sighting of a Pokemon, that spawn almost certainly 96 | happened. On the other hand, there is no guarantee that the database contains all spawns, so there may be 97 | Pokemon missing from this report. Your mileage may vary.

98 |

This report contains statistics about data gathered from scanning {{ area_name }} for single species - {{ 99 | pokemon_name }}.

100 |
101
102 |

During that session, {{ total_spawn_count }} sightings of {{ pokemon_name }} have been seen on an area of 103 | about {{ area_size }} square km. Data gathering started on {{ session_start.strftime('%Y-%m-%d %H:%M:%S') 104 | }} and ended on {{ session_end.strftime('%Y-%m-%d %H:%M:%S') }}, lasting {{ session_length_hours }} 105 | hours.

106 | {% if google_maps_key %} 107 |

Heatmap

108 |

All noticed spawn locations of {{ pokemon_name }}. The redder the point is, {{ pokemon_name }} spawned more 109 | often there.

110 |

(will slow down browser!)

111 |
112 | {% endif %} 113 |

Spawning hours

114 |

At what time of the day has {{ pokemon_name }} been seen most number of times?

115 |
116 |

Footnotes

117 |

This report was generated using Monocle, a tool for gathering data about Pokemon Go.

118 |

Check out Monocle on GitHub for more info.

119 |

This report is available under Creative Commons CC-BY-4.0 license: https://creativecommons.org/licenses/by/4.0/.

121 |
122 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /monocle/templates/workersmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monocle - {{ area_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

Monocle is initializing, please wait.

19 |
20 | 21 | {{ social_links }} 22 | 23 | 24 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /monocle/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from os import mkdir 4 | from os.path import join, exists 5 | from sys import platform 6 | from asyncio import sleep 7 | from math import sqrt 8 | from uuid import uuid4 9 | from enum import Enum 10 | from csv import DictReader 11 | from cyrandom import choice, shuffle, uniform 12 | from time import time 13 | from pickle import dump as pickle_dump, load as pickle_load, HIGHEST_PROTOCOL 14 | 15 | from geopy import Point 16 | from geopy.distance import distance 17 | from aiopogo import utilities as pgoapi_utils 18 | from pogeo import get_distance 19 | 20 | from . import bounds, sanitized as conf 21 | 22 | 23 | IPHONES = {'iPhone5,1': 'N41AP', 24 | 'iPhone5,2': 'N42AP', 25 | 'iPhone5,3': 'N48AP', 26 | 'iPhone5,4': 'N49AP', 27 | 'iPhone6,1': 'N51AP', 28 | 'iPhone6,2': 'N53AP', 29 | 'iPhone7,1': 'N56AP', 30 | 'iPhone7,2': 'N61AP', 31 | 'iPhone8,1': 'N71AP', 32 | 'iPhone8,2': 'N66AP', 33 | 'iPhone8,4': 'N69AP', 34 | 'iPhone9,1': 'D10AP', 35 | 'iPhone9,2': 'D11AP', 36 | 'iPhone9,3': 'D101AP', 37 | 'iPhone9,4': 'D111AP'} 38 | 39 | 40 | class Units(Enum): 41 | miles = 1 42 | kilometers = 2 43 | meters = 3 44 | 45 | 46 | def best_factors(n): 47 | return next(((i, n//i) for i in range(int(n**0.5), 0, -1) if n % i == 0)) 48 | 49 | 50 | def percentage_split(seq, percentages): 51 | percentages[-1] += 1.0 - sum(percentages) 52 | prv = 0 53 | size = len(seq) 54 | cum_percentage = 0 55 | for p in percentages: 56 | cum_percentage += p 57 | nxt = int(cum_percentage * size) 58 | yield seq[prv:nxt] 59 | prv = nxt 60 | 61 | 62 | def get_start_coords(worker_no, grid=conf.GRID, bounds=bounds): 63 | """Returns center of square for given worker""" 64 | per_column = int((grid[0] * grid[1]) / grid[0]) 65 | 66 | column = worker_no % per_column 67 | row = int(worker_no / per_column) 68 | part_lat = (bounds.south - bounds.north) / grid[0] 69 | part_lon = (bounds.east - bounds.west) / grid[1] 70 | start_lat = bounds.north + part_lat * row + part_lat / 2 71 | start_lon = bounds.west + part_lon * column + part_lon / 2 72 | return start_lat, start_lon 73 | 74 | 75 | def float_range(start, end, step): 76 | """range for floats, also capable of iterating backwards""" 77 | if start > end: 78 | while end <= start: 79 | yield start 80 | start += -step 81 | else: 82 | while start <= end: 83 | yield start 84 | start += step 85 | 86 | 87 | def get_gains(dist=70): 88 | """Returns lat and lon gain 89 | 90 | Gain is space between circles. 91 | """ 92 | start = Point(*bounds.center) 93 | base = dist * sqrt(3) 94 | height = base * sqrt(3) / 2 95 | dis_a = distance(meters=base) 96 | dis_h = distance(meters=height) 97 | lon_gain = dis_a.destination(point=start, bearing=90).longitude 98 | lat_gain = dis_h.destination(point=start, bearing=0).latitude 99 | return abs(start.latitude - lat_gain), abs(start.longitude - lon_gain) 100 | 101 | 102 | def round_coords(point, precision, _round=round): 103 | return _round(point[0], precision), _round(point[1], precision) 104 | 105 | 106 | def get_bootstrap_points(bounds): 107 | coords = [] 108 | if bounds.multi: 109 | for b in bounds.polygons: 110 | coords.extend(get_bootstrap_points(b)) 111 | return coords 112 | lat_gain, lon_gain = get_gains(conf.BOOTSTRAP_RADIUS) 113 | west, east = bounds.west, bounds.east 114 | bound = bool(bounds) 115 | for map_row, lat in enumerate( 116 | float_range(bounds.south, bounds.north, lat_gain) 117 | ): 118 | row_start_lon = west 119 | if map_row % 2 != 0: 120 | row_start_lon -= 0.5 * lon_gain 121 | for lon in float_range(row_start_lon, east, lon_gain): 122 | point = lat, lon 123 | if not bound or point in bounds: 124 | coords.append(point) 125 | shuffle(coords) 126 | return coords 127 | 128 | 129 | def get_device_info(account): 130 | device_info = {'brand': 'Apple', 131 | 'device': 'iPhone', 132 | 'manufacturer': 'Apple'} 133 | try: 134 | if account['iOS'].startswith('1'): 135 | device_info['product'] = 'iOS' 136 | else: 137 | device_info['product'] = 'iPhone OS' 138 | device_info['hardware'] = account['model'] + '\x00' 139 | device_info['model'] = IPHONES[account['model']] + '\x00' 140 | except (KeyError, AttributeError): 141 | account = generate_device_info(account) 142 | return get_device_info(account) 143 | device_info['version'] = account['iOS'] 144 | device_info['device_id'] = account['id'] 145 | return device_info 146 | 147 | 148 | def generate_device_info(account): 149 | ios8 = ('8.0', '8.0.1', '8.0.2', '8.1', '8.1.1', '8.1.2', '8.1.3', '8.2', '8.3', '8.4', '8.4.1') 150 | ios9 = ('9.0', '9.0.1', '9.0.2', '9.1', '9.2', '9.2.1', '9.3', '9.3.1', '9.3.2', '9.3.3', '9.3.4', '9.3.5') 151 | # 10.0 was only for iPhone 7 and 7 Plus, and is rare 152 | ios10 = ('10.0.1', '10.0.2', '10.0.3', '10.1', '10.1.1', '10.2', '10.2.1', '10.3', '10.3.1', '10.3.2', '10.3.3') 153 | 154 | devices = tuple(IPHONES.keys()) 155 | account['model'] = choice(devices) 156 | 157 | account['id'] = uuid4().hex 158 | 159 | if account['model'] in ('iPhone9,1', 'iPhone9,2', 160 | 'iPhone9,3', 'iPhone9,4'): 161 | account['iOS'] = choice(ios10) 162 | elif account['model'] in ('iPhone8,1', 'iPhone8,2'): 163 | account['iOS'] = choice(ios9 + ios10) 164 | elif account['model'] == 'iPhone8,4': 165 | # iPhone SE started on 9.3 166 | account['iOS'] = choice(('9.3', '9.3.1', '9.3.2', '9.3.3', '9.3.4', '9.3.5') + ios10) 167 | else: 168 | account['iOS'] = choice(ios8 + ios9 + ios10) 169 | 170 | return account 171 | 172 | 173 | def create_account_dict(account): 174 | if isinstance(account, (tuple, list)): 175 | length = len(account) 176 | else: 177 | raise TypeError('Account must be a tuple or list.') 178 | 179 | if length not in (1, 3, 4, 6): 180 | raise ValueError('Each account should have either 3 (account info only) or 6 values (account and device info).') 181 | if length in (1, 4) and (not conf.PASS or not conf.PROVIDER): 182 | raise ValueError('No default PASS or PROVIDER are set.') 183 | 184 | entry = {} 185 | entry['username'] = account[0] 186 | 187 | if length == 1 or length == 4: 188 | entry['password'], entry['provider'] = conf.PASS, conf.PROVIDER 189 | else: 190 | entry['password'], entry['provider'] = account[1:3] 191 | 192 | if length == 4 or length == 6: 193 | entry['model'], entry['iOS'], entry['id'] = account[-3:] 194 | else: 195 | entry = generate_device_info(entry) 196 | 197 | entry['time'] = 0 198 | entry['captcha'] = False 199 | entry['banned'] = False 200 | 201 | return entry 202 | 203 | 204 | def accounts_from_config(pickled_accounts=None): 205 | accounts = {} 206 | for account in conf.ACCOUNTS: 207 | username = account[0] 208 | if pickled_accounts and username in pickled_accounts: 209 | accounts[username] = pickled_accounts[username] 210 | if len(account) == 3 or len(account) == 6: 211 | accounts[username]['password'] = account[1] 212 | accounts[username]['provider'] = account[2] 213 | else: 214 | accounts[username] = create_account_dict(account) 215 | return accounts 216 | 217 | 218 | def accounts_from_csv(new_accounts, pickled_accounts): 219 | accounts = {} 220 | for username, account in new_accounts.items(): 221 | if pickled_accounts: 222 | pickled_account = pickled_accounts.get(username) 223 | if pickled_account: 224 | if pickled_account['password'] != account['password']: 225 | del pickled_account['password'] 226 | account.update(pickled_account) 227 | accounts[username] = account 228 | continue 229 | account['provider'] = account.get('provider') or 'ptc' 230 | if not all(account.get(x) for x in ('model', 'iOS', 'id')): 231 | account = generate_device_info(account) 232 | account['time'] = 0 233 | account['captcha'] = False 234 | account['banned'] = False 235 | accounts[username] = account 236 | return accounts 237 | 238 | 239 | def get_current_hour(now=None, _time=time): 240 | now = now or _time() 241 | return round(now - (now % 3600)) 242 | 243 | 244 | def time_until_time(seconds, seen=None, _time=time): 245 | current_seconds = seen or _time() % 3600 246 | if current_seconds > seconds: 247 | return seconds + 3600 - current_seconds 248 | elif current_seconds + 3600 < seconds: 249 | return seconds - 3600 - current_seconds 250 | else: 251 | return seconds - current_seconds 252 | 253 | 254 | def get_address(): 255 | if conf.MANAGER_ADDRESS: 256 | return conf.MANAGER_ADDRESS 257 | if platform == 'win32': 258 | return r'\\.\pipe\monocle' 259 | if hasattr(socket, 'AF_UNIX'): 260 | return join(conf.DIRECTORY, 'monocle.sock') 261 | return ('127.0.0.1', 5001) 262 | 263 | 264 | def load_pickle(name, raise_exception=False): 265 | location = join(conf.DIRECTORY, 'pickles', '{}.pickle'.format(name)) 266 | try: 267 | with open(location, 'rb') as f: 268 | return pickle_load(f) 269 | except (FileNotFoundError, EOFError): 270 | if raise_exception: 271 | raise FileNotFoundError 272 | else: 273 | return None 274 | 275 | 276 | def dump_pickle(name, var): 277 | folder = join(conf.DIRECTORY, 'pickles') 278 | try: 279 | mkdir(folder) 280 | except FileExistsError: 281 | pass 282 | except Exception as e: 283 | raise OSError("Failed to create 'pickles' folder, please create it manually") from e 284 | 285 | location = join(folder, '{}.pickle'.format(name)) 286 | with open(location, 'wb') as f: 287 | pickle_dump(var, f, HIGHEST_PROTOCOL) 288 | 289 | 290 | def load_accounts(): 291 | pickled_accounts = load_pickle('accounts') 292 | 293 | if conf.ACCOUNTS_CSV: 294 | accounts = load_accounts_csv() 295 | if pickled_accounts and set(pickled_accounts) == set(accounts): 296 | return pickled_accounts 297 | else: 298 | accounts = accounts_from_csv(accounts, pickled_accounts) 299 | elif conf.ACCOUNTS: 300 | if pickled_accounts and set(pickled_accounts) == set(acc[0] for acc in conf.ACCOUNTS): 301 | return pickled_accounts 302 | else: 303 | accounts = accounts_from_config(pickled_accounts) 304 | else: 305 | raise ValueError('Must provide accounts in a CSV or your config file.') 306 | 307 | dump_pickle('accounts', accounts) 308 | return accounts 309 | 310 | 311 | def load_accounts_csv(): 312 | csv_location = join(conf.DIRECTORY, conf.ACCOUNTS_CSV) 313 | with open(csv_location, 'rt') as f: 314 | accounts = {} 315 | reader = DictReader(f) 316 | for row in reader: 317 | accounts[row['username']] = dict(row) 318 | return accounts 319 | 320 | 321 | def randomize_point(point, amount=0.0003, randomize=uniform): 322 | '''Randomize point, by up to ~47 meters by default.''' 323 | lat, lon = point 324 | return ( 325 | randomize(lat - amount, lat + amount), 326 | randomize(lon - amount, lon + amount) 327 | ) 328 | -------------------------------------------------------------------------------- /monocle/web_utils.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from datetime import datetime 3 | from multiprocessing.managers import BaseManager, RemoteError 4 | from time import time 5 | 6 | from monocle import sanitized as conf 7 | from monocle.db import get_forts, Pokestop, session_scope, Sighting, Spawnpoint 8 | from monocle.utils import Units, get_address 9 | from monocle.names import DAMAGE, MOVES, POKEMON 10 | 11 | if conf.MAP_WORKERS: 12 | try: 13 | UNIT = getattr(Units, conf.SPEED_UNIT.lower()) 14 | if UNIT is Units.miles: 15 | UNIT_STRING = "MPH" 16 | elif UNIT is Units.kilometers: 17 | UNIT_STRING = "KMH" 18 | elif UNIT is Units.meters: 19 | UNIT_STRING = "m/h" 20 | except AttributeError: 21 | UNIT_STRING = "MPH" 22 | 23 | def get_args(): 24 | parser = ArgumentParser() 25 | parser.add_argument( 26 | '-H', 27 | '--host', 28 | help='Set web server listening host', 29 | default='0.0.0.0' 30 | ) 31 | parser.add_argument( 32 | '-P', 33 | '--port', 34 | type=int, 35 | help='Set web server listening port', 36 | default=5000 37 | ) 38 | parser.add_argument( 39 | '-d', '--debug', help='Debug Mode', action='store_true' 40 | ) 41 | parser.set_defaults(debug=False) 42 | return parser.parse_args() 43 | 44 | 45 | class AccountManager(BaseManager): pass 46 | AccountManager.register('worker_dict') 47 | 48 | 49 | class Workers: 50 | def __init__(self): 51 | self._data = {} 52 | self._manager = AccountManager(address=get_address(), authkey=conf.AUTHKEY) 53 | 54 | def connect(self): 55 | try: 56 | self._manager.connect() 57 | self._data = self._manager.worker_dict() 58 | except (FileNotFoundError, AttributeError, RemoteError, ConnectionRefusedError, BrokenPipeError): 59 | print('Unable to connect to manager for worker data.') 60 | self._data = {} 61 | 62 | @property 63 | def data(self): 64 | try: 65 | if self._data: 66 | return self._data.items() 67 | else: 68 | raise ValueError 69 | except (FileNotFoundError, RemoteError, ConnectionRefusedError, ValueError, BrokenPipeError): 70 | self.connect() 71 | return self._data.items() 72 | 73 | 74 | def get_worker_markers(workers): 75 | return [{ 76 | 'lat': lat, 77 | 'lon': lon, 78 | 'worker_no': worker_no, 79 | 'time': datetime.fromtimestamp(timestamp).strftime('%I:%M:%S %p'), 80 | 'speed': '{:.1f}{}'.format(speed, UNIT_STRING), 81 | 'total_seen': total_seen, 82 | 'visits': visits, 83 | 'seen_here': seen_here 84 | } for worker_no, ((lat, lon), timestamp, speed, total_seen, visits, seen_here) in workers.data] 85 | 86 | 87 | def sighting_to_marker(pokemon, names=POKEMON, moves=MOVES, damage=DAMAGE): 88 | pokemon_id = pokemon.pokemon_id 89 | marker = { 90 | 'id': 'pokemon-' + str(pokemon.id), 91 | 'trash': pokemon_id in conf.TRASH_IDS, 92 | 'name': names[pokemon_id], 93 | 'pokemon_id': pokemon_id, 94 | 'lat': pokemon.lat, 95 | 'lon': pokemon.lon, 96 | 'expires_at': pokemon.expire_timestamp, 97 | } 98 | move1 = pokemon.move_1 99 | if pokemon.move_1: 100 | move2 = pokemon.move_2 101 | marker['atk'] = pokemon.atk_iv 102 | marker['def'] = pokemon.def_iv 103 | marker['sta'] = pokemon.sta_iv 104 | marker['move1'] = moves[move1] 105 | marker['move2'] = moves[move2] 106 | marker['damage1'] = damage[move1] 107 | marker['damage2'] = damage[move2] 108 | return marker 109 | 110 | 111 | def get_pokemarkers(after_id=0): 112 | with session_scope() as session: 113 | pokemons = session.query(Sighting) \ 114 | .filter(Sighting.expire_timestamp > time(), 115 | Sighting.id > after_id) 116 | if conf.MAP_FILTER_IDS: 117 | pokemons = pokemons.filter(~Sighting.pokemon_id.in_(conf.MAP_FILTER_IDS)) 118 | return tuple(map(sighting_to_marker, pokemons)) 119 | 120 | 121 | def get_gym_markers(names=POKEMON): 122 | with session_scope() as session: 123 | forts = get_forts(session) 124 | return [{ 125 | 'id': 'fort-' + str(fort['fort_id']), 126 | 'sighting_id': fort['id'], 127 | 'prestige': fort['prestige'], 128 | 'pokemon_id': fort['guard_pokemon_id'], 129 | 'pokemon_name': names[fort['guard_pokemon_id']], 130 | 'team': fort['team'], 131 | 'lat': fort['lat'], 132 | 'lon': fort['lon'] 133 | } for fort in forts] 134 | 135 | 136 | def get_spawnpoint_markers(): 137 | with session_scope() as session: 138 | spawns = session.query(Spawnpoint) 139 | return [{ 140 | 'spawn_id': spawn.spawn_id, 141 | 'despawn_time': spawn.despawn_time, 142 | 'lat': spawn.lat, 143 | 'lon': spawn.lon, 144 | 'duration': spawn.duration 145 | } for spawn in spawns] 146 | 147 | if conf.BOUNDARIES: 148 | from shapely.geometry import mapping 149 | 150 | def get_scan_coords(): 151 | coordinates = mapping(conf.BOUNDARIES)['coordinates'] 152 | coords = coordinates[0] 153 | markers = [{ 154 | 'type': 'scanarea', 155 | 'coords': coords 156 | }] 157 | for blacklist in coordinates[1:]: 158 | markers.append({ 159 | 'type': 'scanblacklist', 160 | 'coords': blacklist 161 | }) 162 | return markers 163 | else: 164 | def get_scan_coords(): 165 | return ({ 166 | 'type': 'scanarea', 167 | 'coords': (conf.MAP_START, (conf.MAP_START[0], conf.MAP_END[1]), 168 | conf.MAP_END, (conf.MAP_END[0], conf.MAP_START[1]), conf.MAP_START) 169 | },) 170 | 171 | 172 | def get_pokestop_markers(): 173 | with session_scope() as session: 174 | pokestops = session.query(Pokestop) 175 | return [{ 176 | 'external_id': pokestop.external_id, 177 | 'lat': pokestop.lat, 178 | 'lon': pokestop.lon 179 | } for pokestop in pokestops] 180 | 181 | 182 | def sighting_to_report_marker(sighting): 183 | return { 184 | 'icon': 'static/monocle-icons/icons/{}.png'.format(sighting.pokemon_id), 185 | 'lat': sighting.lat, 186 | 'lon': sighting.lon, 187 | } 188 | 189 | -------------------------------------------------------------------------------- /optional-requirements.txt: -------------------------------------------------------------------------------- 1 | asyncpushbullet>=0.12 2 | peony-twitter>=1.0.0 3 | gpsoauth>=0.4.0 4 | shapely>=1.3.0 5 | selenium>=3.0 6 | uvloop>=0.7.0 7 | gpsoauth>=0.4.0 8 | psycopg2>=2.6 9 | cchardet>=2.1.0 10 | aiodns>=1.1.0 11 | aiosocks>=0.2.3 12 | ujson>=1.35 13 | sanic>=0.3 14 | asyncpg>=0.8 15 | mysqlclient>=1.3 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | geopy>=1.11.0 2 | protobuf>=3.0.0 3 | flask>=0.11.1 4 | sqlalchemy>=1.1.0 5 | aiopogo>=2.0.2,<2.1 6 | polyline>=1.3.1 7 | aiohttp>=2.1,<2.3 8 | pogeo==0.3.* 9 | cyrandom>=0.1.2 10 | -------------------------------------------------------------------------------- /scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import monocle.sanitized as conf 4 | 5 | from asyncio import gather, set_event_loop_policy, Task, wait_for, TimeoutError 6 | try: 7 | if conf.UVLOOP: 8 | from uvloop import EventLoopPolicy 9 | set_event_loop_policy(EventLoopPolicy()) 10 | except ImportError: 11 | pass 12 | 13 | from multiprocessing.managers import BaseManager, DictProxy 14 | from queue import Queue, Full 15 | from argparse import ArgumentParser 16 | from signal import signal, SIGINT, SIGTERM, SIG_IGN 17 | from logging import getLogger, basicConfig, WARNING, INFO 18 | from logging.handlers import RotatingFileHandler 19 | from os.path import exists, join 20 | from sys import platform 21 | from time import monotonic, sleep 22 | 23 | from sqlalchemy.exc import DBAPIError 24 | from aiopogo import close_sessions, activate_hash_server 25 | 26 | from monocle.shared import LOOP, get_logger, SessionManager, ACCOUNTS 27 | from monocle.utils import get_address, dump_pickle 28 | from monocle.worker import Worker 29 | from monocle.overseer import Overseer 30 | from monocle.db import FORT_CACHE 31 | from monocle import altitudes, db_proc, spawns 32 | 33 | 34 | class AccountManager(BaseManager): 35 | pass 36 | 37 | 38 | class CustomQueue(Queue): 39 | def full_wait(self, maxsize=0, timeout=None): 40 | '''Block until queue size falls below maxsize''' 41 | starttime = monotonic() 42 | with self.not_full: 43 | if maxsize > 0: 44 | if timeout is None: 45 | while self._qsize() >= maxsize: 46 | self.not_full.wait() 47 | elif timeout < 0: 48 | raise ValueError("'timeout' must be a non-negative number") 49 | else: 50 | endtime = monotonic() + timeout 51 | while self._qsize() >= maxsize: 52 | remaining = endtime - monotonic() 53 | if remaining <= 0.0: 54 | raise Full 55 | self.not_full.wait(remaining) 56 | self.not_empty.notify() 57 | endtime = monotonic() 58 | return endtime - starttime 59 | 60 | 61 | _captcha_queue = CustomQueue() 62 | _extra_queue = Queue() 63 | _worker_dict = {} 64 | 65 | def get_captchas(): 66 | return _captcha_queue 67 | 68 | def get_extras(): 69 | return _extra_queue 70 | 71 | def get_workers(): 72 | return _worker_dict 73 | 74 | def mgr_init(): 75 | signal(SIGINT, SIG_IGN) 76 | 77 | 78 | def parse_args(): 79 | parser = ArgumentParser() 80 | parser.add_argument( 81 | '--no-status-bar', 82 | dest='status_bar', 83 | help='Log to console instead of displaying status bar', 84 | action='store_false' 85 | ) 86 | parser.add_argument( 87 | '--log-level', 88 | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], 89 | default=WARNING 90 | ) 91 | parser.add_argument( 92 | '--bootstrap', 93 | dest='bootstrap', 94 | help='Bootstrap even if spawns are known.', 95 | action='store_true' 96 | ) 97 | parser.add_argument( 98 | '--no-pickle', 99 | dest='pickle', 100 | help='Do not load spawns from pickle', 101 | action='store_false' 102 | ) 103 | return parser.parse_args() 104 | 105 | 106 | def configure_logger(filename='scan.log'): 107 | if filename: 108 | handlers = (RotatingFileHandler(filename, maxBytes=500000, backupCount=4),) 109 | else: 110 | handlers = None 111 | basicConfig( 112 | format='[{asctime}][{levelname:>8s}][{name}] {message}', 113 | datefmt='%Y-%m-%d %X', 114 | style='{', 115 | level=INFO, 116 | handlers=handlers 117 | ) 118 | 119 | 120 | def exception_handler(loop, context): 121 | try: 122 | log = getLogger('eventloop') 123 | log.error('A wild exception appeared!') 124 | log.error(context) 125 | except Exception: 126 | print('Exception in exception handler.') 127 | 128 | 129 | def cleanup(overseer, manager): 130 | try: 131 | overseer.print_handle.cancel() 132 | overseer.running = False 133 | print('Exiting, please wait until all tasks finish') 134 | 135 | log = get_logger('cleanup') 136 | print('Finishing tasks...') 137 | 138 | LOOP.create_task(overseer.exit_progress()) 139 | pending = gather(*Task.all_tasks(loop=LOOP), return_exceptions=True) 140 | try: 141 | LOOP.run_until_complete(wait_for(pending, 40)) 142 | except TimeoutError as e: 143 | print('Coroutine completion timed out, moving on.') 144 | except Exception as e: 145 | log = get_logger('cleanup') 146 | log.exception('A wild {} appeared during exit!', e.__class__.__name__) 147 | 148 | db_proc.stop() 149 | overseer.refresh_dict() 150 | 151 | print('Dumping pickles...') 152 | dump_pickle('accounts', ACCOUNTS) 153 | FORT_CACHE.pickle() 154 | altitudes.pickle() 155 | if conf.CACHE_CELLS: 156 | dump_pickle('cells', Worker.cells) 157 | 158 | spawns.pickle() 159 | while not db_proc.queue.empty(): 160 | pending = db_proc.queue.qsize() 161 | # Spaces at the end are important, as they clear previously printed 162 | # output - \r doesn't clean whole line 163 | print('{} DB items pending '.format(pending), end='\r') 164 | sleep(.5) 165 | finally: 166 | print('Closing pipes, sessions, and event loop...') 167 | manager.shutdown() 168 | SessionManager.close() 169 | close_sessions() 170 | LOOP.close() 171 | print('Done.') 172 | 173 | 174 | def main(): 175 | args = parse_args() 176 | log = get_logger() 177 | if args.status_bar: 178 | configure_logger(filename=join(conf.DIRECTORY, 'scan.log')) 179 | log.info('-' * 37) 180 | log.info('Starting up!') 181 | else: 182 | configure_logger(filename=None) 183 | log.setLevel(args.log_level) 184 | 185 | AccountManager.register('captcha_queue', callable=get_captchas) 186 | AccountManager.register('extra_queue', callable=get_extras) 187 | if conf.MAP_WORKERS: 188 | AccountManager.register('worker_dict', callable=get_workers, 189 | proxytype=DictProxy) 190 | address = get_address() 191 | manager = AccountManager(address=address, authkey=conf.AUTHKEY) 192 | try: 193 | manager.start(mgr_init) 194 | except (OSError, EOFError) as e: 195 | if platform == 'win32' or not isinstance(address, str): 196 | raise OSError('Another instance is running with the same manager address. Stop that process or change your MANAGER_ADDRESS.') from e 197 | else: 198 | raise OSError('Another instance is running with the same socket. Stop that process or: rm {}'.format(address)) from e 199 | 200 | LOOP.set_exception_handler(exception_handler) 201 | 202 | overseer = Overseer(manager) 203 | overseer.start(args.status_bar) 204 | launcher = LOOP.create_task(overseer.launch(args.bootstrap, args.pickle)) 205 | activate_hash_server(conf.HASH_KEY) 206 | if platform != 'win32': 207 | LOOP.add_signal_handler(SIGINT, launcher.cancel) 208 | LOOP.add_signal_handler(SIGTERM, launcher.cancel) 209 | try: 210 | LOOP.run_until_complete(launcher) 211 | except (KeyboardInterrupt, SystemExit): 212 | launcher.cancel() 213 | finally: 214 | cleanup(overseer, manager) 215 | 216 | 217 | if __name__ == '__main__': 218 | main() 219 | -------------------------------------------------------------------------------- /scripts/create_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | monocle_dir = Path(__file__).resolve().parents[1] 7 | sys.path.append(str(monocle_dir)) 8 | 9 | from monocle.db import Base, _engine 10 | 11 | Base.metadata.create_all(_engine) 12 | print('Done!') 13 | -------------------------------------------------------------------------------- /scripts/export_accounts_csv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import csv 4 | import sys 5 | 6 | from datetime import datetime 7 | from pathlib import Path 8 | 9 | monocle_dir = Path(__file__).resolve().parents[1] 10 | sys.path.append(str(monocle_dir)) 11 | 12 | from monocle.shared import ACCOUNTS 13 | 14 | accounts_file = monocle_dir / 'accounts.csv' 15 | try: 16 | now = datetime.now().strftime("%Y-%m-%d-%H%M") 17 | accounts_file.rename('accounts-{}.csv'.format(now)) 18 | except FileNotFoundError: 19 | pass 20 | 21 | banned = [] 22 | 23 | with accounts_file.open('w') as csvfile: 24 | writer = csv.writer(csvfile, delimiter=',') 25 | writer.writerow(('username', 'password', 'provider', 'model', 'iOS', 'id')) 26 | for account in ACCOUNTS.values(): 27 | if account.get('banned', False): 28 | banned.append(account) 29 | continue 30 | writer.writerow((account['username'], 31 | account['password'], 32 | account['provider'], 33 | account['model'], 34 | account['iOS'], 35 | account['id'])) 36 | 37 | if banned: 38 | banned_file = monocle_dir / 'banned.csv' 39 | write_header = not banned_file.exists() 40 | with banned_file.open('a') as csvfile: 41 | writer = csv.writer(csvfile, delimiter=',') 42 | if write_header: 43 | writer.writerow(('username', 'password', 'provider', 'level', 'created', 'last used')) 44 | for account in banned: 45 | row = [account['username'], account['password'], account['provider']] 46 | row.append(account.get('level')) 47 | try: 48 | row.append(datetime.fromtimestamp(account['created']).strftime('%x %X')) 49 | except KeyError: 50 | row.append(None) 51 | try: 52 | row.append(datetime.fromtimestamp(account['time']).strftime('%x %X')) 53 | except KeyError: 54 | row.append(None) 55 | writer.writerow(row) 56 | 57 | print('Done!') 58 | -------------------------------------------------------------------------------- /scripts/pickle_landmarks.example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import pickle 5 | 6 | from pathlib import Path 7 | 8 | monocle_dir = Path(__file__).resolve().parents[1] 9 | sys.path.append(str(monocle_dir)) 10 | 11 | from monocle.landmarks import Landmarks 12 | 13 | pickle_dir = monocle_dir / 'pickles' 14 | pickle_dir.mkdir(exist_ok=True) 15 | 16 | LANDMARKS = Landmarks(query_suffix='Salt Lake City') 17 | 18 | # replace the following with your own landmarks 19 | LANDMARKS.add('Rice Eccles Stadium', hashtags={'Utes'}) 20 | LANDMARKS.add('the Salt Lake Temple', hashtags={'TempleSquare'}) 21 | LANDMARKS.add('City Creek Center', points=((40.769210, -111.893901), (40.767231, -111.888275)), hashtags={'CityCreek'}) 22 | LANDMARKS.add('the State Capitol', query='Utah State Capitol Building') 23 | LANDMARKS.add('the University of Utah', hashtags={'Utes'}, phrase='at', is_area=True) 24 | LANDMARKS.add('Yalecrest', points=((40.750263, -111.836502), (40.750377, -111.851108), (40.751515, -111.853833), (40.741212, -111.853909), (40.741188, -111.836519)), is_area=True) 25 | 26 | pickle_path = pickle_dir / 'landmarks.pickle' 27 | with pickle_path.open('wb') as f: 28 | pickle.dump(LANDMARKS, f, pickle.HIGHEST_PROTOCOL) 29 | 30 | 31 | print('\033[94mDone. Now add the following to your config:\n\033[92m', 32 | 'import pickle', 33 | "with open('pickles/landmarks.pickle', 'rb') as f:", 34 | ' LANDMARKS = pickle.load(f)', 35 | sep='\n') 36 | -------------------------------------------------------------------------------- /scripts/print_accounts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pickle import load 4 | from pprint import PrettyPrinter 5 | from pathlib import Path 6 | from datetime import datetime 7 | 8 | pickle_path = Path(__file__).resolve().parents[1] / 'pickles' / 'accounts.pickle' 9 | 10 | with pickle_path.open('rb') as f: 11 | accounts = load(f) 12 | 13 | for account in accounts.values(): 14 | if 'time' in account: 15 | account['time'] = datetime.fromtimestamp(account['time']).strftime('%x %X') 16 | if 'created' in account: 17 | account['created'] = datetime.fromtimestamp(account['created']).strftime('%x %X') 18 | if 'expiry' in account: 19 | account['expiry'] = datetime.fromtimestamp(account['expiry']).strftime('%x %X') 20 | 21 | pp = PrettyPrinter(indent=3) 22 | pp.pprint(accounts) 23 | -------------------------------------------------------------------------------- /scripts/print_levels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pickle import load 4 | from pprint import PrettyPrinter 5 | from pathlib import Path 6 | 7 | pickle_path = Path(__file__).resolve().parents[1] / 'pickles' / 'accounts.pickle' 8 | 9 | with pickle_path.open('rb') as f: 10 | accounts = load(f) 11 | 12 | for username, account in accounts.items(): 13 | if 'level' in account: 14 | print(username, '-', account['level']) 15 | else: 16 | print(username, '- unknown') 17 | -------------------------------------------------------------------------------- /scripts/print_spawns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pickle import load 4 | from pprint import PrettyPrinter 5 | from pathlib import Path 6 | 7 | pickle_path = Path(__file__).resolve().parents[1] / 'pickles' / 'spawns.pickle' 8 | 9 | with pickle_path.open('rb') as f: 10 | spawns = load(f) 11 | 12 | pp = PrettyPrinter(indent=3) 13 | pp.pprint(spawns) 14 | -------------------------------------------------------------------------------- /scripts/test_notifications.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asyncio import get_event_loop, set_event_loop_policy 4 | from pathlib import Path 5 | from cyrandom import uniform, randint, choice 6 | from argparse import ArgumentParser 7 | 8 | try: 9 | from uvloop import EventLoopPolicy 10 | set_event_loop_policy(EventLoopPolicy()) 11 | except ImportError: 12 | pass 13 | 14 | import time 15 | import logging 16 | import sys 17 | 18 | monocle_dir = Path(__file__).resolve().parents[1] 19 | sys.path.append(str(monocle_dir)) 20 | 21 | from monocle import names, sanitized as conf 22 | 23 | parser = ArgumentParser() 24 | parser.add_argument( 25 | '-i', '--id', 26 | type=int, 27 | help='Pokémon ID to notify about' 28 | ) 29 | parser.add_argument( 30 | '-lat', '--latitude', 31 | type=float, 32 | help='latitude for fake spawn' 33 | ) 34 | parser.add_argument( 35 | '-lon', '--longitude', 36 | type=float, 37 | help='longitude for fake spawn' 38 | ) 39 | parser.add_argument( 40 | '-r', '--remaining', 41 | type=int, 42 | help='seconds remaining on fake spawn' 43 | ) 44 | parser.add_argument( 45 | '-u', '--unmodified', 46 | action='store_true', 47 | help="don't add ID to ALWAYS_NOTIFY_IDS" 48 | ) 49 | args = parser.parse_args() 50 | 51 | if args.id is not None: 52 | pokemon_id = args.id 53 | if args.id == 0: 54 | names.POKEMON[0] = 'Test' 55 | else: 56 | pokemon_id = randint(1, 252) 57 | 58 | if not args.unmodified: 59 | conf.ALWAYS_NOTIFY_IDS = {pokemon_id} 60 | 61 | conf.HASHTAGS = {'test'} 62 | 63 | from monocle.notification import Notifier 64 | from monocle.shared import SessionManager 65 | from monocle.names import MOVES 66 | 67 | root = logging.getLogger() 68 | root.setLevel(logging.DEBUG) 69 | ch = logging.StreamHandler(sys.stdout) 70 | ch.setLevel(logging.DEBUG) 71 | root.addHandler(ch) 72 | 73 | MOVES = tuple(MOVES.keys()) 74 | 75 | if args.latitude is not None: 76 | lat = args.latitude 77 | else: 78 | lat = uniform(conf.MAP_START[0], conf.MAP_END[0]) 79 | 80 | if args.longitude is not None: 81 | lon = args.longitude 82 | else: 83 | lon = uniform(conf.MAP_START[1], conf.MAP_END[1]) 84 | 85 | if args.remaining: 86 | tth = args.remaining 87 | else: 88 | tth = uniform(89, 3599) 89 | 90 | now = time.time() 91 | 92 | pokemon = { 93 | 'encounter_id': 93253523, 94 | 'spawn_id': 3502935, 95 | 'pokemon_id': pokemon_id, 96 | 'time_till_hidden': tth, 97 | 'lat': lat, 98 | 'lon': lon, 99 | 'individual_attack': randint(0, 15), 100 | 'individual_defense': randint(0, 15), 101 | 'individual_stamina': randint(0, 15), 102 | 'seen': now, 103 | 'move_1': choice(MOVES), 104 | 'move_2': choice(MOVES), 105 | 'valid': True, 106 | 'expire_timestamp': now + tth 107 | } 108 | 109 | notifier = Notifier() 110 | 111 | loop = get_event_loop() 112 | 113 | if loop.run_until_complete(notifier.notify(pokemon, randint(1, 2))): 114 | print('Success') 115 | else: 116 | print('Failure') 117 | 118 | SessionManager.close() 119 | loop.close() 120 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | from os.path import exists 5 | from shutil import copyfile 6 | 7 | from monocle import __version__ as version, __title__ as name 8 | 9 | if not exists('monocle/config.py'): 10 | copyfile('config.example.py', 'monocle/config.py') 11 | 12 | setup( 13 | name=name, 14 | version=version, 15 | packages=(name,), 16 | include_package_data=True, 17 | zip_safe=False, 18 | scripts=('scan.py', 'web.py', 'web_sanic.py', 'gyms.py', 'solve_captchas.py'), 19 | install_requires=[ 20 | 'geopy>=1.11.0', 21 | 'protobuf>=3.0.0', 22 | 'flask>=0.11.1', 23 | 'sqlalchemy>=1.1.0', 24 | 'aiopogo>=2.0.2,<2.1', 25 | 'polyline>=1.3.1', 26 | 'aiohttp>=2.1,<2.3', 27 | 'pogeo==0.3.*', 28 | 'cyrandom>=0.1.2' 29 | ], 30 | extras_require={ 31 | 'twitter': ['peony-twitter>=1.0.0'], 32 | 'pushbullet': ['asyncpushbullet>=0.12'], 33 | 'landmarks': ['shapely>=1.3.0'], 34 | 'boundaries': ['shapely>=1.3.0'], 35 | 'manual_captcha': ['selenium>=3.0'], 36 | 'performance': ['uvloop>=0.7.0', 'cchardet>=1.1.0', 'aiodns>=1.1.0', 'ujson>=1.35'], 37 | 'mysql': ['mysqlclient>=1.3'], 38 | 'postgres': ['psycopg2>=2.6'], 39 | 'images': ['pycairo>=1.10.0'], 40 | 'socks': ['aiosocks>=0.2.3'], 41 | 'sanic': ['sanic>=0.4', 'asyncpg>=0.8', 'ujson>=1.35'], 42 | 'google': ['gpsoauth>=0.4.0'], 43 | } 44 | ) 45 | -------------------------------------------------------------------------------- /solve_captchas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asyncio import get_event_loop, sleep 4 | from multiprocessing.managers import BaseManager 5 | from time import time 6 | 7 | from aiopogo import PGoApi, close_sessions, activate_hash_server, exceptions as ex 8 | from aiopogo.auth_ptc import AuthPtc 9 | from selenium import webdriver 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | 14 | from monocle import altitudes, sanitized as conf 15 | from monocle.utils import get_device_info, get_address, randomize_point 16 | from monocle.bounds import center 17 | 18 | 19 | async def solve_captcha(url, api, driver, timestamp): 20 | driver.get(url) 21 | WebDriverWait(driver, 86400).until(EC.text_to_be_present_in_element_value((By.NAME, "g-recaptcha-response"), "")) 22 | driver.switch_to.frame(driver.find_element_by_xpath("//*/iframe[@title='recaptcha challenge']")) 23 | token = driver.find_element_by_id("recaptcha-token").get_attribute("value") 24 | request = api.create_request() 25 | request.verify_challenge(token=token) 26 | request.get_hatched_eggs() 27 | request.get_inventory(last_timestamp_ms=timestamp) 28 | request.check_awarded_badges() 29 | request.get_buddy_walked() 30 | request.check_challenge() 31 | 32 | for attempt in range(-1, conf.MAX_RETRIES): 33 | try: 34 | responses = await request.call() 35 | return responses['VERIFY_CHALLENGE'].success 36 | except (ex.HashServerException, ex.MalformedResponseException, ex.ServerBusyOrOfflineException) as e: 37 | if attempt == conf.MAX_RETRIES - 1: 38 | raise 39 | else: 40 | print('{}, trying again soon.'.format(e)) 41 | await sleep(4) 42 | except (KeyError, TypeError): 43 | return False 44 | 45 | 46 | async def main(): 47 | try: 48 | class AccountManager(BaseManager): pass 49 | AccountManager.register('captcha_queue') 50 | AccountManager.register('extra_queue') 51 | manager = AccountManager(address=get_address(), authkey=conf.AUTHKEY) 52 | manager.connect() 53 | captcha_queue = manager.captcha_queue() 54 | extra_queue = manager.extra_queue() 55 | 56 | activate_hash_server(conf.HASH_KEY) 57 | 58 | driver = webdriver.Chrome() 59 | driver.set_window_size(803, 807) 60 | 61 | while not captcha_queue.empty(): 62 | account = captcha_queue.get() 63 | username = account.get('username') 64 | location = account.get('location') 65 | if location and location != (0,0,0): 66 | lat = location[0] 67 | lon = location[1] 68 | else: 69 | lat, lon = randomize_point(center, 0.0001) 70 | 71 | try: 72 | alt = altitudes.get((lat, lon)) 73 | except KeyError: 74 | alt = await altitudes.fetch((lat, lon)) 75 | 76 | try: 77 | device_info = get_device_info(account) 78 | api = PGoApi(device_info=device_info) 79 | api.set_position(lat, lon, alt) 80 | 81 | authenticated = False 82 | try: 83 | if account['provider'] == 'ptc': 84 | api.auth_provider = AuthPtc() 85 | api.auth_provider._access_token = account['auth'] 86 | api.auth_provider._access_token_expiry = account['expiry'] 87 | if api.auth_provider.check_access_token(): 88 | api.auth_provider.authenticated = True 89 | authenticated = True 90 | except KeyError: 91 | pass 92 | 93 | if not authenticated: 94 | await api.set_authentication(username=username, 95 | password=account['password'], 96 | provider=account.get('provider', 'ptc')) 97 | 98 | request = api.create_request() 99 | await request.call() 100 | 101 | await sleep(.6) 102 | 103 | request.download_remote_config_version(platform=1, app_version=6900) 104 | request.check_challenge() 105 | request.get_hatched_eggs() 106 | request.get_inventory(last_timestamp_ms=account.get('inventory_timestamp', 0)) 107 | request.check_awarded_badges() 108 | request.download_settings() 109 | responses = await request.call() 110 | account['time'] = time() 111 | 112 | challenge_url = responses['CHECK_CHALLENGE'].challenge_url 113 | timestamp = responses['GET_INVENTORY'].inventory_delta.new_timestamp_ms 114 | account['location'] = lat, lon 115 | account['inventory_timestamp'] = timestamp 116 | if challenge_url == ' ': 117 | account['captcha'] = False 118 | print('No CAPTCHA was pending on {}.'.format(username)) 119 | extra_queue.put(account) 120 | else: 121 | if await solve_captcha(challenge_url, api, driver, timestamp): 122 | account['time'] = time() 123 | account['captcha'] = False 124 | print('Solved CAPTCHA for {}, putting back in rotation.'.format(username)) 125 | extra_queue.put(account) 126 | else: 127 | account['time'] = time() 128 | print('Failed to solve for {}'.format(username)) 129 | captcha_queue.put(account) 130 | except KeyboardInterrupt: 131 | captcha_queue.put(account) 132 | break 133 | except KeyError: 134 | print('Unexpected or empty response for {}, putting back on queue.'.format(username)) 135 | captcha_queue.put(account) 136 | try: 137 | print(response) 138 | except Exception: 139 | pass 140 | await sleep(3) 141 | except (ex.AuthException, ex.AuthTokenExpiredException) as e: 142 | print('Authentication error on {}: {}'.format(username, e)) 143 | captcha_queue.put(account) 144 | await sleep(3) 145 | except ex.AiopogoError as e: 146 | print('aiopogo error on {}: {}'.format(username, e)) 147 | captcha_queue.put(account) 148 | await sleep(3) 149 | except Exception: 150 | captcha_queue.put(account) 151 | raise 152 | finally: 153 | try: 154 | driver.close() 155 | close_sessions() 156 | except Exception: 157 | pass 158 | 159 | if __name__ == '__main__': 160 | loop = get_event_loop() 161 | loop.run_until_complete(main()) 162 | -------------------------------------------------------------------------------- /sql/0.7.0.sql: -------------------------------------------------------------------------------- 1 | # for use with PostgreSQL and MySQL (untested) 2 | # for SQLite, recreate database and import existing data 3 | 4 | ALTER TABLE sightings 5 | ALTER COLUMN encounter_id TYPE numeric(20,0) USING encounter_id::numeric(20,0); 6 | 7 | ALTER TABLE longspawns 8 | ALTER COLUMN encounter_id TYPE numeric(20,0) USING encounter_id::numeric(20,0); 9 | 10 | ALTER TABLE longspawns 11 | DROP COLUMN normalized_timestamp; 12 | 13 | ALTER TABLE fort_sightings 14 | ALTER COLUMN last_modified TYPE integer; 15 | 16 | -------------------------------------------------------------------------------- /sql/add_spawnpoint_failures.sql: -------------------------------------------------------------------------------- 1 | # you can use tinyint instead of smallint on MySQL 2 | ALTER TABLE `spawnpoints` ADD `failures` smallint; 3 | -------------------------------------------------------------------------------- /sql/mysql-doubles.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `sightings` CHANGE COLUMN `lat` `lat` double(17,14); 2 | ALTER TABLE `sightings` CHANGE COLUMN `lon` `lon` double(17,14); 3 | ALTER TABLE `mystery_sightings` CHANGE COLUMN `lat` `lat` double(17,14); 4 | ALTER TABLE `mystery_sightings` CHANGE COLUMN `lon` `lon` double(17,14); 5 | ALTER TABLE `spawnpoints` CHANGE COLUMN `lat` `lat` double(17,14); 6 | ALTER TABLE `spawnpoints` CHANGE COLUMN `lon` `lon` double(17,14); 7 | ALTER TABLE `forts` CHANGE COLUMN `lat` `lat` double(17,14); 8 | ALTER TABLE `forts` CHANGE COLUMN `lon` `lon` double(17,14); 9 | ALTER TABLE `pokestops` CHANGE COLUMN `lat` `lat` double(17,14); 10 | ALTER TABLE `pokestops` CHANGE COLUMN `lon` `lon` double(17,14); 11 | -------------------------------------------------------------------------------- /sql/remove_altitude.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE spawnpoints 2 | DROP COLUMN alt; 3 | -------------------------------------------------------------------------------- /sql/remove_normalized_timestamp.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE sightings 2 | DROP COLUMN normalized_timestamp; 3 | -------------------------------------------------------------------------------- /sql/spawn_id_integer.sql: -------------------------------------------------------------------------------- 1 | # convert existing spawn_id hex strings to integers 2 | # set SPAWN_ID_INT to True after running 3 | # only works on PostgreSQL 4 | 5 | # create function for hex conversion 6 | CREATE OR REPLACE FUNCTION hex_to_bigint(hexval varchar) RETURNS bigint AS $$ 7 | DECLARE 8 | result bigint; 9 | BEGIN 10 | EXECUTE 'SELECT x''' || hexval || '''::bigint' INTO result; 11 | RETURN result; 12 | END; 13 | $$ LANGUAGE plpgsql IMMUTABLE STRICT; 14 | 15 | # increase size of columns to temporarily fit expanded values 16 | # not necessary if your column is already text or >16 characters 17 | ALTER TABLE sightings 18 | ALTER COLUMN spawn_id TYPE character varying (17); 19 | 20 | ALTER TABLE longspawns 21 | ALTER COLUMN spawn_id TYPE character varying (17); 22 | 23 | # convert hex strings to bigints, may take a while on large databases 24 | UPDATE sightings SET spawn_id = hex_to_bigint(spawn_id); 25 | UPDATE longspawns SET spawn_id = hex_to_bigint(spawn_id); 26 | 27 | # convert column data types to bigints 28 | ALTER TABLE sightings 29 | ALTER COLUMN spawn_id TYPE bigint USING spawn_id::bigint; 30 | ALTER TABLE longspawns 31 | ALTER COLUMN spawn_id TYPE bigint USING spawn_id::bigint; 32 | -------------------------------------------------------------------------------- /sql/v0.5.0.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `sightings` ADD `encounter_id` VARCHAR(32); 2 | -------------------------------------------------------------------------------- /sql/v0.5.1.sql: -------------------------------------------------------------------------------- 1 | # MySQL only; SQLite doesn't support CHANGE COLUMN. You need to export data, 2 | # remove database, recreate database and import data. Be careful while doing 3 | # it, or you may lose what you gathered. 4 | ALTER TABLE `sightings` CHANGE COLUMN `lat` `lat` VARCHAR(20); 5 | ALTER TABLE `sightings` CHANGE COLUMN `lon` `lon` VARCHAR(20); 6 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | from pkg_resources import resource_filename 5 | 6 | try: 7 | from ujson import dumps 8 | from flask import json as flask_json 9 | flask_json.dumps = lambda obj, **kwargs: dumps(obj, double_precision=6) 10 | except ImportError: 11 | from json import dumps 12 | 13 | from flask import Flask, jsonify, Markup, render_template, request 14 | 15 | from monocle import db, sanitized as conf 16 | from monocle.names import POKEMON 17 | from monocle.web_utils import * 18 | from monocle.bounds import area, center 19 | 20 | 21 | app = Flask(__name__, template_folder=resource_filename('monocle', 'templates'), static_folder=resource_filename('monocle', 'static')) 22 | 23 | 24 | def social_links(): 25 | social_links = '' 26 | 27 | if conf.FB_PAGE_ID: 28 | social_links = '' 29 | if conf.TWITTER_SCREEN_NAME: 30 | social_links += '' 31 | if conf.DISCORD_INVITE_ID: 32 | social_links += '' 33 | if conf.TELEGRAM_USERNAME: 34 | social_links += '' 35 | 36 | return Markup(social_links) 37 | 38 | 39 | def render_map(): 40 | css_js = '' 41 | 42 | if conf.LOAD_CUSTOM_CSS_FILE: 43 | css_js = '' 44 | if conf.LOAD_CUSTOM_JS_FILE: 45 | css_js += '' 46 | 47 | js_vars = Markup( 48 | "_defaultSettings['FIXED_OPACITY'] = '{:d}'; " 49 | "_defaultSettings['SHOW_TIMER'] = '{:d}'; " 50 | "_defaultSettings['TRASH_IDS'] = [{}]; ".format(conf.FIXED_OPACITY, conf.SHOW_TIMER, ', '.join(str(p_id) for p_id in conf.TRASH_IDS))) 51 | 52 | template = app.jinja_env.get_template('custom.html' if conf.LOAD_CUSTOM_HTML_FILE else 'newmap.html') 53 | return template.render( 54 | area_name=conf.AREA_NAME, 55 | map_center=center, 56 | map_provider_url=conf.MAP_PROVIDER_URL, 57 | map_provider_attribution=conf.MAP_PROVIDER_ATTRIBUTION, 58 | social_links=social_links(), 59 | init_js_vars=js_vars, 60 | extra_css_js=Markup(css_js) 61 | ) 62 | 63 | 64 | def render_worker_map(): 65 | template = app.jinja_env.get_template('workersmap.html') 66 | return template.render( 67 | area_name=conf.AREA_NAME, 68 | map_center=center, 69 | map_provider_url=conf.MAP_PROVIDER_URL, 70 | map_provider_attribution=conf.MAP_PROVIDER_ATTRIBUTION, 71 | social_links=social_links() 72 | ) 73 | 74 | 75 | @app.route('/') 76 | def fullmap(map_html=render_map()): 77 | return map_html 78 | 79 | 80 | @app.route('/data') 81 | def pokemon_data(): 82 | last_id = request.args.get('last_id', 0) 83 | return jsonify(get_pokemarkers(last_id)) 84 | 85 | 86 | @app.route('/gym_data') 87 | def gym_data(): 88 | return jsonify(get_gym_markers()) 89 | 90 | 91 | @app.route('/spawnpoints') 92 | def spawn_points(): 93 | return jsonify(get_spawnpoint_markers()) 94 | 95 | 96 | @app.route('/pokestops') 97 | def get_pokestops(): 98 | return jsonify(get_pokestop_markers()) 99 | 100 | 101 | @app.route('/scan_coords') 102 | def scan_coords(): 103 | return jsonify(get_scan_coords()) 104 | 105 | 106 | if conf.MAP_WORKERS: 107 | workers = Workers() 108 | 109 | @app.route('/workers_data') 110 | def workers_data(): 111 | return jsonify(get_worker_markers(workers)) 112 | 113 | 114 | @app.route('/workers') 115 | def workers_map(map_html=render_worker_map()): 116 | return map_html 117 | 118 | 119 | 120 | @app.route('/report') 121 | def report_main(area_name=conf.AREA_NAME, 122 | names=POKEMON, 123 | key=conf.GOOGLE_MAPS_KEY if conf.REPORT_MAPS else None): 124 | with db.session_scope() as session: 125 | counts = db.get_sightings_per_pokemon(session) 126 | 127 | count = sum(counts.values()) 128 | counts_tuple = tuple(counts.items()) 129 | nonexistent = [(x, names[x]) for x in range(1, 252) if x not in counts] 130 | del counts 131 | 132 | top_pokemon = list(counts_tuple[-30:]) 133 | top_pokemon.reverse() 134 | bottom_pokemon = counts_tuple[:30] 135 | rare_pokemon = [r for r in counts_tuple if r[0] in conf.RARE_IDS] 136 | if rare_pokemon: 137 | rare_sightings = db.get_all_sightings( 138 | session, [r[0] for r in rare_pokemon] 139 | ) 140 | else: 141 | rare_sightings = [] 142 | js_data = { 143 | 'charts_data': { 144 | 'punchcard': db.get_punch_card(session), 145 | 'top30': [(names[r[0]], r[1]) for r in top_pokemon], 146 | 'bottom30': [ 147 | (names[r[0]], r[1]) for r in bottom_pokemon 148 | ], 149 | 'rare': [ 150 | (names[r[0]], r[1]) for r in rare_pokemon 151 | ], 152 | }, 153 | 'maps_data': { 154 | 'rare': [sighting_to_report_marker(s) for s in rare_sightings], 155 | }, 156 | 'map_center': center, 157 | 'zoom': 13, 158 | } 159 | icons = { 160 | 'top30': [(r[0], names[r[0]]) for r in top_pokemon], 161 | 'bottom30': [(r[0], names[r[0]]) for r in bottom_pokemon], 162 | 'rare': [(r[0], names[r[0]]) for r in rare_pokemon], 163 | 'nonexistent': nonexistent 164 | } 165 | session_stats = db.get_session_stats(session) 166 | return render_template( 167 | 'report.html', 168 | current_date=datetime.now(), 169 | area_name=area_name, 170 | area_size=area, 171 | total_spawn_count=count, 172 | spawns_per_hour=count // session_stats['length_hours'], 173 | session_start=session_stats['start'], 174 | session_end=session_stats['end'], 175 | session_length_hours=session_stats['length_hours'], 176 | js_data=js_data, 177 | icons=icons, 178 | google_maps_key=key, 179 | ) 180 | 181 | 182 | @app.route('/report/') 183 | def report_single(pokemon_id, 184 | area_name=conf.AREA_NAME, 185 | key=conf.GOOGLE_MAPS_KEY if conf.REPORT_MAPS else None): 186 | with db.session_scope() as session: 187 | session_stats = db.get_session_stats(session) 188 | js_data = { 189 | 'charts_data': { 190 | 'hours': db.get_spawns_per_hour(session, pokemon_id), 191 | }, 192 | 'map_center': center, 193 | 'zoom': 13, 194 | } 195 | return render_template( 196 | 'report_single.html', 197 | current_date=datetime.now(), 198 | area_name=area_name, 199 | area_size=area, 200 | pokemon_id=pokemon_id, 201 | pokemon_name=POKEMON[pokemon_id], 202 | total_spawn_count=db.get_total_spawns_count(session, pokemon_id), 203 | session_start=session_stats['start'], 204 | session_end=session_stats['end'], 205 | session_length_hours=int(session_stats['length_hours']), 206 | google_maps_key=key, 207 | js_data=js_data, 208 | ) 209 | 210 | 211 | @app.route('/report/heatmap') 212 | def report_heatmap(): 213 | pokemon_id = request.args.get('id') 214 | with db.session_scope() as session: 215 | return dumps(db.get_all_spawn_coords(session, pokemon_id=pokemon_id)) 216 | 217 | 218 | def main(): 219 | args = get_args() 220 | app.run(debug=args.debug, threaded=True, host=args.host, port=args.port) 221 | 222 | 223 | if __name__ == '__main__': 224 | main() 225 | -------------------------------------------------------------------------------- /web_sanic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pkg_resources import resource_filename 4 | from time import time 5 | 6 | from sanic import Sanic 7 | from sanic.response import html, json 8 | from jinja2 import Environment, PackageLoader, Markup 9 | from asyncpg import create_pool 10 | 11 | from monocle import sanitized as conf 12 | from monocle.bounds import center 13 | from monocle.names import DAMAGE, MOVES, POKEMON 14 | from monocle.web_utils import get_scan_coords, get_worker_markers, Workers, get_args 15 | 16 | 17 | env = Environment(loader=PackageLoader('monocle', 'templates')) 18 | app = Sanic(__name__) 19 | app.static('/static', resource_filename('monocle', 'static')) 20 | 21 | 22 | def social_links(): 23 | social_links = '' 24 | 25 | if conf.FB_PAGE_ID: 26 | social_links = '' 27 | if conf.TWITTER_SCREEN_NAME: 28 | social_links += '' 29 | if conf.DISCORD_INVITE_ID: 30 | social_links += '' 31 | if conf.TELEGRAM_USERNAME: 32 | social_links += '' 33 | 34 | return Markup(social_links) 35 | 36 | 37 | def render_map(): 38 | css_js = '' 39 | 40 | if conf.LOAD_CUSTOM_CSS_FILE: 41 | css_js = '' 42 | if conf.LOAD_CUSTOM_JS_FILE: 43 | css_js += '' 44 | 45 | js_vars = Markup( 46 | "_defaultSettings['FIXED_OPACITY'] = '{:d}'; " 47 | "_defaultSettings['SHOW_TIMER'] = '{:d}'; " 48 | "_defaultSettings['TRASH_IDS'] = [{}]; ".format(conf.FIXED_OPACITY, conf.SHOW_TIMER, ', '.join(str(p_id) for p_id in conf.TRASH_IDS))) 49 | 50 | template = env.get_template('custom.html' if conf.LOAD_CUSTOM_HTML_FILE else 'newmap.html') 51 | return html(template.render( 52 | area_name=conf.AREA_NAME, 53 | map_center=center, 54 | map_provider_url=conf.MAP_PROVIDER_URL, 55 | map_provider_attribution=conf.MAP_PROVIDER_ATTRIBUTION, 56 | social_links=social_links(), 57 | init_js_vars=js_vars, 58 | extra_css_js=Markup(css_js) 59 | )) 60 | 61 | 62 | def render_worker_map(): 63 | template = env.get_template('workersmap.html') 64 | return html(template.render( 65 | area_name=conf.AREA_NAME, 66 | map_center=center, 67 | map_provider_url=conf.MAP_PROVIDER_URL, 68 | map_provider_attribution=conf.MAP_PROVIDER_ATTRIBUTION, 69 | social_links=social_links() 70 | )) 71 | 72 | 73 | @app.get('/') 74 | async def fullmap(request, html_map=render_map()): 75 | return html_map 76 | 77 | 78 | if conf.MAP_WORKERS: 79 | workers = Workers() 80 | 81 | 82 | @app.get('/workers_data') 83 | async def workers_data(request): 84 | return json(get_worker_markers(workers)) 85 | 86 | 87 | @app.get('/workers') 88 | async def workers_map(request, html_map=render_worker_map()): 89 | return html_map 90 | 91 | 92 | del env 93 | 94 | 95 | @app.get('/data') 96 | async def pokemon_data(request, _time=time): 97 | last_id = request.args.get('last_id', 0) 98 | async with app.pool.acquire() as conn: 99 | results = await conn.fetch(''' 100 | SELECT id, pokemon_id, expire_timestamp, lat, lon, atk_iv, def_iv, sta_iv, move_1, move_2 101 | FROM sightings 102 | WHERE expire_timestamp > {} AND id > {} 103 | '''.format(_time(), last_id)) 104 | return json(list(map(sighting_to_marker, results))) 105 | 106 | 107 | @app.get('/gym_data') 108 | async def gym_data(request, names=POKEMON, _str=str): 109 | async with app.pool.acquire() as conn: 110 | results = await conn.fetch(''' 111 | SELECT 112 | fs.fort_id, 113 | fs.id, 114 | fs.team, 115 | fs.prestige, 116 | fs.guard_pokemon_id, 117 | fs.last_modified, 118 | f.lat, 119 | f.lon 120 | FROM fort_sightings fs 121 | JOIN forts f ON f.id=fs.fort_id 122 | WHERE (fs.fort_id, fs.last_modified) IN ( 123 | SELECT fort_id, MAX(last_modified) 124 | FROM fort_sightings 125 | GROUP BY fort_id 126 | ) 127 | ''') 128 | return json([{ 129 | 'id': 'fort-' + _str(fort['fort_id']), 130 | 'sighting_id': fort['id'], 131 | 'prestige': fort['prestige'], 132 | 'pokemon_id': fort['guard_pokemon_id'], 133 | 'pokemon_name': names[fort['guard_pokemon_id']], 134 | 'team': fort['team'], 135 | 'lat': fort['lat'], 136 | 'lon': fort['lon'] 137 | } for fort in results]) 138 | 139 | 140 | @app.get('/spawnpoints') 141 | async def spawn_points(request, _dict=dict): 142 | async with app.pool.acquire() as conn: 143 | results = await conn.fetch('SELECT spawn_id, despawn_time, lat, lon, duration FROM spawnpoints') 144 | return json([_dict(x) for x in results]) 145 | 146 | 147 | @app.get('/pokestops') 148 | async def get_pokestops(request, _dict=dict): 149 | async with app.pool.acquire() as conn: 150 | results = await conn.fetch('SELECT external_id, lat, lon FROM pokestops') 151 | return json([_dict(x) for x in results]) 152 | 153 | 154 | @app.get('/scan_coords') 155 | async def scan_coords(request): 156 | return json(get_scan_coords()) 157 | 158 | 159 | def sighting_to_marker(pokemon, names=POKEMON, moves=MOVES, damage=DAMAGE, trash=conf.TRASH_IDS, _str=str): 160 | pokemon_id = pokemon['pokemon_id'] 161 | marker = { 162 | 'id': 'pokemon-' + _str(pokemon['id']), 163 | 'trash': pokemon_id in trash, 164 | 'name': names[pokemon_id], 165 | 'pokemon_id': pokemon_id, 166 | 'lat': pokemon['lat'], 167 | 'lon': pokemon['lon'], 168 | 'expires_at': pokemon['expire_timestamp'], 169 | } 170 | move1 = pokemon['move_1'] 171 | if move1: 172 | move2 = pokemon['move_2'] 173 | marker['atk'] = pokemon['atk_iv'] 174 | marker['def'] = pokemon['def_iv'] 175 | marker['sta'] = pokemon['sta_iv'] 176 | marker['move1'] = moves[move1] 177 | marker['move2'] = moves[move2] 178 | marker['damage1'] = damage[move1] 179 | marker['damage2'] = damage[move2] 180 | return marker 181 | 182 | 183 | @app.listener('before_server_start') 184 | async def register_db(app, loop): 185 | app.pool = await create_pool(**conf.DB, loop=loop) 186 | 187 | 188 | def main(): 189 | args = get_args() 190 | app.run(debug=args.debug, host=args.host, port=args.port) 191 | 192 | 193 | if __name__ == '__main__': 194 | main() 195 | 196 | --------------------------------------------------------------------------------