Compare commits

...

49 Commits

Author SHA1 Message Date
0936dee06c Bump version: 0.23.4.12 → 0.23.4.13 2021-01-20 22:25:16 +02:00
a1c6fb06a0 Task fetcher updates, state update repeater frequency increased for restricted IPs to not timeout cookie session 2021-01-20 22:25:07 +02:00
c57bf99976 If citizen is on restricted IP, clear cookies when session times out 2021-01-20 16:42:36 +02:00
995b45464f Bump version: 0.23.4.11 → 0.23.4.12 2021-01-20 16:12:51 +02:00
ebe28c948a requirement update 2021-01-20 16:12:40 +02:00
34d7230faf Response serialization update to include status code. Json encoder and decoder updates 2021-01-20 16:07:19 +02:00
3235991cce User agent updates 2021-01-20 15:15:31 +02:00
3fba1d6b3d Bump version: 0.23.4.10 → 0.23.4.11 2021-01-18 16:35:35 +02:00
88c8d5a9a0 Unified all event setting 2021-01-18 16:35:31 +02:00
382749a8d8 Bump version: 0.23.4.9 → 0.23.4.10 2021-01-18 16:12:31 +02:00
0c877e315b as_dict updates 2021-01-18 16:12:26 +02:00
c95f642fee Citizen.as_dict rewrite 2021-01-18 16:00:29 +02:00
da0276f9a6 Removed unused CitizenMilitary.boosters property
Explicitly updated all Citizen*.as_dict properties
2021-01-18 15:22:53 +02:00
f89f91e969 Concurrency checks placed directly in Citizen class
Added concurrency checks also for citizen updates

Sentry: EREPUBLIK-BOT-77
2021-01-18 15:11:40 +02:00
56c2ca1b6e House renewal price check bugfix 2021-01-12 08:05:57 +02:00
3cee2d1f0f Bump version: 0.23.4.8 → 0.23.4.9 2021-01-11 21:05:22 +02:00
0ed03877ce Damage booster's name change 2021-01-11 21:05:12 +02:00
491c9f5efe CitizenMilitary.countries were never used - all countries are available at constants.COUNTIRES, but allies and deployed allies are also available on every Battle 2021-01-11 16:27:23 +02:00
8445b556e7 Bump version: 0.23.4.7 → 0.23.4.8 2021-01-11 16:21:14 +02:00
d02c5c2969 dev requirement update 2021-01-11 16:20:58 +02:00
bff4183cb6 Added option to fight with energy bars, count small energy bars more precisely 2021-01-11 16:19:10 +02:00
47f5142837 Bump version: 0.23.4.6 → 0.23.4.7 2021-01-08 11:09:39 +02:00
a32fd039dd Report details about fighting also on telegram 2021-01-08 11:08:50 +02:00
311e684c0c Add telegram fight reporting API, Fix message duplication on TelegramReporter initialization 2021-01-08 11:07:59 +02:00
52038a86d5 PackBooster icon update 2021-01-07 17:00:59 +02:00
c35a107641 Bump version: 0.23.4.5 → 0.23.4.6 2021-01-07 15:55:02 +02:00
b53b2f0fae Update loop 2021-01-07 15:54:56 +02:00
8da9b6221a Bump version: 0.23.4.4 → 0.23.4.5 2021-01-07 15:36:33 +02:00
caa41c85f6 minor tweaks 2021-01-07 15:36:27 +02:00
da3b5d5768 Update inventory on booster activation 2021-01-07 15:31:46 +02:00
f96d0233f9 CSRF bugfix 2021-01-07 15:31:10 +02:00
c693a5182e Merge branch 'master' of github.com:eeriks/erepublik 2021-01-07 15:14:17 +02:00
0dfba6a9ff Find battle and fight - reuse already calculated hit count 2021-01-07 15:14:11 +02:00
ee3eca9658 Bump version: 0.23.4.3 → 0.23.4.4 2021-01-06 23:37:05 +02:00
0e8680daca bugfix 2021-01-06 23:35:25 +02:00
8e3606f1a3 Doc update 2021-01-05 19:47:43 +02:00
bd0bcc9ac7 Bump version: 0.23.4.2 → 0.23.4.3 2021-01-05 19:35:00 +02:00
c6f2226e64 . 2021-01-05 19:34:43 +02:00
308807d800 isort, pre-commit 2021-01-05 19:29:20 +02:00
91565d840e Added endpoint for collect all WC rewards 2021-01-05 19:18:20 +02:00
b4a9dd88f8 config generator bugdix 2021-01-05 19:17:50 +02:00
8435aa23ba Bump version: 0.23.4.1 → 0.23.4.2 2021-01-05 16:30:49 +02:00
09cd275a69 type bugfix 2021-01-05 16:30:42 +02:00
e23a67231e Bump version: 0.23.4 → 0.23.4.1 2021-01-05 16:19:43 +02:00
6a03d99abf type bugfix 2021-01-05 16:19:27 +02:00
2e344749a6 Bump version: 0.23.3.4 → 0.23.4 2021-01-05 15:50:55 +02:00
5aecefbd9d Protect those precious air boosters and 100% ground boosters 2021-01-05 15:50:38 +02:00
8b9ee5042d Bugfixes and inventory redone 2021-01-05 15:41:51 +02:00
1e93006c3d History updates 2020-12-17 18:18:46 +02:00
19 changed files with 667 additions and 220 deletions

1
.gitignore vendored
View File

@ -102,5 +102,4 @@ ENV/
debug/ debug/
log/ log/
docs/
*dump.json *dump.json

13
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,13 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
default_language_version:
python: python3.7

View File

@ -2,6 +2,25 @@
History History
======= =======
0.23.4 (2021-01-05)
-------------------
* Added expiration data to inventory items
* Inventory is now based on `classes.Inventory`
* Requirement update to make them more flexible regarding versions required
* Restructured inventory
0.23.3 (2020-12-17)
-------------------
* Fixed carpet bombing
* Fixed hits done amount when fighting on ground
* Minor requirement updates
* Minor tweaks to method signatures
* Fixed buy food if unable to work or train because not enough energy and not enough food
* Fixed applications for party presidency and congress if not a party member
* Removed tox
* Updates to github.io config generator
* Fixed `Citizen.concurrency_available` stuck in unset state if exception is being raised while doing concurrency task
0.23.2 (2020-12-01) 0.23.2 (2020-12-01)
------------------- -------------------
* Added concurrency checks to guard against simultaneous fighting/wam'ing/traveling * Added concurrency checks to guard against simultaneous fighting/wam'ing/traveling
@ -17,7 +36,7 @@ History
0.23.0 (2020-11-26) 0.23.0 (2020-11-26)
------------------- -------------------
* ***0.23 - last supported version for Python 3.7.*** * ***0.23 - last officially supported version for Python 3.7.***
* Added `Config.maverick` switch, to allow/deny automated fighting in non native divisions if the player has MaverickPack * Added `Config.maverick` switch, to allow/deny automated fighting in non native divisions if the player has MaverickPack
* Added `CitizenMedia.get_article(article_id:int)` method to get article data * Added `CitizenMedia.get_article(article_id:int)` method to get article data
* Added `CitizenMedia.delete_article(article_id:int)` method to delete article * Added `CitizenMedia.delete_article(article_id:int)` method to delete article

View File

@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@ -76,7 +76,6 @@ servedocs: docs ## compile the docs watching for changes
release: dist ## package and upload a release release: dist ## package and upload a release
twine upload dist/* twine upload dist/*
clean
dist: clean ## builds source and wheel package dist: clean ## builds source and wheel package
python setup.py sdist python setup.py sdist

View File

@ -34,4 +34,3 @@ This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypack
.. _Cookiecutter: https://github.com/audreyr/cookiecutter .. _Cookiecutter: https://github.com/audreyr/cookiecutter
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage

61
docs/erepublik.rst Normal file
View File

@ -0,0 +1,61 @@
erepublik package
=================
Submodules
----------
erepublik.access\_points module
-------------------------------
.. automodule:: erepublik.access_points
:members:
:undoc-members:
:show-inheritance:
erepublik.citizen module
------------------------
.. automodule:: erepublik.citizen
:members:
:undoc-members:
:show-inheritance:
erepublik.classes module
------------------------
.. automodule:: erepublik.classes
:members:
:undoc-members:
:show-inheritance:
erepublik.constants module
--------------------------
.. automodule:: erepublik.constants
:members:
:undoc-members:
:show-inheritance:
erepublik.types module
----------------------
.. automodule:: erepublik.types
:members:
:undoc-members:
:show-inheritance:
erepublik.utils module
----------------------
.. automodule:: erepublik.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: erepublik
:members:
:undoc-members:
:show-inheritance:

View File

@ -393,7 +393,7 @@
} }
config.air = air.checked; config.air = air.checked;
config.ground = ground.cehcked; config.ground = ground.checked;
config.boosters = boosters.checked; config.boosters = boosters.checked;
config.continuous_fighting = continuous_fighting.checked; config.continuous_fighting = continuous_fighting.checked;
config.next_energy = next_energy.checked; config.next_energy = next_energy.checked;
@ -402,10 +402,9 @@
config.travel_to_fight = travel_to_fight.checked; config.travel_to_fight = travel_to_fight.checked;
config.epic_hunt = epic_hunt.checked; config.epic_hunt = epic_hunt.checked;
config.epic_hunt_ebs = config.epic_hunt ? epic_hunt_ebs.checked : config.epic_hunt; config.epic_hunt_ebs = config.epic_hunt ? epic_hunt_ebs.checked : config.epic_hunt;
config.maverick = false;
// Advanced // Advanced
let telegram = document.getElementById('telegram'); // Generated let telegram = document.getElementById('telegram'); // Generated
config.telegram = telegram.checked; config.telegram = telegram.checked;
let telegram_chat_id = document.getElementById('telegram_chat_id'); // Generated let telegram_chat_id = document.getElementById('telegram_chat_id'); // Generated

7
docs/modules.rst Normal file
View File

@ -0,0 +1,7 @@
erepublik
=========
.. toctree::
:maxdepth: 4
erepublik

View File

@ -7,4 +7,3 @@ To use eRepublik script in a project::
from erepublik import Citizen from erepublik import Citizen
player = Citizen('email@domain.com', 'password') player = Citizen('email@domain.com', 'password')
player.update_all() player.update_all()

View File

@ -4,7 +4,7 @@
__author__ = """Eriks Karls""" __author__ = """Eriks Karls"""
__email__ = 'eriks@72.lv' __email__ = 'eriks@72.lv'
__version__ = '0.23.3.4' __version__ = '0.23.4.13'
from erepublik import classes, constants, utils from erepublik import classes, constants, utils
from erepublik.citizen import Citizen from erepublik.citizen import Citizen

View File

@ -13,43 +13,43 @@ __all__ = ['SlowRequests', 'CitizenAPI']
class SlowRequests(Session): class SlowRequests(Session):
last_time: datetime.datetime last_time: datetime.datetime
timeout: datetime.timedelta = datetime.timedelta(milliseconds=500) timeout: datetime.timedelta = datetime.timedelta(milliseconds=500)
uas: List[str] = [ _uas: List[str] = [
# Chrome # Chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36',
# FireFox # FireFox
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0', 'Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0', 'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0', 'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0', 'Mozilla/5.0 (X11; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0',
] ]
debug: bool = False debug: bool = False
def __init__(self, proxies: Dict[str, str] = None): def __init__(self, proxies: Dict[str, str] = None, user_agent: str = None):
super().__init__() super().__init__()
if proxies: if proxies:
self.proxies = proxies self.proxies = proxies
if user_agent is None:
user_agent = random.choice(self._uas)
self.request_log_name = utils.get_file(utils.now().strftime("debug/requests_%Y-%m-%d.log")) self.request_log_name = utils.get_file(utils.now().strftime("debug/requests_%Y-%m-%d.log"))
self.last_time = utils.now() self.last_time = utils.now()
self.headers.update({ self.headers.update({'User-Agent': user_agent})
'User-Agent': random.choice(self.uas)
})
@property @property
def as_dict(self): def as_dict(self):
return dict(last_time=self.last_time, timeout=self.timeout, user_agent=self.headers['User-Agent'], return dict(last_time=self.last_time, timeout=self.timeout, cookies=self.cookies.get_dict(), debug=self.debug,
request_log_name=self.request_log_name, debug=self.debug) user_agent=self.headers['User-Agent'], request_log_name=self.request_log_name, proxies=self.proxies)
def request(self, method, url, *args, **kwargs): def request(self, method, url, *args, **kwargs):
self._slow_down_requests() self._slow_down_requests()
@ -120,6 +120,10 @@ class CitizenBaseAPI:
self._req = SlowRequests() self._req = SlowRequests()
self.token = "" self.token = ""
@property
def as_dict(self):
return dict(url=self.url, request=self._req.as_dict, token=self.token)
def post(self, url: str, data=None, json=None, **kwargs) -> Response: def post(self, url: str, data=None, json=None, **kwargs) -> Response:
return self._req.post(url, data, json, **kwargs) return self._req.post(url, data, json, **kwargs)
@ -305,11 +309,11 @@ class ErepublikEconomyAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/buyGoldItems", data=data) return self.post(f"{self.url}/main/buyGoldItems", data=data)
def _post_economy_activate_booster(self, quality: int, duration: int, kind: str) -> Response: def _post_economy_activate_booster(self, quality: int, duration: int, kind: str) -> Response:
data = dict(type=kind, quality=quality, duration=duration, fromInventory=True) data = dict(type=kind, quality=quality, duration=duration, fromInventory=True, _token=self.token)
return self.post(f"{self.url}/economy/activateBooster", data=data) return self.post(f"{self.url}/economy/activateBooster", data=data)
def _post_economy_activate_house(self, quality: int) -> Response: def _post_economy_activate_house(self, quality: int) -> Response:
data = {"action": "activate", "quality": quality, "type": "house", "_token": self.token} data = dict(action="activate", quality=quality, type="house", _token=self.token)
return self.post(f"{self.url}/economy/activateHouse", data=data) return self.post(f"{self.url}/economy/activateHouse", data=data)
def _post_economy_donate_items_action(self, citizen_id: int, amount: int, industry: int, def _post_economy_donate_items_action(self, citizen_id: int, amount: int, industry: int,
@ -595,6 +599,10 @@ class ErepublikProfileAPI(CitizenBaseAPI):
data = dict(_token=self.token, rewardId=reward_id) data = dict(_token=self.token, rewardId=reward_id)
return self.post(f"{self.url}/main/weekly-challenge-collect-reward", data=data) return self.post(f"{self.url}/main/weekly-challenge-collect-reward", data=data)
def _post_main_weekly_challenge_collect_all(self, max_reward_id: int) -> Response:
data = dict(_token=self.token, maxRewardId=max_reward_id)
return self.post(f"{self.url}/main/weekly-challenge-collect-all", data=data)
def _post_main_profile_update(self, action: str, params: str): def _post_main_profile_update(self, action: str, params: str):
data = {"action": action, "params": params, "_token": self.token} data = {"action": action, "params": params, "_token": self.token}
return self.post(f"{self.url}/main/profile-update", data=data) return self.post(f"{self.url}/main/profile-update", data=data)

View File

@ -2,7 +2,7 @@ import re
import sys import sys
import warnings import warnings
import weakref import weakref
from datetime import datetime, timedelta from datetime import datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from itertools import product from itertools import product
from threading import Event from threading import Event
@ -11,7 +11,8 @@ from typing import Any, Dict, List, NoReturn, Optional, Set, Tuple, Union
from requests import HTTPError, RequestException, Response from requests import HTTPError, RequestException, Response
from . import access_points, classes, constants, utils from . import access_points, classes, constants, types, utils
from .access_points import SlowRequests
from .classes import OfferItem from .classes import OfferItem
@ -20,9 +21,7 @@ class BaseCitizen(access_points.CitizenAPI):
_last_inventory_update: datetime = constants.min_datetime _last_inventory_update: datetime = constants.min_datetime
promos: Dict[str, datetime] = None promos: Dict[str, datetime] = None
inventory: Dict[str, Dict[str, Dict[int, Dict[str, Union[str, int, float]]]]] _inventory: classes.Inventory
inventory_status: Dict[str, int]
boosters: Dict[int, Dict[int, int]] = {50: {}, 100: {}}
ot_points: int = 0 ot_points: int = 0
food: Dict[str, int] = {"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0, "total": 0} food: Dict[str, int] = {"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0, "total": 0}
@ -40,9 +39,9 @@ class BaseCitizen(access_points.CitizenAPI):
energy: classes.Energy = None energy: classes.Energy = None
details: classes.Details = None details: classes.Details = None
politics: classes.Politics = None politics: classes.Politics = None
my_companies: classes.MyCompanies = None
reporter: classes.Reporter = None reporter: classes.Reporter = None
stop_threads: Event = None stop_threads: Event = None
concurrency_available: Event = None
telegram: classes.TelegramReporter = None telegram: classes.TelegramReporter = None
r: Response = None r: Response = None
@ -59,14 +58,11 @@ class BaseCitizen(access_points.CitizenAPI):
self.my_companies = classes.MyCompanies(self) self.my_companies = classes.MyCompanies(self)
self.reporter = classes.Reporter(self) self.reporter = classes.Reporter(self)
self.stop_threads = Event() self.stop_threads = Event()
self.concurrency_available = Event()
self.concurrency_available.set()
self.telegram = classes.TelegramReporter(stop_event=self.stop_threads) self.telegram = classes.TelegramReporter(stop_event=self.stop_threads)
self.config.email = email self.config.email = email
self.config.password = password self.config.password = password
self.inventory = {} self._inventory = classes.Inventory()
self.inventory_status = dict(used=0, total=0)
self.wheel_of_fortune = False self.wheel_of_fortune = False
def get_csrf_token(self): def get_csrf_token(self):
@ -241,33 +237,42 @@ class BaseCitizen(access_points.CitizenAPI):
def update_inventory(self): def update_inventory(self):
""" """
Updates class properties and returns structured inventory. Updates citizen inventory
Return structure: {status: {used: int, total: int}, items: {active/final/raw: {item_token:{quality: data}}}
If item kind is damageBoosters or aircraftDamageBoosters then kind is renamed to kind+quality and duration is
used as quality.
""" """
self._update_inventory_data(self._get_economy_inventory_items().json()) self._update_inventory_data(self._get_economy_inventory_items().json())
def get_inventory(self, force: bool = False): @property
def inventory(self) -> classes.Inventory:
return self.get_inventory()
def get_inventory(self, force: bool = False) -> classes.Inventory:
if utils.good_timedelta(self._last_inventory_update, timedelta(minutes=2)) < self.now or force: if utils.good_timedelta(self._last_inventory_update, timedelta(minutes=2)) < self.now or force:
self.update_inventory() self.update_inventory()
return self.inventory return self._inventory
def _update_inventory_data(self, inv_data: Dict[str, Any]): def _update_inventory_data(self, inv_data: Dict[str, Any]):
if not isinstance(inv_data, dict): if not isinstance(inv_data, dict):
raise TypeError("Parameter `inv_data` must be dict not '{type(data)}'!") raise TypeError("Parameter `inv_data` must be dict not '{type(data)}'!")
def _expire_value_to_python(_expire_value: str) -> Dict[str, Union[int, datetime]]:
_data = re.search(
r'((?P<amount>\d+) item\(s\) )?[eE]xpires? on Day (?P<eday>\d,\d{3}), (?P<time>\d\d:\d\d)',
_expire_value).groupdict()
eday = utils.date_from_eday(int(_data['eday'].replace(',', '')))
dt = constants.erep_tz.localize(datetime.combine(eday, time(*[int(_) for _ in _data['time'].split(':')])))
return {'amount': _data.get('amount'), 'expiration': dt}
status = inv_data.get("inventoryStatus", {}) status = inv_data.get("inventoryStatus", {})
if status: if status:
self.inventory_status.clear() self._inventory.used = status.get("usedStorage")
self.inventory_status.update(used=status.get("usedStorage"), total=status.get("totalStorage")) self._inventory.total = status.get("totalStorage")
data = inv_data.get('inventoryItems', {}) data = inv_data.get('inventoryItems', {})
if not data: if not data:
return return
self._last_inventory_update = self.now self._last_inventory_update = self.now
self.food.update({"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0}) self.food.update({"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0})
self.eb_small = self.eb_double = self.eb_normal = 0 self.eb_small = self.eb_double = self.eb_normal = 0
active_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {} active_items: types.InvFinal = {}
if data.get("activeEnhancements", {}).get("items", {}): if data.get("activeEnhancements", {}).get("items", {}):
for item_data in data.get("activeEnhancements", {}).get("items", {}).values(): for item_data in data.get("activeEnhancements", {}).get("items", {}).values():
if item_data.get('token'): if item_data.get('token'):
@ -278,29 +283,35 @@ class BaseCitizen(access_points.CitizenAPI):
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]] kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in active_items: if kind not in active_items:
active_items[kind] = {} active_items[kind] = {}
icon = item_data['icon'] if item_data[ expiration_info = []
'icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png" if item_data.get('attributes').get('expirationInfo'):
item_data = dict(name=item_data.get("name"), time_left=item_data['active']['time_left'], icon=icon, expire_info = item_data.get('attributes').get('expirationInfo')
kind=kind, expiration_info = [_expire_value_to_python(v) for v in expire_info['value']]
quality=item_data.get("quality", 0)) if not item_data.get('icon') and item_data.get('isPackBooster'):
item_data['icon'] = f"//www.erepublik.com/images/icons/boosters/52px/{item_data.get('type')}.png"
icon = item_data['icon'] if item_data['icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png"
inv_item: types.InvFinalItem = dict(
name=item_data.get("name"), time_left=item_data['active']['time_left'], icon=icon,
kind=kind, expiration=expiration_info, quality=item_data.get("quality", 0)
)
if item_data.get('isPackBooster'): if item_data.get('isPackBooster'):
active_items[kind].update({0: item_data}) active_items[kind].update({0: inv_item})
else: else:
active_items[kind].update({item_data.get("quality"): item_data}) active_items[kind].update({inv_item.get("quality"): inv_item})
final_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {} final_items: types.InvFinal = {}
boosters: types.InvBooster = {}
if data.get("finalProducts", {}).get("items", {}): if data.get("finalProducts", {}).get("items", {}):
for item_data in data.get("finalProducts", {}).get("items", {}).values(): for item_data in data.get("finalProducts", {}).get("items", {}).values():
is_booster: bool = False
name = item_data['name'] name = item_data['name']
if item_data.get('type'): if item_data.get('type'):
if item_data.get('type') in ['damageBoosters', "aircraftDamageBoosters"]: # in ['damageBoosters', "aircraftDamageBoosters", 'prestigePointsBoosters']
kind = f"{item_data['type']}{item_data['quality']}" if item_data.get('isBooster'):
if item_data['quality'] == 5: is_booster = True
self.boosters[50].update({item_data['duration']: item_data['amount']}) kind = item_data['type']
elif item_data['quality'] == 10:
self.boosters[100].update({item_data['duration']: item_data['amount']})
delta = item_data['duration'] delta = item_data['duration']
if delta // 3600: if delta // 3600:
@ -329,6 +340,9 @@ class BaseCitizen(access_points.CitizenAPI):
elif q == 15: elif q == 15:
self.eb_small += amount self.eb_small += amount
item_data.update(token='energy_bar') item_data.update(token='energy_bar')
elif q == 16:
self.eb_small += amount
item_data.update(token='energy_bar')
kind = re.sub(r'_q\d\d*', "", item_data.get('token')) kind = re.sub(r'_q\d\d*', "", item_data.get('token'))
if item_data.get('token', "") == "house_q100": if item_data.get('token', "") == "house_q100":
@ -337,8 +351,14 @@ class BaseCitizen(access_points.CitizenAPI):
if constants.INDUSTRIES[kind]: if constants.INDUSTRIES[kind]:
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]] kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in final_items: if is_booster:
final_items[kind] = {} if kind not in boosters:
boosters[kind] = {}
if item_data.get('quality', 0) not in boosters[kind]:
boosters[kind][item_data['quality']] = {}
else:
if kind not in final_items:
final_items[kind] = {}
if item_data['icon']: if item_data['icon']:
icon = item_data['icon'] icon = item_data['icon']
@ -355,10 +375,23 @@ class BaseCitizen(access_points.CitizenAPI):
icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png" icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png"
else: else:
icon = "//www.erepublik.net/images/modules/manager/tab_storage.png" icon = "//www.erepublik.net/images/modules/manager/tab_storage.png"
_item_data = dict(kind=kind, quality=item_data.get('quality', 0), amount=item_data.get('amount', 0),
durability=item_data.get('duration', 0), icon=icon, name=name) expiration_info = []
if item_data.get('type') in ('damageBoosters', "aircraftDamageBoosters"): if item_data.get('attributes'):
_item_data = {_item_data['durability']: _item_data} if item_data.get('attributes').get('expirationInfo'):
expire_info = item_data.get('attributes').get('expirationInfo')
expiration_info = [_expire_value_to_python(v) for v in expire_info['value']]
elif item_data.get('attributes').get('expiration'):
_exp = item_data.get('attributes').get('expiration')
exp_dt = (utils.date_from_eday(int(_exp['value'].replace(',', ''))))
expiration_info = [{'amount': item_data.get('amount'), 'expiration': exp_dt}]
_inv_item: Dict[int, types.InvFinalItem]
inv_item: types.InvFinalItem = dict(
kind=kind, quality=item_data.get('quality', 0), icon=icon, expiration=expiration_info,
amount=item_data.get('amount'), durability=item_data.get('duration', 0), name=name
)
if is_booster:
_inv_item = {inv_item['durability']: inv_item}
else: else:
if item_data.get('type') == 'bomb': if item_data.get('type') == 'bomb':
firepower = 0 firepower = 0
@ -367,11 +400,14 @@ class BaseCitizen(access_points.CitizenAPI):
except AttributeError: except AttributeError:
pass pass
finally: finally:
_item_data.update(fire_power=firepower) inv_item.update(fire_power=firepower)
_item_data = {_item_data['quality']: _item_data} _inv_item = {inv_item['quality']: inv_item}
final_items[kind].update(_item_data) if is_booster:
boosters[kind][inv_item['quality']].update(_inv_item)
else:
final_items[kind].update(_inv_item)
raw_materials: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {} raw_materials: types.InvRaw = {}
if data.get("rawMaterials", {}).get("items", {}): if data.get("rawMaterials", {}).get("items", {}):
for item_data in data.get("rawMaterials", {}).get("items", {}).values(): for item_data in data.get("rawMaterials", {}).get("items", {}).values():
if item_data['isPartial']: if item_data['isPartial']:
@ -401,8 +437,11 @@ class BaseCitizen(access_points.CitizenAPI):
offers[kind] = {} offers[kind] = {}
offers[kind].update(offer_data) offers[kind].update(offer_data)
self.inventory.clear() self._inventory.active = active_items
self.inventory.update(active=active_items, final=final_items, raw=raw_materials, offers=offers) self._inventory.final = final_items
self._inventory.boosters = boosters
self._inventory.raw = raw_materials
self._inventory.offers = offers
self.food["total"] = sum([self.food[q] * constants.FOOD_ENERGY[q] for q in constants.FOOD_ENERGY]) self.food["total"] = sum([self.food[q] * constants.FOOD_ENERGY[q] for q in constants.FOOD_ENERGY])
def write_log(self, *args, **kwargs): def write_log(self, *args, **kwargs):
@ -430,7 +469,7 @@ class BaseCitizen(access_points.CitizenAPI):
self._req.debug = bool(debug) self._req.debug = bool(debug)
def to_json(self, indent: bool = False) -> str: def to_json(self, indent: bool = False) -> str:
return utils.json.dumps(self, cls=classes.MyJSONEncoder, indent=4 if indent else None) return utils.json.dumps(self, cls=classes.ErepublikJSONEncoder, indent=4 if indent else None, sort_keys=True)
def get_countries_with_regions(self) -> Set[constants.Country]: def get_countries_with_regions(self) -> Set[constants.Country]:
r_json = self._post_main_travel_data().json() r_json = self._post_main_travel_data().json()
@ -444,13 +483,13 @@ class BaseCitizen(access_points.CitizenAPI):
filename = f"{self.__class__.__name__}__dump.json" filename = f"{self.__class__.__name__}__dump.json"
with open(filename, 'w') as f: with open(filename, 'w') as f:
utils.json.dump(dict(config=self.config, cookies=self._req.cookies.get_dict(), utils.json.dump(dict(config=self.config, cookies=self._req.cookies.get_dict(),
user_agent=self._req.headers.get("User-Agent")), f, cls=classes.MyJSONEncoder) user_agent=self._req.headers.get("User-Agent")), f, cls=classes.ErepublikJSONEncoder)
self.write_log(f"Session saved to: '{filename}'") self.write_log(f"Session saved to: '{filename}'")
@classmethod @classmethod
def load_from_dump(cls, dump_name: str): def load_from_dump(cls, dump_name: str):
with open(dump_name) as f: with open(dump_name) as f:
data = utils.json.load(f) data = utils.json.load(f, object_hook=utils.json_decode_object_hook)
player = cls(data['config']['email'], "") player = cls(data['config']['email'], "")
player._req.cookies.update(data['cookies']) player._req.cookies.update(data['cookies'])
player._req.headers.update({"User-Agent": data['user_agent']}) player._req.headers.update({"User-Agent": data['user_agent']})
@ -470,7 +509,7 @@ class BaseCitizen(access_points.CitizenAPI):
self.write_log(f"Resumed as: {self.name}") self.write_log(f"Resumed as: {self.name}")
if re.search('<div id="accountSecurity" class="it-hurts-when-ip">', resp.text): if re.search('<div id="accountSecurity" class="it-hurts-when-ip">', resp.text):
self.restricted_ip = True self.restricted_ip = True
self.report_error("eRepublik has blacklisted IP. Limited functionality!", True) # self.report_error("eRepublik has blacklisted IP. Limited functionality!", True)
self.logged_in = True self.logged_in = True
self.get_csrf_token() self.get_csrf_token()
@ -485,18 +524,29 @@ class BaseCitizen(access_points.CitizenAPI):
@property @property
def as_dict(self): def as_dict(self):
ret = self.__dict__.copy() ret = super().as_dict
ret.pop('stop_threads', None) ret.update(
ret.pop('_CitizenMilitary__last_war_update_data', None) name=self.name, __str__=self.__str__(),
ret.update(_properties=dict( ebs=dict(normal=self.eb_normal, double=self.eb_double, small=self.eb_small),
now=self.now, should_do_levelup=self.should_do_levelup, is_levelup_reachable=self.is_levelup_reachable, promos=self.promos, inventory=self._inventory.as_dict, ot_points=self.ot_points, food=self.food,
max_time_till_full_ff=self.max_time_till_full_ff, is_levelup_close=self.is_levelup_close, division=self.division, maveric=self.maverick, eday=self.eday, wheel_of_fortune=self.wheel_of_fortune,
time_till_full_ff=self.time_till_full_ff, time_till_week_change=self.time_till_week_change, debug=self.debug,
next_wc_start=self.next_wc_start, next_reachable_energy=self.next_reachable_energy, logged_in=self.logged_in, restricted_ip=self.restricted_ip, _properties=dict(
health_info=self.health_info)) now=self.now, should_do_levelup=self.should_do_levelup, is_levelup_reachable=self.is_levelup_reachable,
max_time_till_full_ff=self.max_time_till_full_ff, is_levelup_close=self.is_levelup_close,
time_till_full_ff=self.time_till_full_ff, time_till_week_change=self.time_till_week_change,
next_wc_start=self.next_wc_start, next_reachable_energy=self.next_reachable_energy,
health_info=self.health_info),
_last_full_update=self._last_full_update, _last_inventory_update=self._last_inventory_update,
config=self.config.as_dict, energy=self.energy.as_dict, details=self.details.as_dict,
politics=self.politics.as_dict, my_companies=self.my_companies.as_dict, reporter=self.reporter.as_dict,
telegram=self.telegram.as_dict, stop_threads=self.stop_threads.is_set(), response=self.r,
)
return ret return ret
def set_locks(self):
self.stop_threads.set()
@property @property
def health_info(self): def health_info(self):
ret = f"{self.energy.recovered}/{self.energy.limit} + {self.energy.recoverable}, " \ ret = f"{self.energy.recovered}/{self.energy.limit} + {self.energy.recoverable}, " \
@ -653,6 +703,10 @@ class BaseCitizen(access_points.CitizenAPI):
self.eb_double -= amount self.eb_double -= amount
elif q == "12": elif q == "12":
self.eb_small -= amount self.eb_small -= amount
elif q == "15":
self.eb_small -= amount
elif q == "16":
self.eb_small -= amount
next_recovery = r_json.get("food_remaining_reset").split(":") next_recovery = r_json.get("food_remaining_reset").split(":")
self.energy.set_reference_time( self.energy.set_reference_time(
utils.good_timedelta(self.now, timedelta(seconds=int(next_recovery[1]) * 60 + int(next_recovery[2]))) utils.good_timedelta(self.now, timedelta(seconds=int(next_recovery[1]) * 60 + int(next_recovery[2])))
@ -679,7 +733,7 @@ class BaseCitizen(access_points.CitizenAPI):
self.get_csrf_token() self.get_csrf_token()
if re.search('<div id="accountSecurity" class="it-hurts-when-ip">', self.r.text): if re.search('<div id="accountSecurity" class="it-hurts-when-ip">', self.r.text):
self.restricted_ip = True self.restricted_ip = True
self.report_error("eRepublik has blacklisted IP. Limited functionality!", True) # self.report_error("eRepublik has blacklisted IP. Limited functionality!", True)
self.logged_in = True self.logged_in = True
@ -694,6 +748,9 @@ class BaseCitizen(access_points.CitizenAPI):
if response.status_code >= 400: if response.status_code >= 400:
self.r = response self.r = response
if response.status_code >= 500: if response.status_code >= 500:
if self.restricted_ip:
self._req.cookies.clear()
return True
self.write_log("eRepublik servers are having internal troubles. Sleeping for 5 minutes") self.write_log("eRepublik servers are having internal troubles. Sleeping for 5 minutes")
self.sleep(5 * 60) self.sleep(5 * 60)
else: else:
@ -725,7 +782,7 @@ class BaseCitizen(access_points.CitizenAPI):
:param msg: Message about the action :param msg: Message about the action
:param kwargs: Extra information regarding action :param kwargs: Extra information regarding action
""" """
kwargs = utils.json.loads(utils.json.dumps(kwargs or {}, cls=classes.MyJSONEncoder)) kwargs = utils.json.loads(utils.json.dumps(kwargs or {}, cls=classes.ErepublikJSONEncoder))
action = action[:32] action = action[:32]
self.write_log(msg) self.write_log(msg)
if self.reporter.allowed: if self.reporter.allowed:
@ -936,7 +993,6 @@ class CitizenCompanies(BaseCitizen):
def work_as_manager_in_holding(self, holding: classes.Holding) -> Optional[Dict[str, Any]]: def work_as_manager_in_holding(self, holding: classes.Holding) -> Optional[Dict[str, Any]]:
return self._work_as_manager(holding) return self._work_as_manager(holding)
@utils.wait_for_lock
def _work_as_manager(self, wam_holding: classes.Holding) -> Optional[Dict[str, Any]]: def _work_as_manager(self, wam_holding: classes.Holding) -> Optional[Dict[str, Any]]:
if self.restricted_ip: if self.restricted_ip:
return None return None
@ -946,13 +1002,13 @@ class CitizenCompanies(BaseCitizen):
raw_factories = wam_holding.get_wam_companies(raw_factory=True) raw_factories = wam_holding.get_wam_companies(raw_factory=True)
fin_factories = wam_holding.get_wam_companies(raw_factory=False) fin_factories = wam_holding.get_wam_companies(raw_factory=False)
free_inventory = self.inventory_status["total"] - self.inventory_status["used"] free_inventory = self.inventory.total - self.inventory.used
wam_list = raw_factories + fin_factories wam_list = raw_factories + fin_factories
wam_list = wam_list[:self.energy.food_fights] wam_list = wam_list[:self.energy.food_fights]
if int(free_inventory * 0.75) < self.my_companies.get_needed_inventory_usage(wam_list): if int(free_inventory * 0.75) < self.my_companies.get_needed_inventory_usage(wam_list):
self.update_inventory() self.update_inventory()
free_inventory = self.inventory_status["total"] - self.inventory_status["used"] free_inventory = self.inventory.total - self.inventory.used
while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list): while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list):
wam_list.pop(-1) wam_list.pop(-1)
@ -1006,7 +1062,7 @@ class CitizenCompanies(BaseCitizen):
class CitizenEconomy(CitizenTravel): class CitizenEconomy(CitizenTravel):
def update_money(self, page: int = 0, currency: int = 62) -> Dict[str, Any]: def update_money(self, page: int = 0, currency: int = 62):
""" """
Gets monetary market offers to get exact amount of CC and Gold available Gets monetary market offers to get exact amount of CC and Gold available
""" """
@ -1016,12 +1072,11 @@ class CitizenEconomy(CitizenTravel):
resp_data = resp.json() resp_data = resp.json()
self.details.cc = float(resp_data.get("ecash").get("value")) self.details.cc = float(resp_data.get("ecash").get("value"))
self.details.gold = float(resp_data.get("gold").get("value")) self.details.gold = float(resp_data.get("gold").get("value"))
return resp_data
def check_house_durability(self) -> Dict[int, datetime]: def check_house_durability(self) -> Dict[int, datetime]:
ret = {} ret = {}
inv = self.get_inventory() inv = self.inventory
for house_quality, active_house in inv['active'].get('House', {}).items(): for house_quality, active_house in inv.active.get('House', {}).items():
till = utils.good_timedelta(self.now, timedelta(seconds=active_house['time_left'])) till = utils.good_timedelta(self.now, timedelta(seconds=active_house['time_left']))
ret.update({house_quality: till}) ret.update({house_quality: till})
return ret return ret
@ -1029,8 +1084,8 @@ class CitizenEconomy(CitizenTravel):
def buy_and_activate_house(self, q: int) -> Optional[Dict[int, datetime]]: def buy_and_activate_house(self, q: int) -> Optional[Dict[int, datetime]]:
original_region = self.details.current_country, self.details.current_region original_region = self.details.current_country, self.details.current_region
ok_to_activate = False ok_to_activate = False
inv = self.get_inventory() inv = self.inventory
if not inv['final'].get('House', {}).get(q, {}): if not inv.final.get('House', {}).get(q, {}):
countries = [self.details.citizenship, ] countries = [self.details.citizenship, ]
if self.details.current_country != self.details.citizenship: if self.details.current_country != self.details.citizenship:
countries.append(self.details.current_country) countries.append(self.details.current_country)
@ -1038,7 +1093,7 @@ class CitizenEconomy(CitizenTravel):
local_cheapest = sorted(offers, key=lambda o: o.price)[0] local_cheapest = sorted(offers, key=lambda o: o.price)[0]
global_cheapest = self.get_market_offers("House", q)[f"q{q}"] global_cheapest = self.get_market_offers("House", q)[f"q{q}"]
if global_cheapest.price + 200 < local_cheapest.price: if global_cheapest.price + 2000 < local_cheapest.price:
if global_cheapest.price + 2000 < self.details.cc: if global_cheapest.price + 2000 < self.details.cc:
if self.travel_to_country(global_cheapest.country): if self.travel_to_country(global_cheapest.country):
buy = self.buy_market_offer(global_cheapest, 1) buy = self.buy_market_offer(global_cheapest, 1)
@ -1084,7 +1139,7 @@ class CitizenEconomy(CitizenTravel):
r: Dict[str, Any] = self._post_economy_activate_house(quality).json() r: Dict[str, Any] = self._post_economy_activate_house(quality).json()
self._update_inventory_data(r) self._update_inventory_data(r)
if r.get("status") and not r.get("error"): if r.get("status") and not r.get("error"):
house: Dict[str, Union[int, str]] = self.get_inventory()['active']['House'][quality] house = self.inventory.active.get('House', {}).get(quality)
time_left = timedelta(seconds=house["time_left"]) time_left = timedelta(seconds=house["time_left"])
active_until = utils.good_timedelta(self.now, time_left) active_until = utils.good_timedelta(self.now, time_left)
self._report_action( self._report_action(
@ -1164,12 +1219,13 @@ class CitizenEconomy(CitizenTravel):
self.write_log(f"Trying to sell unsupported industry {industry}") self.write_log(f"Trying to sell unsupported industry {industry}")
_inv_qlt = quality if industry in [1, 2, 3, 4, 23] else 0 _inv_qlt = quality if industry in [1, 2, 3, 4, 23] else 0
_kind = 'final' if industry in [1, 2, 4, 23] else 'raw' final_kind = industry in [1, 2, 4, 23]
inventory = self.get_inventory() items = (self.inventory.final if final_kind else self.inventory.raw).get(constants.INDUSTRIES[industry],
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}}) {_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount: if items[_inv_qlt]['amount'] < amount:
inventory = self.get_inventory(True) self.update_inventory()
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}}) items = (self.inventory.final if final_kind else self.inventory.raw).get(constants.INDUSTRIES[industry],
{_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount: if items[_inv_qlt]['amount'] < amount:
self._report_action("ECONOMY_SELL_PRODUCTS", "Unable to sell! Not enough items in storage!", self._report_action("ECONOMY_SELL_PRODUCTS", "Unable to sell! Not enough items in storage!",
kwargs=dict(inventory=items[_inv_qlt], amount=amount)) kwargs=dict(inventory=items[_inv_qlt], amount=amount))
@ -1199,7 +1255,6 @@ class CitizenEconomy(CitizenTravel):
self._report_action("BOUGHT_PRODUCTS", json_ret.get('message'), kwargs=json_ret) self._report_action("BOUGHT_PRODUCTS", json_ret.get('message'), kwargs=json_ret)
return json_ret return json_ret
@utils.wait_for_lock
def buy_market_offer(self, offer: OfferItem, amount: int = None) -> Optional[Dict[str, Any]]: def buy_market_offer(self, offer: OfferItem, amount: int = None) -> Optional[Dict[str, Any]]:
if amount is None or amount > offer.amount: if amount is None or amount > offer.amount:
amount = offer.amount amount = offer.amount
@ -1326,7 +1381,7 @@ class CitizenEconomy(CitizenTravel):
self.update_money() self.update_money()
cur = "g" if currency == 62 else "cc" cur = "g" if currency == 62 else "cc"
if success: if success:
self._report_action("DONATE_MONEY", f"Successfully donated {amount}{cur} to citizen with id {citizen_id}!") self.report_money_donation(citizen_id, amount, currency == 1)
else: else:
self._report_action("DONATE_MONEY", f"Unable to donate {amount}{cur}!") self._report_action("DONATE_MONEY", f"Unable to donate {amount}{cur}!")
return success return success
@ -1410,6 +1465,16 @@ class CitizenEconomy(CitizenTravel):
f" treasury", kwargs=r.json()) f" treasury", kwargs=r.json())
return False return False
def report_money_donation(self, citizen_id: int, amount: float, is_currency: bool = True):
self.reporter.report_money_donation(citizen_id, amount, is_currency)
if self.config.telegram:
self.telegram.report_money_donation(citizen_id, amount, is_currency)
def report_item_donation(self, citizen_id: int, amount: float, quality: int, industry: str):
self.reporter.report_item_donation(citizen_id, amount, quality, industry)
if self.config.telegram:
self.telegram.report_item_donation(citizen_id, amount, f"{industry} q{quality}")
class CitizenLeaderBoard(BaseCitizen): class CitizenLeaderBoard(BaseCitizen):
def get_aircraft_damage_rankings(self, country: int, weeks: int = 0, mu: int = 0) -> Dict[str, any]: def get_aircraft_damage_rankings(self, country: int, weeks: int = 0, mu: int = 0) -> Dict[str, any]:
@ -1490,11 +1555,15 @@ class CitizenMedia(BaseCitizen):
class CitizenMilitary(CitizenTravel): class CitizenMilitary(CitizenTravel):
all_battles: Dict[int, classes.Battle] = None all_battles: Dict[int, classes.Battle] = None
countries: Dict[int, Dict[str, Union[str, List[int]]]] = None
__last_war_update_data = None __last_war_update_data = None
active_fs: bool = False active_fs: bool = False
boosters: Dict[int, Dict[int, int]] = {100: {}, 50: {}}
@property
def as_dict(self):
d = super().as_dict
d.update(active_fs=self.active_fs, all_battles=self.all_battles)
return d
def update_war_info(self): def update_war_info(self):
if self.__last_war_update_data and self.__last_war_update_data.get('last_updated', if self.__last_war_update_data and self.__last_war_update_data.get('last_updated',
@ -1505,17 +1574,6 @@ class CitizenMilitary(CitizenTravel):
if r_json.get("countries"): if r_json.get("countries"):
if self.all_battles is None: if self.all_battles is None:
self.all_battles = {} self.all_battles = {}
if self.countries is None:
self.countries = {}
countries = {}
for c_id, c_data in r_json.get("countries").items():
if int(c_id) not in countries:
countries.update({
int(c_id): {"name": c_data.get("name"), "allies": c_data.get("allies")}
})
else:
countries[int(c_id)].update(allies=c_data.get("allies"))
self.countries = countries
self.__last_war_update_data = r_json self.__last_war_update_data = r_json
if r_json.get("battles"): if r_json.get("battles"):
all_battles = {} all_battles = {}
@ -1735,7 +1793,8 @@ class CitizenMilitary(CitizenTravel):
yield battle, battle_zone, side yield battle, battle_zone, side
def find_battle_and_fight(self): def find_battle_and_fight(self):
if self.should_fight()[0]: count = self.should_fight()[0]
if count:
self.write_log("Checking for battles to fight in...") self.write_log("Checking for battles to fight in...")
for battle, division, side in self.find_battle_to_fight(): for battle, division, side in self.find_battle_to_fight():
@ -1762,25 +1821,26 @@ class CitizenMilitary(CitizenTravel):
if self.change_division(battle, division): if self.change_division(battle, division):
self.set_default_weapon(battle, division) self.set_default_weapon(battle, division)
self.fight(battle, division, side) self.fight(battle, division, side, count)
self.travel_to_residence() self.travel_to_residence()
break break
@utils.wait_for_lock
def fight(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide = None, def fight(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide = None,
count: int = None) -> Optional[int]: count: int = None, use_ebs: bool = False) -> Optional[int]:
"""Fight in a battle. """Fight in a battle.
Will auto activate booster and travel if allowed to do it. Will auto activate booster and travel if allowed to do it.
:param battle: Battle battle to fight in :param battle: Battle battle to fight in
:type battle: Battle :type battle: Battle
:param division: Division number to fight in available choices
:type division: BattleDivision
:param side: BattleSide or None. Battle side to fight in, If side not == invader id or not in invader deployed :param side: BattleSide or None. Battle side to fight in, If side not == invader id or not in invader deployed
allies list, then defender's side is chosen allies list, then defender's side is chosen
:type side: BattleSide :type side: BattleSide
:param count: How many hits to do, if not specified self.should_fight() is called. :param count: How many hits to do, if not specified self.should_fight() is called.
:type count: int :type count: int
:param division: Division number to fight in available choices :param use_ebs: Should use energy bars if count > 0 and not enough food_fights
:type division: BattleDivision :type use_ebs: bool
:return: None if no errors while fighting, otherwise error count. :return: None if no errors while fighting, otherwise error count.
:rtype: int :rtype: int
""" """
@ -1788,7 +1848,7 @@ class CitizenMilitary(CitizenTravel):
self._report_action("IP_BLACKLISTED", "Fighting is not allowed from restricted IP!") self._report_action("IP_BLACKLISTED", "Fighting is not allowed from restricted IP!")
return 1 return 1
if not division.is_air and self.config.boosters: if not division.is_air and self.config.boosters:
self.activate_dmg_booster() self.activate_damage_booster(not division.is_air)
if side is None: if side is None:
side = battle.defender if self.details.citizenship in battle.defender.allies + [ side = battle.defender if self.details.citizenship in battle.defender.allies + [
battle.defender.country] else battle.invader battle.defender.country] else battle.invader
@ -1810,15 +1870,13 @@ class CitizenMilitary(CitizenTravel):
error_count += error error_count += error
else: else:
self._eat('blue') self._eat('blue')
if count > 0 and self.energy.recovered < 50 and use_ebs:
self._eat('orange')
if self.energy.recovered < 50 or error_count >= 10 or count <= 0: if self.energy.recovered < 50 or error_count >= 10 or count <= 0:
self.write_log(f"Hits: {total_hits:>4} | Damage: {total_damage}") self.write_log(f"Hits: {total_hits:>4} | Damage: {total_damage}")
ok_to_fight = False ok_to_fight = False
if total_damage: if total_damage:
self.reporter.report_fighting(battle, not side.is_defender, division, total_damage, total_hits) self.report_fighting(battle, not side.is_defender, division, total_damage, total_hits)
# self.reporter.report_action('FIGHT', dict(battle_id=battle.id, side=side, dmg=total_damage,
# air=battle.has_air, hits=total_hits,
# round=battle.zone_id,
# extra=dict(battle=battle, side=side)))
return error_count return error_count
def _shoot(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide): def _shoot(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide):
@ -1873,7 +1931,7 @@ class CitizenMilitary(CitizenTravel):
else: else:
hits = r_json['hits'] hits = r_json['hits']
if r_json['user']['epicBattle']: if r_json['user']['epicBattle']:
hits /= 1+r_json['user']['epicBattle'] hits /= 1 + r_json['user']['epicBattle']
self.energy.recovered = r_json["details"]["wellness"] self.energy.recovered = r_json["details"]["wellness"]
self.details.xp = int(r_json["details"]["points"]) self.details.xp = int(r_json["details"]["points"])
@ -1883,8 +1941,8 @@ class CitizenMilitary(CitizenTravel):
return hits, err, damage return hits, err, damage
@utils.wait_for_lock def deploy_bomb(self, battle: classes.Battle, division: classes.BattleDivision, bomb_id: int, inv_side: bool,
def deploy_bomb(self, battle: classes.Battle, division: classes.BattleDivision, bomb_id: int, inv_side: bool, count: int = 1) -> Optional[int]: count: int = 1) -> Optional[int]:
"""Deploy bombs in a battle for given side. """Deploy bombs in a battle for given side.
:param battle: Battle :param battle: Battle
@ -1979,34 +2037,49 @@ class CitizenMilitary(CitizenTravel):
return utils.calculate_hit(0, rang, True, elite, ne, 0, 20 if weapon else 0) return utils.calculate_hit(0, rang, True, elite, ne, 0, 20 if weapon else 0)
def activate_dmg_booster(self): def activate_damage_booster(self, ground: bool = True) -> int:
if self.config.boosters: kind = 'damage' if ground else 'aircraftDamage'
if not self.get_active_ground_damage_booster(): if self.config.boosters and not self.get_active_damage_booster(ground):
duration = 0 booster: Optional[types.InvFinalItem] = None
for length, amount in self.boosters[50].items(): for quality, data in sorted(self.inventory.boosters.get(kind, {}).items(), key=lambda x: x[0]):
if amount > 2: for _duration, _booster in sorted(data.items(), key=lambda y: y[0]):
duration = length critical_amount = 2 if quality < 10 and ground else 10
if _booster.get('amount') > critical_amount:
booster = _booster
break break
if duration: break
self._report_action("MILITARY_BOOSTER", f"Activated 50% {duration / 60}h damage booster") if booster:
self._post_economy_activate_booster(5, duration, "damage") kind = 'damage' if ground else 'air_damage'
self._report_action("MILITARY_BOOSTER", f"Activated {booster['name']}")
resp = self._post_economy_activate_booster(booster['quality'], booster['durability'], kind).json()
self._update_inventory_data(resp)
return self.get_active_damage_booster(ground)
def get_active_ground_damage_booster(self) -> int: def get_active_damage_booster(self, ground: bool = True) -> int:
inventory = self.get_inventory() kind = 'damage' if ground else 'aircraftDamage'
boosters = self.inventory.active.get(kind, {})
quality = 0 quality = 0
if inventory['active'].get('damageBoosters', {}).get(10): for q, boost in boosters.items():
quality = 100 if boost['quality'] * 10 > quality:
elif inventory['active'].get('damageBoosters', {}).get(5): quality = boost['quality'] * 10
quality = 50
return quality return quality
def activate_battle_effect(self, battle_id: int, kind: str) -> Response: def get_active_ground_damage_booster(self) -> int:
self._report_action('MILITARY_BOOSTER', f'Activated {kind} booster') return self.get_active_damage_booster(True)
return self._post_main_activate_battle_effect(battle_id, kind, self.details.citizen_id)
def activate_pp_booster(self, battle_id: int) -> Response: def get_active_air_damage_booster(self) -> int:
self._report_action('MILITARY_BOOSTER', 'Activated PrestigePoint booster') return self.get_active_damage_booster(False)
return self._post_military_fight_activate_booster(battle_id, 1, 180, "prestige_points")
def activate_battle_effect(self, battle_id: int, kind: str) -> bool:
self._report_action('MILITARY_BOOSTER', f'Activated {kind} booster')
resp = self._post_main_activate_battle_effect(battle_id, kind, self.details.citizen_id).json()
return not resp.get('error')
def activate_pp_booster(self, pp_item: types.InvFinalItem) -> bool:
self._report_action('MILITARY_BOOSTER', f'Activated {pp_item["name"]}')
resp = self._post_economy_activate_booster(pp_item['quality'], pp_item['durability'], 'prestige_points').json()
self._update_inventory_data(resp)
return pp_item.get('kind') in self.inventory.active
def _rw_choose_side(self, battle: classes.Battle, side: classes.BattleSide) -> Response: def _rw_choose_side(self, battle: classes.Battle, side: classes.BattleSide) -> Response:
return self._post_main_battlefield_travel(side.id, battle.id) return self._post_main_battlefield_travel(side.id, battle.id)
@ -2167,6 +2240,11 @@ class CitizenMilitary(CitizenTravel):
if division.wall['dom'] == 50 or division.wall['dom'] > 98: if division.wall['dom'] == 50 or division.wall['dom'] > 98:
yield division, division.wall['for'] == battle.invader.country.id yield division, division.wall['for'] == battle.invader.country.id
def report_fighting(self, battle: classes.Battle, invader: bool, division: classes.BattleDivision, damage: float, hits: int):
self.reporter.report_fighting(battle, invader, division, damage, hits)
if self.config.telegram:
self.telegram.report_fight(battle, invader, division, damage, hits)
class CitizenPolitics(BaseCitizen): class CitizenPolitics(BaseCitizen):
def get_country_parties(self, country: constants.Country = None) -> dict: def get_country_parties(self, country: constants.Country = None) -> dict:
@ -2181,7 +2259,8 @@ class CitizenPolitics(BaseCitizen):
self._report_action('POLITIC_PARTY_PRESIDENT', 'Applied for party president elections') self._report_action('POLITIC_PARTY_PRESIDENT', 'Applied for party president elections')
return self._get_candidate_party(self.politics.party_slug) return self._get_candidate_party(self.politics.party_slug)
else: else:
self._report_action('POLITIC_CONGRESS', 'Unable to apply for party president elections - not a party member') self._report_action('POLITIC_CONGRESS',
'Unable to apply for party president elections - not a party member')
return None return None
def candidate_for_congress(self, presentation: str = "") -> Optional[Response]: def candidate_for_congress(self, presentation: str = "") -> Optional[Response]:
@ -2319,6 +2398,12 @@ class CitizenTasks(CitizenEconomy):
ot_points: int = 0 ot_points: int = 0
next_ot_time: datetime = None next_ot_time: datetime = None
@property
def as_dict(self):
d = super().as_dict
d.update(tg_contract=self.tg_contract, ot_points=self.ot_points, next_ot_time=self.next_ot_time)
return d
def eat(self): def eat(self):
""" Eat food """ """ Eat food """
self._eat("blue") self._eat("blue")
@ -2446,8 +2531,8 @@ class CitizenTasks(CitizenEconomy):
self.ot_points = ot.get("points", 0) self.ot_points = ot.get("points", 0)
class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard, class _Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
CitizenMedia, CitizenPolitics, CitizenSocial, CitizenMilitary, CitizenTasks): CitizenMedia, CitizenPolitics, CitizenSocial, CitizenMilitary, CitizenTasks):
def __init__(self, email: str = "", password: str = "", auto_login: bool = False): def __init__(self, email: str = "", password: str = "", auto_login: bool = False):
super().__init__(email, password) super().__init__(email, password)
self._last_full_update = constants.min_datetime self._last_full_update = constants.min_datetime
@ -2458,10 +2543,18 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
@classmethod @classmethod
def load_from_dump(cls, dump_name: str = ""): def load_from_dump(cls, dump_name: str = ""):
filename = dump_name if dump_name else f"{cls.__name__}__dump.json" filename = dump_name if dump_name else f"{cls.__name__}__dump.json"
player: Citizen = super().load_from_dump(filename) # noqa player: _Citizen = super().load_from_dump(filename) # noqa
player.login() player.login()
return player return player
def _eat(self, colour: str = "blue") -> Response:
resp = super()._eat(colour)
if not any([resp.json().get("units_consumed").values()]):
if colour == 'orange' and resp.json().get('food_remaining'):
self.eat()
return self._eat(colour)
return resp
def config_setup(self, **kwargs): def config_setup(self, **kwargs):
self.config.reset() self.config.reset()
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -2565,17 +2658,21 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
data = self._get_main_weekly_challenge_data().json() data = self._get_main_weekly_challenge_data().json()
self.details.pp = data.get("player", {}).get("prestigePoints", 0) self.details.pp = data.get("player", {}).get("prestigePoints", 0)
self.details.next_pp.clear() self.details.next_pp.clear()
max_collectable_id = data.get('maxRewardId')
should_collect = False
for reward in data.get("rewards", {}).get("normal", {}): for reward in data.get("rewards", {}).get("normal", {}):
status = reward.get("status", "") status = reward.get("status", "")
if status == "rewarded": if status == "rewarded":
continue continue
elif status == "completed": elif status == "completed":
self._post_main_weekly_challenge_reward(reward.get("id", 0)) should_collect = True
elif reward.get("icon", "") == "energy_booster": elif reward.get("icon", "") == "energy_booster":
pps = re.search(r"Reach (\d+) Prestige Points to unlock the following reward: \+1 Energy", pps = re.search(r"Reach (\d+) Prestige Points to unlock the following reward: \+1 Energy",
reward.get("tooltip", "")) reward.get("tooltip", ""))
if pps: if pps:
self.details.next_pp.append(int(pps.group(1))) self.details.next_pp.append(int(pps.group(1)))
if should_collect:
self._post_main_weekly_challenge_collect_all(max_collectable_id)
def should_fight(self, silent: bool = True) -> Tuple[int, str, bool]: def should_fight(self, silent: bool = True) -> Tuple[int, str, bool]:
count, log_msg, force_fight = super().should_fight() count, log_msg, force_fight = super().should_fight()
@ -2612,16 +2709,19 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
def state_update_repeater(self): def state_update_repeater(self):
try: try:
start_time = self.now.replace(second=0, microsecond=0) start_time = self.now.replace(minute=(self.now.minute // 10) * 10, second=0, microsecond=0)
if start_time.minute <= 30: if not self.restricted_ip:
start_time = start_time.replace(minute=30) if start_time.minute <= 30:
else: start_time = start_time.replace(minute=30)
start_time = utils.good_timedelta(start_time.replace(minute=0), timedelta(hours=1)) else:
start_time = utils.good_timedelta(start_time.replace(minute=0), timedelta(hours=1))
while not self.stop_threads.is_set(): while not self.stop_threads.is_set():
start_time = utils.good_timedelta(start_time, timedelta(minutes=10 if self.restricted_ip else 30))
self.update_citizen_info() self.update_citizen_info()
start_time = utils.good_timedelta(start_time, timedelta(minutes=30)) self.update_weekly_challenge()
self.send_state_update() self.send_state_update()
self.send_inventory_update() self.send_inventory_update()
self.update_companies()
self.send_my_companies_update() self.send_my_companies_update()
sleep_seconds = (start_time - self.now).total_seconds() sleep_seconds = (start_time - self.now).total_seconds()
self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0) self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0)
@ -2630,13 +2730,13 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
def send_state_update(self): def send_state_update(self):
data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp, data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp,
inv_total=self.inventory_status['total'], inv=self.inventory_status['used'], inv_total=self.inventory.total, inv=self.inventory.used,
hp_limit=self.energy.limit, hp_limit=self.energy.limit,
hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], ) hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], )
self.reporter.send_state_update(**data) self.reporter.send_state_update(**data)
def send_inventory_update(self): def send_inventory_update(self):
self.reporter.report_action("INVENTORY", json_val=self.get_inventory(True)) self.reporter.report_action("INVENTORY", json_val=self.inventory.as_dict)
def send_my_companies_update(self): def send_my_companies_update(self):
self.reporter.report_action('COMPANIES', json_val=self.my_companies.as_dict) self.reporter.report_action('COMPANIES', json_val=self.my_companies.as_dict)
@ -2774,7 +2874,7 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
for holding in regions.values(): for holding in regions.values():
raw_usage = holding.get_wam_raw_usage() raw_usage = holding.get_wam_raw_usage()
free_storage = self.inventory_status['total'] - self.inventory_status['used'] free_storage = self.inventory.total - self.inventory.used
if (raw_usage['frm'] + raw_usage['wrm']) * 100 > free_storage: if (raw_usage['frm'] + raw_usage['wrm']) * 100 > free_storage:
self._report_action('WAM_UNAVAILABLE', 'Not enough storage!') self._report_action('WAM_UNAVAILABLE', 'Not enough storage!')
continue continue
@ -2813,3 +2913,161 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenLeaderBoard,
self.stop_threads.wait(90) self.stop_threads.wait(90)
except: # noqa except: # noqa
self.report_error('Command central is broken') self.report_error('Command central is broken')
class Citizen(_Citizen):
_concurrency_lock: Event
_update_lock: Event
_update_timeout: int = 10
_concurrency_timeout: int = 600
def __init__(self, *args, **kwargs):
self._concurrency_lock = Event()
self._concurrency_lock.set()
self._update_lock = Event()
self._update_lock.set()
super().__init__(*args, **kwargs)
def update_weekly_challenge(self):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_weekly_challenge()
finally:
self._update_lock.set()
def update_companies(self):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_companies()
finally:
self._update_lock.set()
def update_war_info(self):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_war_info()
finally:
self._update_lock.set()
def update_job_info(self):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_job_info()
finally:
self._update_lock.set()
def update_money(self, page: int = 0, currency: int = 62):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_money(page, currency)
finally:
self._update_lock.set()
def update_inventory(self):
if not self._update_lock.wait(self._update_timeout):
e = f'Update concurrency not freed in {self._update_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._update_lock.clear()
super().update_inventory()
finally:
self._update_lock.set()
def _work_as_manager(self, wam_holding: classes.Holding) -> Optional[Dict[str, Any]]:
if not self._concurrency_lock.wait(self._concurrency_timeout):
e = f'Concurrency not freed in {self._concurrency_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._concurrency_lock.clear()
return super()._work_as_manager(wam_holding)
finally:
self._concurrency_lock.set()
def fight(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide = None,
count: int = None, use_ebs: bool = False) -> Optional[int]:
if not self._concurrency_lock.wait(self._concurrency_timeout):
e = f'Concurrency not freed in {self._concurrency_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._concurrency_lock.clear()
return super().fight(battle, division, side, count, use_ebs)
finally:
self._concurrency_lock.set()
def deploy_bomb(self, battle: classes.Battle, division: classes.BattleDivision, bomb_id: int, inv_side: bool,
count: int = 1) -> Optional[int]:
if not self._concurrency_lock.wait(self._concurrency_timeout):
e = f'Concurrency not freed in {self._concurrency_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._concurrency_lock.clear()
return super().deploy_bomb(battle, division, bomb_id, inv_side, count)
finally:
self._concurrency_lock.set()
def buy_market_offer(self, offer: OfferItem, amount: int = None) -> Optional[Dict[str, Any]]:
if not self._concurrency_lock.wait(self._concurrency_timeout):
e = f'Concurrency not freed in {self._concurrency_timeout}sec!'
self.write_log(e)
if self.debug:
self.report_error(e)
return None
try:
self._concurrency_lock.clear()
return super().buy_market_offer(offer, amount)
finally:
self._concurrency_lock.set()
@property
def as_dict(self):
d = super().as_dict
d.update(locks=dict(concurrency_lock=self._concurrency_lock.is_set(), update_lock=self._update_lock.is_set(),
concurrency_timeout=self._concurrency_timeout, update_timeout=self._update_timeout))
return d
def set_locks(self):
super().set_locks()
self._concurrency_lock.set()
self._update_lock.set()

View File

@ -3,15 +3,15 @@ import hashlib
import threading import threading
import weakref import weakref
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, Generator, Iterable, List, NamedTuple, NoReturn, Tuple, Union from typing import Any, Dict, Generator, Iterable, List, NamedTuple, NoReturn, Union
from requests import Response, Session, post from requests import Response, Session, post
from . import constants, utils from . import constants, types, utils
__all__ = ['Battle', 'BattleDivision', 'BattleSide', 'Company', 'Config', 'Details', 'Energy', 'ErepublikException', __all__ = ['Battle', 'BattleDivision', 'BattleSide', 'Company', 'Config', 'Details', 'Energy', 'ErepublikException',
'ErepublikNetworkException', 'EnergyToFight', 'ErepublikJSONEncoder', 'ErepublikNetworkException', 'EnergyToFight', 'Holding', 'Inventory', 'MyCompanies',
'Holding', 'MyCompanies', 'MyJSONEncoder', 'OfferItem', 'Politics', 'Reporter', 'TelegramReporter'] 'OfferItem', 'Politics', 'Reporter', 'TelegramReporter', ]
class ErepublikException(Exception): class ErepublikException(Exception):
@ -396,11 +396,11 @@ class Config:
self.spin_wheel_of_fortune = False self.spin_wheel_of_fortune = False
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[bool, int, str, List[str]]]:
return dict(email=self.email, work=self.work, train=self.train, wam=self.wam, ot=self.ot, return dict(email=self.email, work=self.work, train=self.train, wam=self.wam, ot=self.ot,
auto_sell=self.auto_sell, auto_sell_all=self.auto_sell_all, employees=self.employees, auto_sell=self.auto_sell, auto_sell_all=self.auto_sell_all, employees=self.employees,
fight=self.fight, air=self.air, ground=self.ground, all_in=self.all_in, fight=self.fight, air=self.air, ground=self.ground, all_in=self.all_in,
next_energy=self.next_energy, boosters=self.boosters, travel_to_fight=self.travel_to_fight, next_energy=self.next_energy, travel_to_fight=self.travel_to_fight,
always_travel=self.always_travel, epic_hunt=self.epic_hunt, epic_hunt_ebs=self.epic_hunt_ebs, always_travel=self.always_travel, epic_hunt=self.epic_hunt, epic_hunt_ebs=self.epic_hunt_ebs,
rw_def_side=self.rw_def_side, interactive=self.interactive, maverick=self.maverick, rw_def_side=self.rw_def_side, interactive=self.interactive, maverick=self.maverick,
continuous_fighting=self.continuous_fighting, auto_buy_raw=self.auto_buy_raw, continuous_fighting=self.continuous_fighting, auto_buy_raw=self.auto_buy_raw,
@ -454,7 +454,7 @@ class Energy:
return self.recovered + self.recoverable return self.recovered + self.recoverable
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[int, datetime.datetime, bool]]:
return dict(limit=self.limit, interval=self.interval, recoverable=self.recoverable, recovered=self.recovered, return dict(limit=self.limit, interval=self.interval, recoverable=self.recoverable, recovered=self.recovered,
reference_time=self.reference_time, food_fights=self.food_fights, reference_time=self.reference_time, food_fights=self.food_fights,
is_recoverable_full=self.is_recoverable_full, is_recovered_full=self.is_recovered_full, is_recoverable_full=self.is_recoverable_full, is_recovered_full=self.is_recovered_full,
@ -509,7 +509,7 @@ class Details:
return next_level_up - self.xp return next_level_up - self.xp
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[int, float, str, constants.Country, bool]]:
return dict(xp=self.xp, cc=self.cc, pp=self.pp, pin=self.pin, gold=self.gold, next_pp=self.next_pp, return dict(xp=self.xp, cc=self.cc, pp=self.pp, pin=self.pin, gold=self.gold, next_pp=self.next_pp,
citizen_id=self.citizen_id, citizenship=self.citizenship, current_region=self.current_region, citizen_id=self.citizen_id, citizenship=self.citizenship, current_region=self.current_region,
current_country=self.current_country, residence_region=self.residence_region, current_country=self.current_country, residence_region=self.residence_region,
@ -528,7 +528,7 @@ class Politics:
is_country_president: bool = False is_country_president: bool = False
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[bool, int, str]]:
return dict(is_party_member=self.is_party_member, party_id=self.party_id, party_slug=self.party_slug, return dict(is_party_member=self.is_party_member, party_id=self.party_id, party_slug=self.party_slug,
is_party_president=self.is_party_president, is_congressman=self.is_congressman, is_party_president=self.is_party_president, is_congressman=self.is_congressman,
is_country_president=self.is_country_president) is_country_president=self.is_country_president)
@ -566,7 +566,7 @@ class Reporter:
return self.citizen.details.citizen_id return self.citizen.details.citizen_id
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[bool, int, str, List[Dict[Any, Any]]]]:
return dict(name=self.name, email=self.email, citizen_id=self.citizen_id, key=self.key, allowed=self.allowed, return dict(name=self.name, email=self.email, citizen_id=self.citizen_id, key=self.key, allowed=self.allowed,
queue=self.__to_update) queue=self.__to_update)
@ -598,10 +598,10 @@ class Reporter:
if self.__to_update: if self.__to_update:
for unreported_data in self.__to_update: for unreported_data in self.__to_update:
unreported_data.update(player_id=self.citizen_id, key=self.key) unreported_data.update(player_id=self.citizen_id, key=self.key)
unreported_data = utils.json.loads(utils.json.dumps(unreported_data, cls=MyJSONEncoder)) unreported_data = utils.json.loads(utils.json.dumps(unreported_data, cls=ErepublikJSONEncoder))
self._req.post(f"{self.url}/bot/update", json=unreported_data) self._req.post(f"{self.url}/bot/update", json=unreported_data)
self.__to_update.clear() self.__to_update.clear()
data = utils.json.loads(utils.json.dumps(data, cls=MyJSONEncoder)) data = utils.json.loads(utils.json.dumps(data, cls=ErepublikJSONEncoder))
r = self._req.post(f"{self.url}/bot/update", json=data) r = self._req.post(f"{self.url}/bot/update", json=data)
return r return r
@ -649,6 +649,16 @@ class Reporter:
air=battle.has_air, hits=hits, air=battle.has_air, hits=hits,
round=battle.zone_id, extra=dict(battle=battle, side=side, division=division))) round=battle.zone_id, extra=dict(battle=battle, side=side, division=division)))
def report_money_donation(self, citizen_id: int, amount: float, is_currency: bool = True):
cur = 'cc' if is_currency else 'gold'
self.report_action('DONATE_MONEY', dict(citizen_id=citizen_id, amount=amount, currency=cur),
f"Successfully donated {amount}{cur} to citizen with id {citizen_id}!")
def report_item_donation(self, citizen_id: int, amount: float, quality: int, industry: str):
self.report_action('DONATE_ITEMS',
dict(citizen_id=citizen_id, amount=amount, quality=quality, industry=industry),
f"Successfully donated {amount} x {industry} q{quality} to citizen with id {citizen_id}!")
def report_promo(self, kind: str, time_until: datetime.datetime): def report_promo(self, kind: str, time_until: datetime.datetime):
self._req.post(f"{self.url}/promos/add/", data=dict(kind=kind, time_untill=time_until)) self._req.post(f"{self.url}/promos/add/", data=dict(kind=kind, time_untill=time_until))
@ -660,16 +670,19 @@ class Reporter:
except: # noqa except: # noqa
return [] return []
def fetch_tasks(self) -> List[Union[str, Tuple[Any]]]: def fetch_tasks(self) -> List[Dict[str, Any]]:
try: try:
task_response = self._req.get(f'{self.url}/api/v1/command', task_response = self._req.post(
params=dict(citizen=self.citizen_id, key=self.key)) f'{self.url}/api/v1/command', data=dict(citizen=self.citizen_id, key=self.key)).json()
return task_response.json().get('task_collection') if task_response.get('status'):
return task_response.get('data')
else:
return []
except: # noqa except: # noqa
return [] return []
class MyJSONEncoder(utils.json.JSONEncoder): class ErepublikJSONEncoder(utils.json.JSONEncoder):
def default(self, o): def default(self, o):
from erepublik.citizen import Citizen from erepublik.citizen import Citizen
if isinstance(o, Decimal): if isinstance(o, Decimal):
@ -683,7 +696,7 @@ class MyJSONEncoder(utils.json.JSONEncoder):
return dict(__type__='timedelta', days=o.days, seconds=o.seconds, return dict(__type__='timedelta', days=o.days, seconds=o.seconds,
microseconds=o.microseconds, total_seconds=o.total_seconds()) microseconds=o.microseconds, total_seconds=o.total_seconds())
elif isinstance(o, Response): elif isinstance(o, Response):
return dict(headers=o.headers.__dict__, url=o.url, text=o.text) return dict(headers=dict(o.__dict__['headers']), url=o.url, text=o.text, status_code=o.status_code)
elif hasattr(o, 'as_dict'): elif hasattr(o, 'as_dict'):
return o.as_dict return o.as_dict
elif isinstance(o, set): elif isinstance(o, set):
@ -730,7 +743,7 @@ class BattleSide:
return self.country.iso return self.country.iso
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[int, constants.Country, bool, List[constants.Country]]]:
return dict(points=self.points, country=self.country, is_defender=self.is_defender, allies=self.allies, return dict(points=self.points, country=self.country, is_defender=self.is_defender, allies=self.allies,
deployed=self.deployed) deployed=self.deployed)
@ -973,7 +986,7 @@ class TelegramReporter:
self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5)) self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5))
self._last_full_energy_report = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-30)) self._last_full_energy_report = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-30))
if self.__queue: if self.__queue:
self.send_message("\n\n\n\n".join(self.__queue)) self.send_message("Telegram initialized")
def send_message(self, message: str) -> bool: def send_message(self, message: str) -> bool:
self.__queue.append(message) self.__queue.append(message)
@ -1001,15 +1014,29 @@ class TelegramReporter:
new_line = '\n' if multiple else '' new_line = '\n' if multiple else ''
self.send_message(f"New award: {new_line}*{msg}*") self.send_message(f"New award: {new_line}*{msg}*")
def report_fight(self, battle: "Battle", invader: bool, division: "BattleDivision", damage: float, hits: int):
side_txt = (battle.invader if invader else battle.defender).country.iso
self.send_message(f"*Fight report*:\n{int(damage):,d} dmg ({hits} hits) in"
f" [battle {battle.id} for {battle.region_name[:16]}]({battle.link}) in d{division.div} on "
f"{side_txt} side")
def report_item_donation(self, citizen_id: int, amount: float, product: str):
self.send_message(f"*Donation*: {amount} x {product} to citizen "
f"[{citizen_id}](https://www.erepublik.com/en/citizen/profile/{citizen_id})")
def report_money_donation(self, citizen_id: int, amount: float, is_currency: bool = True):
self.send_message(f"*Donation*: {amount}{'cc' if is_currency else 'gold'} to citizen "
f"[{citizen_id}](https://www.erepublik.com/en/citizen/profile/{citizen_id})")
def __send_messages(self): def __send_messages(self):
while self._next_time > utils.now(): while self._next_time > utils.now():
if self.__thread_stopper.is_set(): if self.__thread_stopper.is_set():
break break
self.__thread_stopper.wait(utils.get_sleep_seconds(self._next_time)) self.__thread_stopper.wait(utils.get_sleep_seconds(self._next_time))
message = "\n\n\n\n".join(self.__queue) message = "\n\n".join(self.__queue)
if self.player_name: if self.player_name:
message = f"Player *{self.player_name}*\n" + message message = f"Player *{self.player_name}*\n\n" + message
response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown")) response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown"))
self._last_time = utils.now() self._last_time = utils.now()
if response.json().get('ok'): if response.json().get('ok'):
@ -1024,3 +1051,27 @@ class OfferItem(NamedTuple):
amount: int = 0 amount: int = 0
offer_id: int = 0 offer_id: int = 0
citizen_id: int = 0 citizen_id: int = 0
class Inventory:
final: types.InvFinal
active: types.InvFinal
boosters: types.InvBooster
raw: types.InvRaw
market: types.InvRaw
used: int
total: int
def __init__(self):
self.active = {}
self.final = {}
self.boosters = {}
self.raw = {}
self.offers = {}
self.used = 0
self.total = 0
@property
def as_dict(self) -> Dict[str, Union[types.InvFinal, types.InvRaw, int]]:
return dict(active=self.active, final=self.final, boosters=self.boosters, raw=self.raw, offers=self.offers,
total=self.total, used=self.used)

7
erepublik/types.py Normal file
View File

@ -0,0 +1,7 @@
from datetime import datetime
from typing import Dict, List, Union
InvFinalItem = Dict[str, Union[str, int, List[Dict[str, Union[int, datetime]]]]]
InvBooster = Dict[str, Dict[int, Dict[int, InvFinalItem]]]
InvFinal = Dict[str, Dict[int, InvFinalItem]]
InvRaw = Dict[str, Dict[int, Dict[str, Union[str, int]]]]

View File

@ -10,8 +10,9 @@ import unicodedata
import warnings import warnings
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Union
import pytz
import requests import requests
from . import __version__, constants from . import __version__, constants
@ -25,11 +26,8 @@ __all__ = ['VERSION', 'calculate_hit', 'caught_error', 'date_from_eday', 'eday_f
'get_air_hit_dmg_value', 'get_file', 'get_ground_hit_dmg_value', 'get_sleep_seconds', 'good_timedelta', 'get_air_hit_dmg_value', 'get_file', 'get_ground_hit_dmg_value', 'get_sleep_seconds', 'good_timedelta',
'interactive_sleep', 'json', 'localize_dt', 'localize_timestamp', 'normalize_html_json', 'now', 'interactive_sleep', 'json', 'localize_dt', 'localize_timestamp', 'normalize_html_json', 'now',
'process_error', 'process_warning', 'send_email', 'silent_sleep', 'slugify', 'write_file', 'write_request', 'process_error', 'process_warning', 'send_email', 'silent_sleep', 'slugify', 'write_file', 'write_request',
'write_interactive_log', 'write_silent_log', 'get_final_hit_dmg', 'wait_for_lock'] 'write_interactive_log', 'write_silent_log', 'get_final_hit_dmg', 'wait_for_lock',
'json_decode_object_hook', 'json_load', 'json_loads']
if not sys.version_info >= (3, 6):
raise AssertionError('This script requires Python version 3.6 and higher\n'
'But Your version is v{}.{}.{}'.format(*sys.version_info))
VERSION: str = __version__ VERSION: str = __version__
@ -64,7 +62,9 @@ def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.da
return constants.erep_tz.normalize(dt + td) return constants.erep_tz.normalize(dt + td)
def eday_from_date(date: Union[datetime.date, datetime.datetime] = now()) -> int: def eday_from_date(date: Union[datetime.date, datetime.datetime] = None) -> int:
if date is None:
date = now()
if isinstance(date, datetime.date): if isinstance(date, datetime.date):
date = datetime.datetime.combine(date, datetime.time(0, 0, 0)) date = datetime.datetime.combine(date, datetime.time(0, 0, 0))
return (date - datetime.datetime(2007, 11, 20, 0, 0, 0)).days return (date - datetime.datetime(2007, 11, 20, 0, 0, 0)).days
@ -232,8 +232,8 @@ def send_email(name: str, content: List[Any], player=None, local_vars: Dict[str,
if isinstance(local_vars.get('citizen'), Citizen): if isinstance(local_vars.get('citizen'), Citizen):
local_vars['citizen'] = repr(local_vars['citizen']) local_vars['citizen'] = repr(local_vars['citizen'])
from erepublik.classes import MyJSONEncoder from erepublik.classes import ErepublikJSONEncoder
files.append(('file', ("local_vars.json", json.dumps(local_vars, cls=MyJSONEncoder), files.append(('file', ("local_vars.json", json.dumps(local_vars, cls=ErepublikJSONEncoder),
"application/json"))) "application/json")))
if isinstance(player, Citizen): if isinstance(player, Citizen):
files.append(('file', ("instance.json", player.to_json(indent=True), "application/json"))) files.append(('file', ("instance.json", player.to_json(indent=True), "application/json")))
@ -253,7 +253,7 @@ def caught_error(e: Exception):
def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None, def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None,
interactive: Optional[bool] = None): interactive: bool = None):
""" """
Process error logging and email sending to developer Process error logging and email sending to developer
:param interactive: Should print interactively :param interactive: Should print interactively
@ -377,11 +377,6 @@ def get_final_hit_dmg(base_dmg: Union[Decimal, float, str], rang: int,
dmg = dmg * 11 / 10 dmg = dmg * 11 / 10
return Decimal(dmg) return Decimal(dmg)
# def _clear_up_battle_memory(battle):
# del battle.invader._battle, battle.defender._battle
# for div_id, division in battle.div.items():
# del division._battle
def deprecation(message): def deprecation(message):
warnings.warn(message, DeprecationWarning, stacklevel=2) warnings.warn(message, DeprecationWarning, stacklevel=2)
@ -404,4 +399,38 @@ def wait_for_lock(function):
raise e raise e
instance.concurrency_available.set() instance.concurrency_available.set()
return ret return ret
return wrapper return wrapper
def json_decode_object_hook(
o: Union[Dict[str, Any], List[Any], int, float, str]
) -> Union[Dict[str, Any], List[Any], int, float, str, datetime.date, datetime.datetime, datetime.timedelta]:
""" Convert classes.ErepublikJSONEncoder datetime, date and timedelta to their python objects
:param o:
:return: Union[Dict[str, Any], List[Any], int, float, str, datetime.date, datetime.datetime, datetime.timedelta]
"""
if o.get('__type__'):
_type = o.get('__type__')
if _type == 'datetime':
dt = datetime.datetime.strptime(f"{o['date']} {o['time']}", "%Y-%m-%d %H:%M:%S")
if o.get('tzinfo'):
dt = pytz.timezone(o['tzinfo']).localize(dt)
return dt
elif _type == 'date':
dt = datetime.datetime.strptime(f"{o['date']}", "%Y-%m-%d")
return dt.date()
elif _type == 'timedelta':
return datetime.timedelta(seconds=o['total_seconds'])
return o
def json_load(f, **kwargs):
kwargs.update(object_hook=json_decode_object_hook)
return json.load(f, **kwargs)
def json_loads(s: str, **kwargs):
kwargs.update(object_hook=json_decode_object_hook)
return json.loads(s, **kwargs)

View File

@ -1,18 +1,19 @@
bump2version==1.0.1 bump2version==1.0.1
coverage==5.3 coverage==5.3.1
edx-sphinx-theme==1.5.0 edx-sphinx-theme==1.6.1
flake8==3.8.4 flake8==3.8.4
ipython>=7.19.0 ipython>=7.19.0
isort==5.6.4 isort==5.7.0
pip==20.3.3 pip==20.3.3
PyInstaller==4.1 pre-commit==2.9.3
pytz==2020.4
pytest==6.2.1
responses==0.12.1
setuptools==51.0.0
Sphinx==3.3.1
requests>=2.24.0,<2.26.0
PySocks==1.7.1
twine==3.2.0
wheel==0.36.2
pur==5.3.0 pur==5.3.0
PyInstaller==4.2
PySocks==1.7.1
pytest==6.2.1
pytz>=2020.5
requests>=2.25.1
responses==0.12.1
setuptools==51.3.3
Sphinx==3.4.3
twine==3.3.0
wheel==0.36.2

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.23.3.4 current_version = 0.23.4.13
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)?
@ -37,4 +37,3 @@ warn_unused_configs = True
[isort] [isort]
multi_line_output = 2 multi_line_output = 2
line_length = 120 line_length = 120
not_skip = __init__.py

View File

@ -12,7 +12,7 @@ with open('HISTORY.rst') as history_file:
history = history_file.read() history = history_file.read()
requirements = [ requirements = [
'pytz==2020.4', 'pytz>=2020.0',
'requests>=2.24.0,<2.26.0', 'requests>=2.24.0,<2.26.0',
'PySocks==1.7.1' 'PySocks==1.7.1'
] ]
@ -50,6 +50,6 @@ setup(
test_suite='tests', test_suite='tests',
tests_require=test_requirements, tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/', url='https://github.com/eeriks/erepublik/',
version='0.23.3.4', version='0.23.4.13',
zip_safe=False, zip_safe=False,
) )