Compare commits

...

75 Commits

Author SHA1 Message Date
dd6e22af51 Bump version: 0.25.1.3 → 0.25.1.4 2021-07-23 11:14:50 +03:00
df4ed4fceb bugfix 2021-07-23 11:14:27 +03:00
b5973ef815 Bump version: 0.25.1.2 → 0.25.1.3 2021-07-23 10:29:46 +03:00
61df989cb4 Catch connection errors.
Not logged in bugfix.
Minor tweaks.

Black for setup
2021-07-23 10:29:33 +03:00
ade0f9b11a Black formatting
Style config update

Makefile update

Test fix
2021-07-21 14:20:35 +03:00
0b07ee8b8f Bump version: 0.25.1.1 → 0.25.1.2 2021-07-20 14:47:17 +03:00
b8884b4877 Requirement update
Bugfix
2021-07-20 14:05:54 +03:00
5a1f7801a2 Bump version: 0.25.1 → 0.25.1.1 2021-07-20 13:16:55 +03:00
17845f750c Bugfix 2021-07-20 13:14:13 +03:00
883af51197 Bump version: 0.25.0.4 → 0.25.1 2021-07-10 01:32:57 +03:00
814cb5ab87 Programmatic user agent spoofing 2021-07-10 01:29:31 +03:00
3c316bada3 Bump version: 0.25.0.3 → 0.25.0.4 2021-07-10 01:14:30 +03:00
ea03979943 Energy variable bugfix 2021-07-10 01:13:21 +03:00
9aae685b21 Bump version: 0.25.0.2 → 0.25.0.3 2021-07-10 00:47:51 +03:00
cae94d7aa8 requirement update 2021-07-10 00:47:40 +03:00
fae7b0fd37 Bump version: 0.25.0.1 → 0.25.0.2 2021-07-10 00:37:46 +03:00
b7771b4da2 Update 2021-07-10 00:36:22 +03:00
e3a10af101 Bump version: 0.25.0 → 0.25.0.1 2021-05-25 09:54:44 +03:00
33a5bcacf1 Backward compatability bugfix 2021-05-25 09:54:35 +03:00
342ca2e1cc Bump version: 0.24.2.4 → 0.25.0 2021-05-25 09:49:49 +03:00
580240a015 isort 2021-05-25 09:49:27 +03:00
1517103ba3 Remove deprecated eat task from examples 2021-05-25 09:48:08 +03:00
3dac8c5e74 New eating 2021-05-25 09:43:47 +03:00
8cf86fb9d3 Bump version: 0.24.2.3 → 0.24.2.4 2021-04-27 12:21:54 +03:00
cf927df6e6 bugfix 2021-04-27 12:21:50 +03:00
a6f5dbd05f Bump version: 0.24.2.2 → 0.24.2.3 2021-04-27 12:19:22 +03:00
967afa472f Rank.__str__ 2021-04-27 12:18:56 +03:00
a65568cd0c Bump version: 0.24.2.1 → 0.24.2.2 2021-04-27 11:05:57 +03:00
6e45334d99 Rank comparision 2021-04-27 11:05:51 +03:00
936a1010a6 Bump version: 0.24.2 → 0.24.2.1 2021-04-26 13:12:32 +03:00
acc528cb1d bugfix 2021-04-26 13:12:28 +03:00
614d273104 Bump version: 0.24.1 → 0.24.2 2021-04-23 13:46:51 +03:00
95966764e8 Updated rank constants 2021-04-23 13:45:58 +03:00
f52b078e6a Captcha challange solving simplification 2021-03-05 16:11:52 +02:00
3af27f6512 Cookie dump migration 2021-03-03 13:15:26 +02:00
6276242260 bugfixes 2021-03-03 13:13:19 +02:00
45623de97b Reporter update to queue messages if network error occures 2021-02-06 15:33:08 +02:00
25f932121c Code cleanup and JSONEncoder update to support dumping Logger instances 2021-02-06 15:32:30 +02:00
61be2b1edf Bump version: 0.24.0.5 → 0.24.1 2021-02-06 12:22:37 +02:00
ce9034ad24 Legacy fight support until 2021-02-08 00:00:00 2021-02-06 12:22:23 +02:00
69b2073b74 more fixes 2021-02-06 11:07:23 +02:00
eb048bf9f8 session dump 2021-02-06 11:07:03 +02:00
fe1206dc84 Cookie magick 2021-02-05 13:47:30 +02:00
a2a1ed3dad Cookie magick 2021-02-05 13:37:32 +02:00
39c8f6913e Cookie magick 2021-02-04 20:33:59 +02:00
d7b15b3708 changes 2021-02-04 13:50:24 +02:00
4fe3efa045 fix 2021-02-03 21:01:37 +02:00
14bcb46735 More precisly mimic javascript's JSON.stringify() 2021-02-03 20:13:51 +02:00
b04cc896d8 bugfix 2021-02-03 20:13:15 +02:00
f07062788b LocalVars 2021-02-03 18:45:23 +02:00
4504bdaa97 don't lose image id 2021-02-03 18:15:06 +02:00
ac135614cc LocalVars 2021-02-03 17:27:55 +02:00
41752e1f2e clickMatrix bugfix 2021-02-03 17:08:58 +02:00
a1739e627e refactoring 2021-02-03 16:43:49 +02:00
12ff11deea refactoring 2021-02-03 16:30:52 +02:00
4e3a16b8d4 Don't print stack and exc traces 2021-02-03 16:16:23 +02:00
5f56f59ab8 bugfi 2021-02-03 14:15:32 +02:00
50c66efbda report error update 2021-02-03 14:03:43 +02:00
47b3154c6a Typehints 2021-02-03 13:36:26 +02:00
632e4e8ad2 HttpHandler improvements 2021-02-03 12:02:06 +02:00
7c0d66f126 deploy 2021-02-03 02:03:02 +02:00
842fb64dae deploy 2021-02-03 01:58:18 +02:00
b22349cb1a Disabled oldSchool Shoooooooooot 2021-02-03 01:48:53 +02:00
a9ced91741 deploy 2021-02-03 01:47:50 +02:00
e374562189 Bump version: 0.24.0.4 → 0.24.0.5 2021-02-03 01:09:51 +02:00
e0b64e09b1 bugfix 2021-02-03 01:09:43 +02:00
c51337d249 Bump version: 0.24.0.3 → 0.24.0.4 2021-02-03 01:07:21 +02:00
f4896e0b79 Merge branch 'bugfix-v0.23.4'
# Conflicts:
#	erepublik/__init__.py
#	setup.cfg
#	setup.py
2021-02-03 00:50:03 +02:00
13f5c673ad Migrate Citizen.write_log to self.logger.warning where applicable 2021-02-03 00:48:50 +02:00
e95ffbd505 cleanup 2021-02-03 00:48:13 +02:00
5e638806b5 Switch from manual log creation to Python logging 2021-02-03 00:47:52 +02:00
7860fa3669 Bump version: 0.23.4.15 → 0.23.4.16 2021-02-02 23:57:58 +02:00
e38f603e8b Division switch bugfix for option to switch side 2021-02-02 23:57:54 +02:00
0e1c42a8fb Bump version: 0.23.4.14 → 0.23.4.15 2021-02-02 23:52:38 +02:00
ddc412b348 accesspoint update to change side in RW 2021-02-02 23:52:32 +02:00
18 changed files with 2820 additions and 1860 deletions

View File

@ -21,6 +21,7 @@ insert_final_newline = false
indent_style = tab
[*.py]
max_line_length = 240
line_length=120
multi_line_output=0
balanced_wrapping=True

View File

@ -10,4 +10,4 @@ repos:
- id: check-added-large-files
default_language_version:
python: python3.7
python: python3.8

View File

@ -52,12 +52,13 @@ clean-test: ## remove test and coverage artifacts
rm -fr .pytest_cache
lint: ## check style with flake8
black erepublik tests
flake8 erepublik tests
test: ## run tests quickly with the default Python
python setup.py test
python -m unittest
coverage: ## check code coverage quickly with the default Python
coverage: lint ## check code coverage quickly with the default Python
coverage run --source erepublik setup.py test
coverage report -m
coverage html

View File

@ -36,14 +36,6 @@ erepublik.constants module
:undoc-members:
:show-inheritance:
erepublik.types module
----------------------
.. automodule:: erepublik.types
:members:
:undoc-members:
:show-inheritance:
erepublik.utils module
----------------------
@ -52,6 +44,14 @@ erepublik.utils module
:undoc-members:
:show-inheritance:
erepublik.ws module
-------------------
.. automodule:: erepublik.ws
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -3,10 +3,9 @@
"""Top-level package for eRepublik script."""
__author__ = """Eriks Karls"""
__email__ = 'eriks@72.lv'
__version__ = '0.24.0.3'
__email__ = "eriks@72.lv"
__version__ = "0.25.1.4"
from erepublik import classes, constants, utils
from erepublik.citizen import Citizen
__all__ = ["classes", "utils", "Citizen", 'constants']
__all__ = ["Citizen", "__version__"]

212
erepublik/_logging.py Normal file
View File

@ -0,0 +1,212 @@
import base64
import datetime
import inspect
import logging
import os
import sys
import weakref
from logging import LogRecord, handlers
from pathlib import Path
from typing import Any, Dict, Union
import requests
from erepublik.classes import Reporter
from erepublik.constants import erep_tz
from erepublik.utils import json, json_dumps, json_loads, slugify
class ErepublikFileHandler(handlers.TimedRotatingFileHandler):
_file_path: Path
def __init__(self, filename: str = "log/erepublik.log", *args, **kwargs):
log_path = Path(filename)
self._file_path = log_path
log_path.parent.mkdir(parents=True, exist_ok=True)
at_time = erep_tz.localize(datetime.datetime.now()).replace(hour=0, minute=0, second=0, microsecond=0)
kwargs.update(atTime=at_time)
super().__init__(filename, when="d", *args, **kwargs)
def doRollover(self) -> None:
self._file_path.parent.mkdir(parents=True, exist_ok=True)
super().doRollover()
def emit(self, record: LogRecord) -> None:
self._file_path.parent.mkdir(parents=True, exist_ok=True)
super().emit(record)
class ErepublikLogConsoleHandler(logging.StreamHandler):
def __init__(self, *_):
super().__init__(sys.stdout)
class ErepublikFormatter(logging.Formatter):
"""override logging.Formatter to use an aware datetime object"""
dbg_fmt = "[%(asctime)s] DEBUG: %(module)s: %(lineno)d: %(msg)s"
info_fmt = "[%(asctime)s] %(msg)s"
default_fmt = "[%(asctime)s] %(levelname)s: %(msg)s"
def converter(self, timestamp: Union[int, float]) -> datetime.datetime:
return datetime.datetime.fromtimestamp(timestamp).astimezone(erep_tz)
def format(self, record: logging.LogRecord) -> str:
"""
Format the specified record as text.
The record's attribute dictionary is used as the operand to a
string formatting operation which yields the returned string.
Before formatting the dictionary, a couple of preparatory steps
are carried out. The message attribute of the record is computed
using LogRecord.getMessage(). If the formatting string uses the
time (as determined by a call to usesTime(), formatTime() is
called to format the event time. If there is exception information,
it is formatted using formatException() and appended to the message.
"""
if record.levelno == logging.DEBUG:
self._fmt = self.dbg_fmt
elif record.levelno == logging.INFO:
self._fmt = self.info_fmt
else:
self._fmt = self.default_fmt
self._style = logging.PercentStyle(self._fmt)
record.message = record.getMessage()
if self.usesTime():
record.asctime = self.formatTime(record, self.datefmt)
s = self.formatMessage(record)
if record.exc_info:
# Cache the traceback text to avoid converting it multiple times
# (it's constant anyway)
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)
return s
def formatTime(self, record, datefmt=None):
dt = self.converter(record.created)
if datefmt:
s = dt.strftime(datefmt)
else:
s = dt.strftime("%Y-%m-%d %H:%M:%S")
return s
def usesTime(self):
return self._style.usesTime()
class ErepublikErrorHTTTPHandler(handlers.HTTPHandler):
def __init__(self, reporter: Reporter):
logging.Handler.__init__(self, level=logging.ERROR)
self._reporter = weakref.ref(reporter)
self.host = "erep.lv"
self.url = "/ebot/error/"
self.method = "POST"
self.secure = True
self.credentials = (str(reporter.citizen_id), reporter.key)
self.context = None
@property
def reporter(self):
return self._reporter()
def _get_last_response(self) -> Dict[str, str]:
response = self.reporter.citizen.r
url = response.url
last_index = url.index("?") if "?" in url else len(response.url)
name = slugify(response.url[len(self.reporter.citizen.url) : last_index])
html = response.text
try:
json_loads(html)
ext = "json"
except json.decoder.JSONDecodeError:
ext = "html"
try:
resp_time = (
datetime.datetime.strptime(response.headers.get("date"), "%a, %d %b %Y %H:%M:%S %Z")
.replace(tzinfo=datetime.timezone.utc)
.astimezone(erep_tz)
.strftime("%F_%H-%M-%S")
)
except: # noqa
resp_time = slugify(response.headers.get("date"))
return dict(
name=f"{resp_time}_{name}.{ext}", content=html.encode("utf-8"), mimetype="application/json" if ext == "json" else "text/html"
)
def _get_local_vars(self) -> str:
trace = inspect.trace()
local_vars = {}
if trace:
local_vars = trace[-1][0].f_locals
if local_vars.get("__name__") == "__main__":
local_vars.update(
commit_id=local_vars.get("COMMIT_ID"),
interactive=local_vars.get("INTERACTIVE"),
version=local_vars.get("__version__"),
config=local_vars.get("CONFIG"),
)
else:
stack = inspect.stack()
report_error_caller_found = False
for frame in stack:
if report_error_caller_found:
local_vars = frame.frame.f_locals
break
if "report_error" in str(frame.frame):
report_error_caller_found = True
if "state_thread" in local_vars:
local_vars.pop("state_thread", None)
from erepublik import Citizen
if isinstance(local_vars.get("self"), Citizen):
local_vars["self"] = repr(local_vars["self"])
if isinstance(local_vars.get("player"), Citizen):
local_vars["player"] = repr(local_vars["player"])
if isinstance(local_vars.get("citizen"), Citizen):
local_vars["citizen"] = repr(local_vars["citizen"])
return json_dumps(local_vars)
def _get_instance_json(self) -> str:
if self.reporter:
return self.reporter.citizen.to_json(False)
return ""
def mapLogRecord(self, record: logging.LogRecord) -> Dict[str, Any]:
data = super().mapLogRecord(record)
# Log last response
resp = self._get_last_response()
files = [
("file", (resp.get("name"), resp.get("content"), resp.get("mimetype"))),
]
files += list(("file", (f, open(f"log/{f}", "rb"))) for f in os.listdir("log") if f.endswith(".log"))
local_vars_json = self._get_local_vars()
if local_vars_json:
files.append(("file", ("local_vars.json", local_vars_json, "application/json")))
instance_json = self._get_instance_json()
if instance_json:
files.append(("file", ("instance.json", instance_json, "application/json")))
data.update(files=files)
return data
def emit(self, record):
"""
Emit a record.
Send the record to the Web server as a percent-encoded dictionary
"""
try:
proto = "https" if self.secure else "http"
u, p = self.credentials
s = "Basic " + base64.b64encode(f"{u}:{p}".encode("utf-8")).strip().decode("ascii")
headers = {"Authorization": s}
data = self.mapLogRecord(record)
files = data.pop("files") if "files" in data else None
requests.post(f"{proto}://{self.host}{self.url}", headers=headers, data=data, files=files)
except Exception:
self.handleError(record)

View File

@ -1,39 +1,21 @@
import datetime
import hashlib
import random
import time
from typing import Any, Dict, List, Mapping, Union
from requests import Response, Session
from requests.exceptions import ConnectionError
from requests_toolbelt.utils import dump
from . import constants, utils
from erepublik import constants, utils
__all__ = ['SlowRequests', 'CitizenAPI']
__all__ = ["SlowRequests", "CitizenAPI"]
class SlowRequests(Session):
last_time: datetime.datetime
timeout: datetime.timedelta = datetime.timedelta(milliseconds=500)
_uas: List[str] = [
# 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/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 (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/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',
# 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: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 (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:82.0) Gecko/20100101 Firefox/82.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0',
]
debug: bool = False
def __init__(self, proxies: Dict[str, str] = None, user_agent: str = None):
@ -41,21 +23,33 @@ class SlowRequests(Session):
if proxies:
self.proxies = proxies
if user_agent is None:
user_agent = random.choice(self._uas)
user_agent = random.choice(self.get_random_user_agent())
self.request_log_name = utils.get_file(utils.now().strftime("debug/requests_%Y-%m-%d.log"))
self.last_time = utils.now()
self.headers.update({'User-Agent': user_agent})
self.headers.update({"User-Agent": user_agent})
self.hooks["response"] = [self._log_response]
@property
def as_dict(self):
return dict(last_time=self.last_time, timeout=self.timeout, cookies=self.cookies.get_dict(), debug=self.debug,
user_agent=self.headers['User-Agent'], request_log_name=self.request_log_name, proxies=self.proxies)
return dict(
last_time=self.last_time,
timeout=self.timeout,
cookies=self.cookies.get_dict(),
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):
self._slow_down_requests()
self._log_request(url, method, **kwargs)
resp = super().request(method, url, *args, **kwargs)
self._log_response(url, resp)
try:
resp = super().request(method, url, *args, **kwargs)
except ConnectionError:
time.sleep(1)
return self.request(method, url, *args, **kwargs)
# self._log_response(resp)
return resp
def _slow_down_requests(self):
@ -68,46 +62,81 @@ class SlowRequests(Session):
def _log_request(self, url, method, data=None, json=None, params=None, **kwargs):
if self.debug:
args = {}
kwargs.pop('allow_redirects', None)
kwargs.pop("allow_redirects", None)
if kwargs:
args.update({'kwargs': kwargs})
args.update({"kwargs": kwargs})
if data:
args.update({'data': data})
args.update({"data": data})
if json:
args.update({'json': json})
args.update({"json": json})
if params:
args.update({'params': params})
args.update({"params": params})
body = f"[{utils.now().strftime('%F %T')}]\tURL: '{url}'\tMETHOD: {method}\tARGS: {args}\n"
with open(self.request_log_name, 'ab') as file:
with open(self.request_log_name, "ab") as file:
file.write(body.encode("UTF-8"))
pass
def _log_response(self, url, resp, redirect: bool = False):
from erepublik import Citizen
def _log_response(self, response: Response, *args, **kwargs):
redirect = kwargs.get("redirect")
url = response.request.url
if self.debug:
if resp.history and not redirect:
for hist_resp in resp.history:
self._log_request(hist_resp.request.url, 'REDIRECT')
self._log_response(hist_resp.request.url, hist_resp, redirect=True)
if response.history and not redirect:
for hist_resp in response.history:
self._log_request(hist_resp.request.url, "REDIRECT")
self._log_response(hist_resp, redirect=True)
fd_path = 'debug/requests'
fd_time = self.last_time.strftime('%Y/%m/%d/%H-%M-%S')
fd_name = utils.slugify(url[len(Citizen.url):])
fd_extra = '_REDIRECT' if redirect else ""
fd_path = "debug/requests"
fd_time = self.last_time.strftime("%Y/%m/%d/%H-%M-%S")
fd_name = utils.slugify(url[len(CitizenBaseAPI.url) :])
fd_extra = "_REDIRECT" if redirect else ""
try:
utils.json.loads(resp.text)
fd_ext = 'json'
utils.json.loads(response.text)
fd_ext = "json"
except utils.json.JSONDecodeError:
fd_ext = 'html'
fd_ext = "html"
filename = f'{fd_path}/{fd_time}_{fd_name}{fd_extra}.{fd_ext}'
utils.write_file(filename, resp.text)
pass
filename = f"{fd_path}/{fd_time}_{fd_name}{fd_extra}.{fd_ext}"
utils.write_file(filename, response.text)
if not redirect:
data = dump.dump_all(response)
utils.write_file(f"debug/dumps/{fd_time}_{fd_name}{fd_extra}.{fd_ext}.dump", data.decode("utf8"))
@staticmethod
def get_random_user_agent() -> str:
windows_x64 = "Windows NT 10.0; Win64; x64"
linux_x64 = "X11; Linux x86_64"
android_11 = "Android 11; Mobile"
android_10 = "Android 10; Mobile"
android_9 = "Android 9; Mobile"
firefox_tmplt = "Mozilla/5.0 ({osystem}; rv:{version}.0) Gecko/20100101 Firefox/{version}.0"
ff_version = range(85, 92)
chrome_tmplt = "Mozilla/5.0 ({osystem}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36"
chrome_version = [
"85.0.4183.121",
"86.0.4240.183",
"87.0.4280.141",
"88.0.4324.182",
"89.0.4389.128",
"90.0.4430.18",
"91.0.4472.73",
"92.0.4515.14",
]
uas = []
for osystem in [windows_x64, linux_x64, android_9, android_10, android_11]:
for version in ff_version:
uas.append(firefox_tmplt.format(osystem=osystem, version=version))
for version in chrome_version:
uas.append(chrome_tmplt.format(osystem=osystem, version=version))
return random.choice(uas)
class CitizenBaseAPI:
@ -116,7 +145,7 @@ class CitizenBaseAPI:
token: str
def __init__(self):
""" Class for unifying eRepublik known endpoints and their required/optional parameters """
"""Class for unifying eRepublik known endpoints and their required/optional parameters"""
self._req = SlowRequests()
self.token = ""
@ -134,60 +163,90 @@ class CitizenBaseAPI:
return self.get(self.url)
def set_socks_proxy(self, host: str, port: int, username: str = None, password: str = None):
url = f'socks5://{username}:{password}@{host}:{port}' if username and password else f'socks5://{host}:{port}'
url = f"socks5://{username}:{password}@{host}:{port}" if username and password else f"socks5://{host}:{port}"
self._req.proxies = dict(http=url, https=url)
def set_http_proxy(self, host: str, port: int, username: str = None, password: str = None):
url = f'http://{username}:{password}@{host}:{port}' if username and password else f'socks5://{host}:{port}'
url = f"http://{username}:{password}@{host}:{port}" if username and password else f"http://{host}:{port}"
self._req.proxies = dict(http=url)
def _get_main_session_captcha(self) -> Response:
return self.get(f'{self.url}/main/sessionCaptcha')
return self.get(f"{self.url}/main/sessionCaptcha")
def _get_main_session_unlock_popup(self) -> Response:
return self.get(f'{self.url}/main/sessionUnlockPopup')
return self.get(f"{self.url}/main/sessionUnlockPopup")
def _post_main_session_get_challenge(self, captcha_id: int) -> Response:
env = dict(l=['tets', ], s=[], c=["l_chathwe", "l_chatroom"], m=0)
def _post_main_session_get_challenge(self, captcha_id: int, image_id: str = "") -> Response:
c = [cookie.name for cookie in self._req.cookies if not cookie.has_nonstandard_attr("HttpOnly")]
env = dict(l=["tets"], s=[], c=c, m=0)
data = dict(_token=self.token, captchaId=captcha_id, env=utils.b64json(env))
return self.post(f'{self.url}/main/sessionGetChallenge', data=data)
if image_id:
data.update(imageId=image_id, isRefresh=True)
return self.post(f"{self.url}/main/sessionGetChallenge", data=data)
def _post_main_session_unlock(
self, captcha: int, image: str, challenge: str, coords: List[Dict[str, int]], src: str
self, captcha_id: int, image_id: str, challenge_id: str, coords: List[Dict[str, int]], src: str
) -> Response:
env = dict(l=['tets', ], s=[], c=["l_chathwe", "l_chatroom"], m=0)
data = dict(_token=self.token, captchaId=captcha, imageId=image, challengeId=challenge,
clickMatrix=coords, isMobile=0, env=utils.b64json(env), src=src)
return self.post(f'{self.url}/main/sessionUnlock', data=data)
c = [cookie.name for cookie in self._req.cookies if not cookie.has_nonstandard_attr("HttpOnly")]
env = dict(l=["tets"], s=[], c=c, m=0)
cookies = dict(
sh=hashlib.sha256(",".join(env["l"] + env["s"]).encode("utf8")).hexdigest(),
ch=hashlib.sha256(",".join(env["c"]).encode("utf8")).hexdigest(),
)
cookie_kwargs = dict(
expires=int(time.time()) + 120, path="/en/main/sessionUnlock", domain=".www.erepublik.com", secure=True, rest={"HttpOnly": True}
)
self._req.cookies.set("sh", cookies["sh"], **cookie_kwargs)
self._req.cookies.set("ch", cookies["ch"], **cookie_kwargs)
b64_env = utils.b64json(env)
data = dict(
_token=self.token,
captchaId=captcha_id,
imageId=image_id,
challengeId=challenge_id,
clickMatrix=utils.json_dumps(coords).replace(" ", ""),
isMobile=0,
env=b64_env,
src=src,
)
return self.post(
f"{self.url}/main/sessionUnlock",
data=data,
json=data,
headers={"X-Requested-With": "XMLHttpRequest", "Referer": "https://www.erepublik.com/en"},
)
def _post_energy_refill_get_inventory(self):
return self.post(f"{self.url}/economy/energyRefill-getInventory", data={"_token": self.token})
class ErepublikAnniversaryAPI(CitizenBaseAPI):
def _post_main_collect_anniversary_reward(self) -> Response:
return self.post(f"{self.url}/main/collect-anniversary-reward", data={'_token': self.token})
return self.post(f"{self.url}/main/collect-anniversary-reward", data={"_token": self.token})
# 12th anniversary endpoints
def _get_anniversary_quest_data(self) -> Response:
return self.get(f"{self.url}/main/anniversaryQuestData")
def _post_map_rewards_unlock(self, node_id: int) -> Response:
data = {'nodeId': node_id, '_token': self.token}
data = {"nodeId": node_id, "_token": self.token}
return self.post(f"{self.url}/main/map-rewards-unlock", data=data)
def _post_map_rewards_speedup(self, node_id: int, currency_amount: int) -> Response:
data = {'nodeId': node_id, '_token': self.token, 'currencyCost': currency_amount}
data = {"nodeId": node_id, "_token": self.token, "currencyCost": currency_amount}
return self.post(f"{self.url}/main/map-rewards-speedup", data=data)
def _post_map_rewards_claim(self, node_id: int, extra: bool = False) -> Response:
data = {'nodeId': node_id, '_token': self.token}
data = {"nodeId": node_id, "_token": self.token}
if extra:
data['claimExtra'] = 1
data["claimExtra"] = 1
return self.post(f"{self.url}/main/map-rewards-claim", data=data)
def _post_main_wheel_of_fortune_spin(self, cost) -> Response:
return self.post(f"{self.url}/main/wheeloffortune-spin", data={'_token': self.token, '_currentCost': cost})
return self.post(f"{self.url}/main/wheeloffortune-spin", data={"_token": self.token, "_currentCost": cost})
def _post_main_wheel_of_fortune_build(self) -> Response:
return self.post(f"{self.url}/main/wheeloffortune-build", data={'_token': self.token})
return self.post(f"{self.url}/main/wheeloffortune-build", data={"_token": self.token})
class ErepublikArticleAPI(CitizenBaseAPI):
@ -200,13 +259,13 @@ class ErepublikArticleAPI(CitizenBaseAPI):
def _post_main_article_comments(self, article_id: int, page: int = 1) -> Response:
data = dict(_token=self.token, articleId=article_id, page=page)
if page:
data.update({'page': page})
data.update({"page": page})
return self.post(f"{self.url}/main/articleComments", data=data)
def _post_main_article_comments_create(self, message: str, article_id: int, parent: int = 0) -> Response:
data = dict(_token=self.token, message=message, articleId=article_id)
if parent:
data.update({'parentId': parent})
data.update({"parentId": parent})
return self.post(f"{self.url}/main/articleComments/create", data=data)
def _post_main_donate_article(self, article_id: int, amount: int) -> Response:
@ -214,8 +273,7 @@ class ErepublikArticleAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/donate-article", data=data)
def _post_main_write_article(self, title: str, content: str, country_id: int, kind_id: int) -> Response:
data = dict(_token=self.token, article_name=title, article_body=content, article_location=country_id,
article_category=kind_id)
data = dict(_token=self.token, article_name=title, article_body=content, article_location=country_id, article_category=kind_id)
return self.post(f"{self.url}/main/write-article", data=data)
def _post_main_vote_article(self, article_id: int) -> Response:
@ -225,13 +283,12 @@ class ErepublikArticleAPI(CitizenBaseAPI):
class ErepublikCompanyAPI(CitizenBaseAPI):
def _post_economy_assign_to_holding(self, factory_id: int, holding_id: int) -> Response:
data = dict(_token=self.token, factoryId=factory_id, action='assign', holdingCompanyId=holding_id)
data = dict(_token=self.token, factoryId=factory_id, action="assign", holdingCompanyId=holding_id)
return self.post(f"{self.url}/economy/assign-to-holding", data=data)
def _post_economy_create_company(self, industry_id: int, building_type: int = 1) -> Response:
data = {'_token': self.token, "company[industry_id]": industry_id, "company[building_type]": building_type}
return self.post(f"{self.url}/economy/create-company", data=data,
headers={'Referer': f"{self.url}/economy/create-company"})
data = {"_token": self.token, "company[industry_id]": industry_id, "company[building_type]": building_type}
return self.post(f"{self.url}/economy/create-company", data=data, headers={"Referer": f"{self.url}/economy/create-company"})
def _get_economy_inventory_items(self) -> Response:
return self.get(f"{self.url}/economy/inventory-items/")
@ -248,39 +305,43 @@ class ErepublikCompanyAPI(CitizenBaseAPI):
data["grounds[%i][id]" % idx] = tg_id
data["grounds[%i][train]" % idx] = 1
if data:
data['_token'] = self.token
data["_token"] = self.token
return self.post(f"{self.url}/economy/train", data=data)
def _post_economy_upgrade_company(self, factory: int, level: int, pin: str = None) -> Response:
data = dict(_token=self.token, type='upgrade', companyId=factory, level=level, pin="" if pin is None else pin)
data = dict(_token=self.token, type="upgrade", companyId=factory, level=level, pin="" if pin is None else pin)
return self.post(f"{self.url}/economy/upgrade-company", data=data)
def _post_economy_work(self, action_type: str, wam: List[int] = None, employ: Dict[int, int] = None) -> Response:
data: Dict[str, Union[int, str]] = dict(action_type=action_type, _token=self.token)
if action_type == 'production':
if action_type == "production":
if employ is None:
employ = {}
if wam is None:
wam = []
max_idx = 0
for company_id in sorted(wam or []):
data.update({
f"companies[{max_idx}][id]": company_id,
f"companies[{max_idx}][employee_works]": employ.pop(company_id, 0),
f"companies[{max_idx}][own_work]": 1
})
data.update(
{
f"companies[{max_idx}][id]": company_id,
f"companies[{max_idx}][employee_works]": employ.pop(company_id, 0),
f"companies[{max_idx}][own_work]": 1,
}
)
max_idx += 1
for company_id in sorted(employ or []):
data.update({
f"companies[{max_idx}][id]": company_id,
f"companies[{max_idx}][employee_works]": employ.pop(company_id, 0),
f"companies[{max_idx}][own_work]": 0
})
data.update(
{
f"companies[{max_idx}][id]": company_id,
f"companies[{max_idx}][employee_works]": employ.pop(company_id, 0),
f"companies[{max_idx}][own_work]": 0,
}
)
max_idx += 1
return self.post(f"{self.url}/economy/work", data=data)
def _post_economy_work_overtime(self) -> Response:
data = dict(action_type='workOvertime', _token=self.token)
data = dict(action_type="workOvertime", _token=self.token)
return self.post(f"{self.url}/economy/workOvertime", data=data)
def _post_economy_job_market_apply(self, citizen_id: int, salary: float) -> Response:
@ -288,29 +349,28 @@ class ErepublikCompanyAPI(CitizenBaseAPI):
return self.post(f"{self.url}/economy/job-market-apply", data=data)
def _post_economy_resign(self) -> Response:
return self.post(f"{self.url}/economy/resign",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={'_token': self.token, 'action_type': 'resign'})
return self.post(
f"{self.url}/economy/resign",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"_token": self.token, "action_type": "resign"},
)
def _post_economy_sell_company(self, factory_id: int, pin: int = None, sell: bool = True) -> Response:
data = dict(_token=self.token, pin="" if pin is None else pin)
if sell:
data.update({'sell': 'sell'})
data.update({"sell": "sell"})
else:
data.update({'dissolve': factory_id})
return self.post(f"{self.url}/economy/sell-company/{factory_id}",
data=data, headers={'Referer': self.url})
data.update({"dissolve": factory_id})
return self.post(f"{self.url}/economy/sell-company/{factory_id}", data=data, headers={"Referer": self.url})
class ErepublikCountryAPI(CitizenBaseAPI):
def _get_country_military(self, country_name: str) -> Response:
return self.get(f"{self.url}/country/military/{country_name}")
def _post_main_country_donate(self, country_id: int, action: str, value: Union[int, float],
quality: int = None) -> Response:
def _post_main_country_donate(self, country_id: int, action: str, value: Union[int, float], quality: int = None) -> Response:
data = dict(countryId=country_id, action=action, _token=self.token, value=value, quality=quality)
return self.post(f"{self.url}/main/country-donate", data=data,
headers={'Referer': f"{self.url}/country/economy/Latvia"})
return self.post(f"{self.url}/main/country-donate", data=data, headers={"Referer": f"{self.url}/country/economy/Latvia"})
class ErepublikEconomyAPI(CitizenBaseAPI):
@ -332,20 +392,20 @@ class ErepublikEconomyAPI(CitizenBaseAPI):
return self.post(f"{self.url}/economy/activateBooster", data=data)
def _post_economy_activate_house(self, quality: int) -> Response:
data = dict(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)
def _post_economy_donate_items_action(self, citizen_id: int, amount: int, industry: int,
quality: int) -> Response:
def _post_economy_donate_items_action(self, citizen_id: int, amount: int, industry: int, quality: int) -> Response:
data = dict(citizen_id=citizen_id, amount=amount, industry_id=industry, quality=quality, _token=self.token)
return self.post(f"{self.url}/economy/donate-items-action", data=data,
headers={'Referer': f"{self.url}/economy/donate-items/{citizen_id}"})
return self.post(
f"{self.url}/economy/donate-items-action", data=data, headers={"Referer": f"{self.url}/economy/donate-items/{citizen_id}"}
)
def _post_economy_donate_money_action(self, citizen_id: int, amount: float = 0.0,
currency: int = 62) -> Response:
def _post_economy_donate_money_action(self, citizen_id: int, amount: float = 0.0, currency: int = 62) -> Response:
data = dict(citizen_id=citizen_id, _token=self.token, currency_id=currency, amount=amount)
return self.post(f"{self.url}/economy/donate-money-action", data=data,
headers={'Referer': f"{self.url}/economy/donate-money/{citizen_id}"})
return self.post(
f"{self.url}/economy/donate-money-action", data=data, headers={"Referer": f"{self.url}/economy/donate-money/{citizen_id}"}
)
def _post_economy_exchange_purchase(self, amount: float, currency: int, offer: int) -> Response:
data = dict(_token=self.token, amount=amount, currencyId=currency, offerId=offer)
@ -356,46 +416,56 @@ class ErepublikEconomyAPI(CitizenBaseAPI):
return self.post(f"{self.url}/economy/exchange/retrieve/", data=data)
def _post_economy_game_tokens_market(self, action: str) -> Response:
assert action in ['retrieve', ]
assert action in [
"retrieve",
]
data = dict(_token=self.token, action=action)
return self.post(f"{self.url}/economy/gameTokensMarketAjax", data=data)
def _post_economy_marketplace(self, country: int, industry: int, quality: int,
order_asc: bool = True) -> Response:
data = dict(countryId=country, industryId=industry, quality=quality, ajaxMarket=1,
orderBy='price_asc' if order_asc else 'price_desc', _token=self.token)
def _post_economy_marketplace(self, country: int, industry: int, quality: int, order_asc: bool = True) -> Response:
data = dict(
countryId=country,
industryId=industry,
quality=quality,
ajaxMarket=1,
orderBy="price_asc" if order_asc else "price_desc",
_token=self.token,
)
return self.post(f"{self.url}/economy/marketplaceAjax", data=data)
def _post_economy_marketplace_actions(self, action: str, **kwargs) -> Response:
if action == 'buy':
data = dict(_token=self.token, offerId=kwargs['offer'], amount=kwargs['amount'],
orderBy='price_asc', currentPage=1, buyAction=1)
elif action == 'sell':
data = dict(_token=self.token, countryId=kwargs['country_id'], price=kwargs['price'],
industryId=kwargs['industry'], quality=kwargs['quality'], amount=kwargs['amount'],
sellAction='postOffer')
elif action == 'delete':
data = dict(_token=self.token, offerId=kwargs['offer_id'], sellAction='deleteOffer')
if action == "buy":
data = dict(
_token=self.token, offerId=kwargs["offer"], amount=kwargs["amount"], orderBy="price_asc", currentPage=1, buyAction=1
)
elif action == "sell":
data = dict(
_token=self.token,
countryId=kwargs["country_id"],
price=kwargs["price"],
industryId=kwargs["industry"],
quality=kwargs["quality"],
amount=kwargs["amount"],
sellAction="postOffer",
)
elif action == "delete":
data = dict(_token=self.token, offerId=kwargs["offer_id"], sellAction="deleteOffer")
else:
raise ValueError(f"Action '{action}' is not supported! Only 'buy/sell/delete' actions are available")
return self.post(f"{self.url}/economy/marketplaceActions", data=data)
class ErepublikLeaderBoardAPI(CitizenBaseAPI):
def _get_main_leaderboards_damage_aircraft_rankings(self, country_id: int, weeks: int = 0,
mu_id: int = 0) -> Response: # noqa
def _get_main_leaderboards_damage_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-damage-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0")
def _get_main_leaderboards_damage_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0,
div: int = 0) -> Response: # noqa
def _get_main_leaderboards_damage_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-damage-rankings/{country_id}/{weeks}/{mu_id}/{div}")
def _get_main_leaderboards_kills_aircraft_rankings(self, country_id: int, weeks: int = 0,
mu_id: int = 0) -> Response: # noqa
def _get_main_leaderboards_kills_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-kills-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0")
def _get_main_leaderboards_kills_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0,
div: int = 0) -> Response: # noqa
def _get_main_leaderboards_kills_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-kills-rankings/{country_id}/{weeks}/{mu_id}/{div}")
@ -403,7 +473,7 @@ class ErepublikLocationAPI(CitizenBaseAPI):
def _get_main_city_data_residents(self, city_id: int, page: int = 1, params: Mapping[str, Any] = None) -> Response:
if params is None:
params = {}
return self.get(f"{self.url}/main/city-data/{city_id}/residents", params={'currentPage': page, **params})
return self.get(f"{self.url}/main/city-data/{city_id}/residents", params={"currentPage": page, **params})
class ErepublikMilitaryAPI(CitizenBaseAPI):
@ -414,7 +484,7 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
return self.get(f"{self.url}/military/battlefield-choose-side/{battle_id}/{side_id}")
def _get_military_show_weapons(self, battle_id: int) -> Response:
return self.get(f"{self.url}/military/show-weapons", params={'_token': self.token, 'battleId': battle_id})
return self.get(f"{self.url}/military/show-weapons", params={"_token": self.token, "battleId": battle_id})
def _get_military_campaigns(self) -> Response:
return self.get(f"{self.url}/military/campaigns-new/")
@ -426,7 +496,7 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
return self.get(f"{self.url}/military/campaignsJson/citizen")
def _get_military_unit_data(self, unit_id: int, **kwargs) -> Response:
params = {'groupId': unit_id, 'panel': 'members', **kwargs}
params = {"groupId": unit_id, "panel": "members", **kwargs}
return self.get(f"{self.url}/military/military-unit-data/", params=params)
def _post_main_activate_battle_effect(self, battle_id: int, kind: str, citizen_id: int) -> Response:
@ -437,8 +507,10 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
data = dict(_token=self.token, sideCountryId=side_id, battleId=battle_id)
return self.post(f"{self.url}/main/battlefieldTravel", data=data)
def _post_main_battlefield_change_division(self, battle_id: int, division_id: int) -> Response:
def _post_main_battlefield_change_division(self, battle_id: int, division_id: int, side_id: int = None) -> Response:
data = dict(_token=self.token, battleZoneId=division_id, battleId=battle_id)
if side_id is not None:
data.update(sideCountryId=side_id)
return self.post(f"{self.url}/main/battlefieldTravel", data=data)
def _get_wars_show(self, war_id: int) -> Response:
@ -448,22 +520,32 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
data = dict(type=kind, quality=quality, duration=duration, battleId=battle_id, _token=self.token)
return self.post(f"{self.url}/military/fight-activateBooster", data=data)
def _post_military_change_weapon(self, battle_id: int, battle_zone: int, weapon_level: int, ) -> Response:
def _post_military_change_weapon(
self,
battle_id: int,
battle_zone: int,
weapon_level: int,
) -> Response:
data = dict(battleId=battle_id, _token=self.token, battleZoneId=battle_zone, customizationLevel=weapon_level)
return self.post(f"{self.url}/military/change-weapon", data=data)
def _post_military_battle_console(self, battle_id: int, action: str, page: int = 1, **kwargs) -> Response:
data = dict(battleId=battle_id, action=action, _token=self.token)
if action == 'battleStatistics':
data.update(round=kwargs['round_id'], zoneId=kwargs['round_id'], leftPage=page, rightPage=page,
division=kwargs['division'], type=kwargs.get('type', 'damage'), )
elif action == 'warList':
if action == "battleStatistics":
data.update(
round=kwargs["round_id"],
zoneId=kwargs["round_id"],
leftPage=page,
rightPage=page,
division=kwargs["division"],
type=kwargs.get("type", "damage"),
)
elif action == "warList":
data.update(page=page)
return self.post(f"{self.url}/military/battle-console", data=data)
def _post_military_deploy_bomb(self, battle_id: int, division_id: int, side_id: int, bomb_id: int) -> Response:
data = dict(battleId=battle_id, battleZoneId=division_id, sideId=side_id, sideCountryId=side_id,
bombId=bomb_id, _token=self.token)
data = dict(battleId=battle_id, battleZoneId=division_id, sideId=side_id, sideCountryId=side_id, bombId=bomb_id, _token=self.token)
return self.post(f"{self.url}/military/deploy-bomb", data=data)
def _post_military_fight_air(self, battle_id: int, side_id: int, zone_id: int) -> Response:
@ -485,10 +567,20 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
def _post_fight_deploy_start_deploy(
self, battle_id: int, side_id: int, battle_zone_id: int, energy: int, weapon: int, **kwargs
) -> Response:
data = dict(_token=self.token, battleId=battle_id, battleZoneId=battle_zone_id, sideCountryId=side_id,
weaponQuality=weapon, totalEnergy=energy, **kwargs)
data = dict(
_token=self.token,
battleId=battle_id,
battleZoneId=battle_zone_id,
sideCountryId=side_id,
weaponQuality=weapon,
totalEnergy=energy,
**kwargs,
)
return self.post(f"{self.url}/military/fightDeploy-startDeploy", data=data)
def _post_military_fight_deploy_deploy_report_data(self, deployment_id: int) -> Response:
data = dict(_token=self.token, deploymentId=deployment_id)
return self.post(f"{self.url}/military/fightDeploy-deployReportData", data=data)
class ErepublikPoliticsAPI(CitizenBaseAPI):
@ -509,8 +601,7 @@ class ErepublikPoliticsAPI(CitizenBaseAPI):
return self.get(f"{self.url}/main/presidential-elections/{country_id}/{timestamp}")
def _post_propose_president_candidate(self, party_slug: str, citizen_id: int) -> Response:
return self.post(f"{self.url}/propose-president-candidate/{party_slug}",
data=dict(_token=self.token, citizen=citizen_id))
return self.post(f"{self.url}/propose-president-candidate/{party_slug}", data=dict(_token=self.token, citizen=citizen_id))
def _get_auto_propose_president_candidate(self, party_slug: str) -> Response:
return self.get(f"{self.url}/auto-propose-president-candidate/{party_slug}")
@ -518,17 +609,15 @@ class ErepublikPoliticsAPI(CitizenBaseAPI):
class ErepublikPresidentAPI(CitizenBaseAPI):
def _post_wars_attack_region(self, war_id: int, region_id: int, region_name: str) -> Response:
data = {'_token': self.token, 'warId': war_id, 'regionName': region_name, 'regionNameConfirm': region_name}
return self.post(f'{self.url}/wars/attack-region/{war_id}/{region_id}', data=data)
data = {"_token": self.token, "warId": war_id, "regionName": region_name, "regionNameConfirm": region_name}
return self.post(f"{self.url}/wars/attack-region/{war_id}/{region_id}", data=data)
def _post_new_war(self, self_country_id: int, attack_country_id: int, debate: str = "") -> Response:
data = dict(requirments=1, _token=self.token, debate=debate,
countryNameConfirm=constants.COUNTRIES[attack_country_id].link)
data = dict(requirments=1, _token=self.token, debate=debate, countryNameConfirm=constants.COUNTRIES[attack_country_id].link)
return self.post(f"{self.url}/{constants.COUNTRIES[self_country_id].link}/new-war", data=data)
def _post_new_donation(self, country_id: int, amount: int, org_name: str, debate: str = "") -> Response:
data = dict(requirments=1, _token=self.token, debate=debate, currency=1, value=amount, commit='Propose',
type_name=org_name)
data = dict(requirments=1, _token=self.token, debate=debate, currency=1, value=amount, commit="Propose", type_name=org_name)
return self.post(f"{self.url}/{constants.COUNTRIES[country_id].link}/new-donation", data=data)
@ -549,10 +638,10 @@ class ErepublikProfileAPI(CitizenBaseAPI):
return self.get(f"{self.url}/main/messages-paginated/{page}")
def _get_main_money_donation_accept(self, donation_id: int) -> Response:
return self.get(f"{self.url}/main/money-donation/accept/{donation_id}", params={'_token': self.token})
return self.get(f"{self.url}/main/money-donation/accept/{donation_id}", params={"_token": self.token})
def _get_main_money_donation_reject(self, donation_id: int) -> Response:
return self.get(f"{self.url}/main/money-donation/reject/{donation_id}", params={'_token': self.token})
return self.get(f"{self.url}/main/money-donation/reject/{donation_id}", params={"_token": self.token})
def _get_main_notifications_ajax_community(self, page: int = 1) -> Response:
return self.get(f"{self.url}/main/notificationsAjax/community/{page}")
@ -572,16 +661,16 @@ class ErepublikProfileAPI(CitizenBaseAPI):
def _post_main_citizen_add_remove_friend(self, citizen: int, add: bool) -> Response:
data = dict(_token=self.token, citizenId=citizen, url="//www.erepublik.com/en/main/citizen-addRemoveFriend")
if add:
data.update({'action': 'addFriend'})
data.update({"action": "addFriend"})
else:
data.update({'action': 'removeFriend'})
data.update({"action": "removeFriend"})
return self.post(f"{self.url}/main/citizen-addRemoveFriend", data=data)
def _post_main_daily_task_reward(self) -> Response:
return self.post(f"{self.url}/main/daily-tasks-reward", data=dict(_token=self.token))
def _post_delete_message(self, msg_id: list) -> Response:
data = {'_token': self.token, "delete_message[]": msg_id}
data = {"_token": self.token, "delete_message[]": msg_id}
return self.post(f"{self.url}/main/messages-delete", data)
def _post_eat(self, color: str) -> Response:
@ -593,37 +682,36 @@ class ErepublikProfileAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/global-alerts/close", data=data)
def _post_forgot_password(self, email: str) -> Response:
data = dict(_token=self.token, email=email, commit='Reset password')
data = dict(_token=self.token, email=email, commit="Reset password")
return self.post(f"{self.url}/forgot-password", data=data)
def _post_login(self, email: str, password: str) -> Response:
data = dict(csrf_token=self.token, citizen_email=email, citizen_password=password, remember='on')
data = dict(csrf_token=self.token, citizen_email=email, citizen_password=password, remember="on")
return self.post(f"{self.url}/login", data=data)
def _post_main_messages_alert(self, notification_ids: List[int]) -> Response:
data = {'_token': self.token, "delete_alerts[]": notification_ids, 'deleteAllAlerts': '1', 'delete': 'Delete'}
data = {"_token": self.token, "delete_alerts[]": notification_ids, "deleteAllAlerts": "1", "delete": "Delete"}
return self.post(f"{self.url}/main/messages-alerts/1", data=data)
def _post_main_notifications_ajax_community(self, notification_ids: List[int], page: int = 1) -> Response:
data = {'_token': self.token, "delete_alerts[]": notification_ids}
data = {"_token": self.token, "delete_alerts[]": notification_ids}
return self.post(f"{self.url}/main/notificationsAjax/community/{page}", data=data)
def _post_main_notifications_ajax_system(self, notification_ids: List[int], page: int = 1) -> Response:
data = {'_token': self.token, "delete_alerts[]": notification_ids}
data = {"_token": self.token, "delete_alerts[]": notification_ids}
return self.post(f"{self.url}/main/notificationsAjax/system/{page}", data=data)
def _post_main_notifications_ajax_report(self, notification_ids: List[int], page: int = 1) -> Response:
data = {'_token': self.token, "delete_alerts[]": notification_ids}
data = {"_token": self.token, "delete_alerts[]": notification_ids}
return self.post(f"{self.url}/main/notificationsAjax/report/{page}", data=data)
def _post_main_messages_compose(self, subject: str, body: str, citizens: List[int]) -> Response:
url_pk = 0 if len(citizens) > 1 else str(citizens[0])
data = dict(citizen_name=",".join([str(x) for x in citizens]),
citizen_subject=subject, _token=self.token, citizen_message=body)
data = dict(citizen_name=",".join([str(x) for x in citizens]), citizen_subject=subject, _token=self.token, citizen_message=body)
return self.post(f"{self.url}/main/messages-compose/{url_pk}", data=data)
def _post_military_group_missions(self) -> Response:
data = dict(action='check', _token=self.token)
data = dict(action="check", _token=self.token)
return self.post(f"{self.url}/military/group-missions", data=data)
def _post_main_weekly_challenge_reward(self, reward_id: int) -> Response:
@ -635,7 +723,7 @@ class ErepublikProfileAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/weekly-challenge-collect-all", data=data)
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)
@ -652,85 +740,95 @@ class ErepublikWallPostAPI(CitizenBaseAPI):
# ## Country
def _post_main_country_comment_retrieve(self, post_id: int) -> Response:
data = {'_token': self.token, 'postId': post_id}
data = {"_token": self.token, "postId": post_id}
return self.post(f"{self.url}/main/country-comment/retrieve/json", data=data)
def _post_main_country_comment_create(self, post_id: int, comment_message: str) -> Response:
data = {'_token': self.token, 'postId': post_id, 'comment_message': comment_message}
data = {"_token": self.token, "postId": post_id, "comment_message": comment_message}
return self.post(f"{self.url}/main/country-comment/create/json", data=data)
def _post_main_country_post_create(self, body: str, post_as: int) -> Response:
data = {'_token': self.token, 'post_message': body, 'post_as': post_as}
data = {"_token": self.token, "post_message": body, "post_as": post_as}
return self.post(f"{self.url}/main/country-post/create/json", data=data)
def _post_main_country_post_retrieve(self) -> Response:
data = {'_token': self.token, 'page': 1, 'switchedFrom': False}
data = {"_token": self.token, "page": 1, "switchedFrom": False}
return self.post(f"{self.url}/main/country-post/retrieve/json", data=data)
# ## Military Unit
def _post_main_military_unit_comment_retrieve(self, post_id: int) -> Response:
data = {'_token': self.token, 'postId': post_id}
data = {"_token": self.token, "postId": post_id}
return self.post(f"{self.url}/main/military-unit-comment/retrieve/json", data=data)
def _post_main_military_unit_comment_create(self, post_id: int, comment_message: str) -> Response:
data = {'_token': self.token, 'postId': post_id, 'comment_message': comment_message}
data = {"_token": self.token, "postId": post_id, "comment_message": comment_message}
return self.post(f"{self.url}/main/military-unit-comment/create/json", data=data)
def _post_main_military_unit_post_create(self, body: str, post_as: int) -> Response:
data = {'_token': self.token, 'post_message': body, 'post_as': post_as}
data = {"_token": self.token, "post_message": body, "post_as": post_as}
return self.post(f"{self.url}/main/military-unit-post/create/json", data=data)
def _post_main_military_unit_post_retrieve(self) -> Response:
data = {'_token': self.token, 'page': 1, 'switchedFrom': False}
data = {"_token": self.token, "page": 1, "switchedFrom": False}
return self.post(f"{self.url}/main/military-unit-post/retrieve/json", data=data)
# ## Party
def _post_main_party_comment_retrieve(self, post_id: int) -> Response:
data = {'_token': self.token, 'postId': post_id}
data = {"_token": self.token, "postId": post_id}
return self.post(f"{self.url}/main/party-comment/retrieve/json", data=data)
def _post_main_party_comment_create(self, post_id: int, comment_message: str) -> Response:
data = {'_token': self.token, 'postId': post_id, 'comment_message': comment_message}
data = {"_token": self.token, "postId": post_id, "comment_message": comment_message}
return self.post(f"{self.url}/main/party-comment/create/json", data=data)
def _post_main_party_post_create(self, body: str) -> Response:
data = {'_token': self.token, 'post_message': body}
data = {"_token": self.token, "post_message": body}
return self.post(f"{self.url}/main/party-post/create/json", data=data)
def _post_main_party_post_retrieve(self) -> Response:
data = {'_token': self.token, 'page': 1, 'switchedFrom': False}
data = {"_token": self.token, "page": 1, "switchedFrom": False}
return self.post(f"{self.url}/main/party-post/retrieve/json", data=data)
# ## Friend's Wall
def _post_main_wall_comment_retrieve(self, post_id: int) -> Response:
data = {'_token': self.token, 'postId': post_id}
data = {"_token": self.token, "postId": post_id}
return self.post(f"{self.url}/main/wall-comment/retrieve/json", data=data)
def _post_main_wall_comment_create(self, post_id: int, comment_message: str) -> Response:
data = {'_token': self.token, 'postId': post_id, 'comment_message': comment_message}
data = {"_token": self.token, "postId": post_id, "comment_message": comment_message}
return self.post(f"{self.url}/main/wall-comment/create/json", data=data)
def _post_main_wall_post_create(self, body: str) -> Response:
data = {'_token': self.token, 'post_message': body}
data = {"_token": self.token, "post_message": body}
return self.post(f"{self.url}/main/wall-post/create/json", data=data)
def _post_main_wall_post_retrieve(self) -> Response:
data = {'_token': self.token, 'page': 1, 'switchedFrom': False}
data = {"_token": self.token, "page": 1, "switchedFrom": False}
return self.post(f"{self.url}/main/wall-post/retrieve/json", data=data)
# ## Medal posting
def _post_main_wall_post_automatic(self, message: str, achievement_id: int) -> Response:
return self.post(f"{self.url}/main/wall-post/automatic", data=dict(_token=self.token, message=message,
achievementId=achievement_id))
return self.post(
f"{self.url}/main/wall-post/automatic", data=dict(_token=self.token, message=message, achievementId=achievement_id)
)
class CitizenAPI(
ErepublikArticleAPI, ErepublikCountryAPI, ErepublikCompanyAPI, ErepublikEconomyAPI,
ErepublikLeaderBoardAPI, ErepublikLocationAPI, ErepublikMilitaryAPI, ErepublikProfileAPI,
ErepublikPresidentAPI, ErepublikPoliticsAPI, ErepublikAnniversaryAPI, ErepublikWallPostAPI,
ErepublikTravelAPI
ErepublikArticleAPI,
ErepublikCountryAPI,
ErepublikCompanyAPI,
ErepublikEconomyAPI,
ErepublikLeaderBoardAPI,
ErepublikLocationAPI,
ErepublikMilitaryAPI,
ErepublikProfileAPI,
ErepublikPresidentAPI,
ErepublikPoliticsAPI,
ErepublikAnniversaryAPI,
ErepublikWallPostAPI,
ErepublikTravelAPI,
):
pass

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,21 @@ from typing import Dict, Optional, Union
import pytz
__all__ = ['erep_tz', 'min_datetime', 'max_datetime', 'Country', 'AIR_RANKS', 'COUNTRIES', 'FOOD_ENERGY',
'GROUND_RANKS', 'GROUND_RANK_POINTS', 'INDUSTRIES', 'TERRAINS']
__all__ = [
"erep_tz",
"min_datetime",
"max_datetime",
"Country",
"AIR_RANKS",
"COUNTRIES",
"FOOD_ENERGY",
"GROUND_RANKS",
"GROUND_RANK_POINTS",
"INDUSTRIES",
"TERRAINS",
]
erep_tz = pytz.timezone('US/Pacific')
erep_tz = pytz.timezone("US/Pacific")
min_datetime = erep_tz.localize(datetime.datetime(2007, 11, 20))
max_datetime = erep_tz.localize(datetime.datetime(2281, 9, 4))
@ -53,18 +64,70 @@ class Country:
class Industries:
__by_name = {'food': 1, 'weapon': 2, 'ticket': 3, 'house': 4, 'aircraft': 23,
'foodraw': 7, 'weaponraw': 12, 'houseraw': 18, 'aircraftraw': 24, 'airplaneraw': 24,
'frm': 7, 'wrm': 12, 'hrm': 18, 'arm': 24,
'frm q1': 7, 'frm q2': 8, 'frm q3': 9, 'frm q4': 10, 'frm q5': 11,
'wrm q1': 12, 'wrm q2': 13, 'wrm q3': 14, 'wrm q4': 15, 'wrm q5': 16,
'hrm q1': 18, 'hrm q2': 19, 'hrm q3': 20, 'hrm q4': 21, 'hrm q5': 22,
'arm q1': 24, 'arm q2': 25, 'arm q3': 26, 'arm q4': 27, 'arm q5': 28}
__by_id = {1: 'Food', 2: 'Weapon', 3: 'Ticket', 4: 'House', 23: 'Aircraft',
7: 'foodRaw', 8: 'FRM q2', 9: 'FRM q3', 10: 'FRM q4', 11: 'FRM q5',
12: 'weaponRaw', 13: 'WRM q2', 14: 'WRM q3', 15: 'WRM q4', 16: 'WRM q5',
17: 'houseRaw', 18: 'houseRaw', 19: 'HRM q2', 20: 'HRM q3', 21: 'HRM q4', 22: 'HRM q5',
24: 'aircraftRaw', 25: 'ARM q2', 26: 'ARM q3', 27: 'ARM q4', 28: 'ARM q5'}
__by_name = {
"food": 1,
"weapon": 2,
"ticket": 3,
"house": 4,
"aircraft": 23,
"foodraw": 7,
"weaponraw": 12,
"houseraw": 18,
"aircraftraw": 24,
"airplaneraw": 24,
"frm": 7,
"wrm": 12,
"hrm": 18,
"arm": 24,
"frm q1": 7,
"frm q2": 8,
"frm q3": 9,
"frm q4": 10,
"frm q5": 11,
"wrm q1": 12,
"wrm q2": 13,
"wrm q3": 14,
"wrm q4": 15,
"wrm q5": 16,
"hrm q1": 18,
"hrm q2": 19,
"hrm q3": 20,
"hrm q4": 21,
"hrm q5": 22,
"arm q1": 24,
"arm q2": 25,
"arm q3": 26,
"arm q4": 27,
"arm q5": 28,
}
__by_id = {
1: "Food",
2: "Weapon",
3: "Ticket",
4: "House",
23: "Aircraft",
7: "foodRaw",
8: "FRM q2",
9: "FRM q3",
10: "FRM q4",
11: "FRM q5",
12: "weaponRaw",
13: "WRM q2",
14: "WRM q3",
15: "WRM q4",
16: "WRM q5",
17: "houseRaw",
18: "houseRaw",
19: "HRM q2",
20: "HRM q3",
21: "HRM q4",
22: "HRM q5",
24: "aircraftRaw",
25: "ARM q2",
26: "ARM q3",
27: "ARM q4",
28: "ARM q5",
}
def __getitem__(self, item) -> Optional[Union[int, str]]:
if isinstance(item, int):
@ -81,111 +144,515 @@ class Industries:
return dict(by_id=self.__by_id, by_name=self.__by_name)
AIR_RANKS: Dict[int, str] = {
1: 'Airman', 2: 'Airman 1st Class', 3: 'Airman 1st Class*', 4: 'Airman 1st Class**', 5: 'Airman 1st Class***',
6: 'Airman 1st Class****', 7: 'Airman 1st Class*****', 8: 'Senior Airman', 9: 'Senior Airman*',
10: 'Senior Airman**', 11: 'Senior Airman***', 12: 'Senior Airman****', 13: 'Senior Airman*****',
14: 'Staff Sergeant', 15: 'Staff Sergeant*', 16: 'Staff Sergeant**', 17: 'Staff Sergeant***',
18: 'Staff Sergeant****', 19: 'Staff Sergeant*****', 20: 'Aviator', 21: 'Aviator*', 22: 'Aviator**',
23: 'Aviator***', 24: 'Aviator****', 25: 'Aviator*****', 26: 'Flight Lieutenant', 27: 'Flight Lieutenant*',
28: 'Flight Lieutenant**', 29: 'Flight Lieutenant***', 30: 'Flight Lieutenant****', 31: 'Flight Lieutenant*****',
32: 'Squadron Leader', 33: 'Squadron Leader*', 34: 'Squadron Leader**', 35: 'Squadron Leader***',
36: 'Squadron Leader****', 37: 'Squadron Leader*****', 38: 'Chief Master Sergeant', 39: 'Chief Master Sergeant*',
40: 'Chief Master Sergeant**', 41: 'Chief Master Sergeant***', 42: 'Chief Master Sergeant****',
43: 'Chief Master Sergeant*****', 44: 'Wing Commander', 45: 'Wing Commander*', 46: 'Wing Commander**',
47: 'Wing Commander***', 48: 'Wing Commander****', 49: 'Wing Commander*****', 50: 'Group Captain',
51: 'Group Captain*', 52: 'Group Captain**', 53: 'Group Captain***', 54: 'Group Captain****',
55: 'Group Captain*****', 56: 'Air Commodore', 57: 'Air Commodore*', 58: 'Air Commodore**', 59: 'Air Commodore***',
60: 'Air Commodore****', 61: 'Air Commodore*****',
class Rank:
id: int
name: str
rank_points: int
is_air: bool
def __init__(self, id: int, name: str, rank_points: int, is_air: bool = False):
self.id = id
self.name = name
self.rank_points = rank_points
self.is_air = bool(is_air)
def __int__(self):
return self.id
def __eq__(self, other):
if isinstance(other, Rank):
return self.id == other.id if other.is_air == self.is_air else False
else:
return self.id == int(other)
def __ne__(self, other):
if isinstance(other, Rank):
return not self.id == other.id if other.is_air == self.is_air else True
else:
return not self.id == int(other)
def __lt__(self, other):
if isinstance(other, Rank):
return self.id < other.id if other.is_air == self.is_air else False
else:
return self.id < int(other)
def __le__(self, other):
if isinstance(other, Rank):
return self.id <= other.id if other.is_air == self.is_air else False
else:
return self.id <= int(other)
def __gt__(self, other):
if isinstance(other, Rank):
return self.id > other.id if other.is_air == self.is_air else False
else:
return self.id > int(other)
def __ge__(self, other):
if isinstance(other, Rank):
return self.id >= other.id if other.is_air == self.is_air else False
else:
return self.id >= int(other)
@property
def as_dict(self):
return dict(id=self.id, name=self.name, rank_points=self.rank_points, is_air=self.is_air)
def __str__(self):
return f"{'Air' if self.is_air else 'Ground'}Rank<#{self.id} {self.name}>"
def __repr__(self):
return str(self)
AIR_RANK_NAMES: Dict[int, str] = {
1: "Airman",
2: "Airman 1st Class",
3: "Airman 1st Class*",
4: "Airman 1st Class**",
5: "Airman 1st Class***",
6: "Airman 1st Class****",
7: "Airman 1st Class*****",
8: "Senior Airman",
9: "Senior Airman*",
10: "Senior Airman**",
11: "Senior Airman***",
12: "Senior Airman****",
13: "Senior Airman*****",
14: "Staff Sergeant",
15: "Staff Sergeant*",
16: "Staff Sergeant**",
17: "Staff Sergeant***",
18: "Staff Sergeant****",
19: "Staff Sergeant*****",
20: "Aviator",
21: "Aviator*",
22: "Aviator**",
23: "Aviator***",
24: "Aviator****",
25: "Aviator*****",
26: "Flight Lieutenant",
27: "Flight Lieutenant*",
28: "Flight Lieutenant**",
29: "Flight Lieutenant***",
30: "Flight Lieutenant****",
31: "Flight Lieutenant*****",
32: "Squadron Leader",
33: "Squadron Leader*",
34: "Squadron Leader**",
35: "Squadron Leader***",
36: "Squadron Leader****",
37: "Squadron Leader*****",
38: "Chief Master Sergeant",
39: "Chief Master Sergeant*",
40: "Chief Master Sergeant**",
41: "Chief Master Sergeant***",
42: "Chief Master Sergeant****",
43: "Chief Master Sergeant*****",
44: "Wing Commander",
45: "Wing Commander*",
46: "Wing Commander**",
47: "Wing Commander***",
48: "Wing Commander****",
49: "Wing Commander*****",
50: "Group Captain",
51: "Group Captain*",
52: "Group Captain**",
53: "Group Captain***",
54: "Group Captain****",
55: "Group Captain*****",
56: "Air Commodore",
57: "Air Commodore*",
58: "Air Commodore**",
59: "Air Commodore***",
60: "Air Commodore****",
61: "Air Commodore*****",
62: "Air Vice Marshal",
63: "Air Vice Marshal*",
64: "Air Vice Marshal**",
65: "Air Vice Marshal***",
66: "Air Vice Marshal****",
67: "Air Vice Marshal*****",
68: "Air Marshal",
69: "Air Marshal*",
70: "Air Marshal**",
71: "Air Marshal***",
72: "Air Marshal****",
73: "Air Marshal*****",
74: "Air Chief Marshal",
75: "Air Chief Marshal*",
76: "Air Chief Marshal**",
77: "Air Chief Marshal***",
78: "Air Chief Marshal****",
79: "Air Chief Marshal*****",
}
AIR_RANK_POINTS: Dict[int, Optional[int]] = {
1: 0,
2: 10,
3: 25,
4: 45,
5: 70,
6: 100,
7: 140,
8: 190,
9: 270,
10: 380,
11: 530,
12: 850,
13: 1300,
14: 2340,
15: 3300,
16: 4200,
17: 5150,
18: 6100,
19: 7020,
20: 9100,
21: 12750,
22: 16400,
23: 20000,
24: 23650,
25: 27300,
26: 35500,
27: 48000,
28: 60000,
29: 72400,
30: 84500,
31: 97000,
32: 110000,
33: 140000,
34: 170000,
35: 210000,
36: 290000,
37: 350000,
38: 429000,
39: 601000,
40: 772000,
41: 944000,
42: 1115000,
43: 1287000,
44: 1673000,
45: 2238000,
46: 2804000,
47: 3369000,
48: 3935000,
49: 4500000,
50: 5020000,
51: 7028000,
52: 9036000,
53: 11044000,
54: 13052000,
55: 15060000,
56: 19580000,
57: 27412000,
58: 35244000,
59: 43076000,
60: 50908000,
61: 58740000,
62: 76360000,
63: 113166443,
64: 137448000,
65: None,
66: None,
67: None,
68: None,
69: None,
70: None,
71: None,
72: None,
73: None,
74: None,
75: None,
76: None,
77: None,
78: None,
79: None,
}
AIR_RANKS: Dict[int, Rank] = {i: Rank(i, AIR_RANK_NAMES[i], AIR_RANK_POINTS[i], True) for i in range(1, 80)}
COUNTRIES: Dict[int, Country] = {
1: Country(1, 'Romania', 'Romania', 'ROU'), 9: Country(9, 'Brazil', 'Brazil', 'BRA'),
10: Country(10, 'Italy', 'Italy', 'ITA'), 11: Country(11, 'France', 'France', 'FRA'),
12: Country(12, 'Germany', 'Germany', 'DEU'), 13: Country(13, 'Hungary', 'Hungary', 'HUN'),
14: Country(14, 'China', 'China', 'CHN'), 15: Country(15, 'Spain', 'Spain', 'ESP'),
23: Country(23, 'Canada', 'Canada', 'CAN'), 24: Country(24, 'USA', 'USA', 'USA'),
26: Country(26, 'Mexico', 'Mexico', 'MEX'), 27: Country(27, 'Argentina', 'Argentina', 'ARG'),
28: Country(28, 'Venezuela', 'Venezuela', 'VEN'), 29: Country(29, 'United Kingdom', 'United-Kingdom', 'GBR'),
30: Country(30, 'Switzerland', 'Switzerland', 'CHE'), 31: Country(31, 'Netherlands', 'Netherlands', 'NLD'),
32: Country(32, 'Belgium', 'Belgium', 'BEL'), 33: Country(33, 'Austria', 'Austria', 'AUT'),
34: Country(34, 'Czech Republic', 'Czech-Republic', 'CZE'), 35: Country(35, 'Poland', 'Poland', 'POL'),
36: Country(36, 'Slovakia', 'Slovakia', 'SVK'), 37: Country(37, 'Norway', 'Norway', 'NOR'),
38: Country(38, 'Sweden', 'Sweden', 'SWE'), 39: Country(39, 'Finland', 'Finland', 'FIN'),
40: Country(40, 'Ukraine', 'Ukraine', 'UKR'), 41: Country(41, 'Russia', 'Russia', 'RUS'),
42: Country(42, 'Bulgaria', 'Bulgaria', 'BGR'), 43: Country(43, 'Turkey', 'Turkey', 'TUR'),
44: Country(44, 'Greece', 'Greece', 'GRC'), 45: Country(45, 'Japan', 'Japan', 'JPN'),
47: Country(47, 'South Korea', 'South-Korea', 'KOR'), 48: Country(48, 'India', 'India', 'IND'),
49: Country(49, 'Indonesia', 'Indonesia', 'IDN'), 50: Country(50, 'Australia', 'Australia', 'AUS'),
51: Country(51, 'South Africa', 'South-Africa', 'ZAF'),
52: Country(52, 'Republic of Moldova', 'Republic-of-Moldova', 'MDA'),
53: Country(53, 'Portugal', 'Portugal', 'PRT'), 54: Country(54, 'Ireland', 'Ireland', 'IRL'),
55: Country(55, 'Denmark', 'Denmark', 'DNK'), 56: Country(56, 'Iran', 'Iran', 'IRN'),
57: Country(57, 'Pakistan', 'Pakistan', 'PAK'), 58: Country(58, 'Israel', 'Israel', 'ISR'),
59: Country(59, 'Thailand', 'Thailand', 'THA'), 61: Country(61, 'Slovenia', 'Slovenia', 'SVN'),
63: Country(63, 'Croatia', 'Croatia', 'HRV'), 64: Country(64, 'Chile', 'Chile', 'CHL'),
65: Country(65, 'Serbia', 'Serbia', 'SRB'), 66: Country(66, 'Malaysia', 'Malaysia', 'MYS'),
67: Country(67, 'Philippines', 'Philippines', 'PHL'), 68: Country(68, 'Singapore', 'Singapore', 'SGP'),
69: Country(69, 'Bosnia and Herzegovina', 'Bosnia-Herzegovina', 'BiH'),
70: Country(70, 'Estonia', 'Estonia', 'EST'), 80: Country(80, 'Montenegro', 'Montenegro', 'MNE'),
71: Country(71, 'Latvia', 'Latvia', 'LVA'), 72: Country(72, 'Lithuania', 'Lithuania', 'LTU'),
73: Country(73, 'North Korea', 'North-Korea', 'PRK'), 74: Country(74, 'Uruguay', 'Uruguay', 'URY'),
75: Country(75, 'Paraguay', 'Paraguay', 'PRY'), 76: Country(76, 'Bolivia', 'Bolivia', 'BOL'),
77: Country(77, 'Peru', 'Peru', 'PER'), 78: Country(78, 'Colombia', 'Colombia', 'COL'),
79: Country(79, 'Republic of Macedonia (FYROM)', 'Republic-of-Macedonia-FYROM', 'MKD'),
81: Country(81, 'Republic of China (Taiwan)', 'Republic-of-China-Taiwan', 'TWN'),
82: Country(82, 'Cyprus', 'Cyprus', 'CYP'), 167: Country(167, 'Albania', 'Albania', 'ALB'),
83: Country(83, 'Belarus', 'Belarus', 'BLR'), 84: Country(84, 'New Zealand', 'New-Zealand', 'NZL'),
164: Country(164, 'Saudi Arabia', 'Saudi-Arabia', 'SAU'), 165: Country(165, 'Egypt', 'Egypt', 'EGY'),
166: Country(166, 'United Arab Emirates', 'United-Arab-Emirates', 'UAE'),
168: Country(168, 'Georgia', 'Georgia', 'GEO'), 169: Country(169, 'Armenia', 'Armenia', 'ARM'),
170: Country(170, 'Nigeria', 'Nigeria', 'NGA'), 171: Country(171, 'Cuba', 'Cuba', 'CUB')
1: Country(1, "Romania", "Romania", "ROU"),
9: Country(9, "Brazil", "Brazil", "BRA"),
10: Country(10, "Italy", "Italy", "ITA"),
11: Country(11, "France", "France", "FRA"),
12: Country(12, "Germany", "Germany", "DEU"),
13: Country(13, "Hungary", "Hungary", "HUN"),
14: Country(14, "China", "China", "CHN"),
15: Country(15, "Spain", "Spain", "ESP"),
23: Country(23, "Canada", "Canada", "CAN"),
24: Country(24, "USA", "USA", "USA"),
26: Country(26, "Mexico", "Mexico", "MEX"),
27: Country(27, "Argentina", "Argentina", "ARG"),
28: Country(28, "Venezuela", "Venezuela", "VEN"),
29: Country(29, "United Kingdom", "United-Kingdom", "GBR"),
30: Country(30, "Switzerland", "Switzerland", "CHE"),
31: Country(31, "Netherlands", "Netherlands", "NLD"),
32: Country(32, "Belgium", "Belgium", "BEL"),
33: Country(33, "Austria", "Austria", "AUT"),
34: Country(34, "Czech Republic", "Czech-Republic", "CZE"),
35: Country(35, "Poland", "Poland", "POL"),
36: Country(36, "Slovakia", "Slovakia", "SVK"),
37: Country(37, "Norway", "Norway", "NOR"),
38: Country(38, "Sweden", "Sweden", "SWE"),
39: Country(39, "Finland", "Finland", "FIN"),
40: Country(40, "Ukraine", "Ukraine", "UKR"),
41: Country(41, "Russia", "Russia", "RUS"),
42: Country(42, "Bulgaria", "Bulgaria", "BGR"),
43: Country(43, "Turkey", "Turkey", "TUR"),
44: Country(44, "Greece", "Greece", "GRC"),
45: Country(45, "Japan", "Japan", "JPN"),
47: Country(47, "South Korea", "South-Korea", "KOR"),
48: Country(48, "India", "India", "IND"),
49: Country(49, "Indonesia", "Indonesia", "IDN"),
50: Country(50, "Australia", "Australia", "AUS"),
51: Country(51, "South Africa", "South-Africa", "ZAF"),
52: Country(52, "Republic of Moldova", "Republic-of-Moldova", "MDA"),
53: Country(53, "Portugal", "Portugal", "PRT"),
54: Country(54, "Ireland", "Ireland", "IRL"),
55: Country(55, "Denmark", "Denmark", "DNK"),
56: Country(56, "Iran", "Iran", "IRN"),
57: Country(57, "Pakistan", "Pakistan", "PAK"),
58: Country(58, "Israel", "Israel", "ISR"),
59: Country(59, "Thailand", "Thailand", "THA"),
61: Country(61, "Slovenia", "Slovenia", "SVN"),
63: Country(63, "Croatia", "Croatia", "HRV"),
64: Country(64, "Chile", "Chile", "CHL"),
65: Country(65, "Serbia", "Serbia", "SRB"),
66: Country(66, "Malaysia", "Malaysia", "MYS"),
67: Country(67, "Philippines", "Philippines", "PHL"),
68: Country(68, "Singapore", "Singapore", "SGP"),
69: Country(69, "Bosnia and Herzegovina", "Bosnia-Herzegovina", "BiH"),
70: Country(70, "Estonia", "Estonia", "EST"),
80: Country(80, "Montenegro", "Montenegro", "MNE"),
71: Country(71, "Latvia", "Latvia", "LVA"),
72: Country(72, "Lithuania", "Lithuania", "LTU"),
73: Country(73, "North Korea", "North-Korea", "PRK"),
74: Country(74, "Uruguay", "Uruguay", "URY"),
75: Country(75, "Paraguay", "Paraguay", "PRY"),
76: Country(76, "Bolivia", "Bolivia", "BOL"),
77: Country(77, "Peru", "Peru", "PER"),
78: Country(78, "Colombia", "Colombia", "COL"),
79: Country(79, "Republic of Macedonia (FYROM)", "Republic-of-Macedonia-FYROM", "MKD"),
81: Country(81, "Republic of China (Taiwan)", "Republic-of-China-Taiwan", "TWN"),
82: Country(82, "Cyprus", "Cyprus", "CYP"),
167: Country(167, "Albania", "Albania", "ALB"),
83: Country(83, "Belarus", "Belarus", "BLR"),
84: Country(84, "New Zealand", "New-Zealand", "NZL"),
164: Country(164, "Saudi Arabia", "Saudi-Arabia", "SAU"),
165: Country(165, "Egypt", "Egypt", "EGY"),
166: Country(166, "United Arab Emirates", "United-Arab-Emirates", "UAE"),
168: Country(168, "Georgia", "Georgia", "GEO"),
169: Country(169, "Armenia", "Armenia", "ARM"),
170: Country(170, "Nigeria", "Nigeria", "NGA"),
171: Country(171, "Cuba", "Cuba", "CUB"),
}
FOOD_ENERGY: Dict[str, int] = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20)
GROUND_RANKS: Dict[int, str] = {
1: 'Recruit', 2: 'Private', 3: 'Private*', 4: 'Private**', 5: 'Private***',
6: 'Corporal', 7: 'Corporal*', 8: 'Corporal**', 9: 'Corporal***',
10: 'Sergeant', 11: 'Sergeant*', 12: 'Sergeant**', 13: 'Sergeant***',
14: 'Lieutenant', 15: 'Lieutenant*', 16: 'Lieutenant**', 17: 'Lieutenant***',
18: 'Captain', 19: 'Captain*', 20: 'Captain**', 21: 'Captain***',
22: 'Major', 23: 'Major*', 24: 'Major**', 25: 'Major***',
26: 'Commander', 27: 'Commander*', 28: 'Commander**', 29: 'Commander***',
30: 'Lt Colonel', 31: 'Lt Colonel*', 32: 'Lt Colonel**', 33: 'Lt Colonel***',
34: 'Colonel', 35: 'Colonel*', 36: 'Colonel**', 37: 'Colonel***',
38: 'General', 39: 'General*', 40: 'General**', 41: 'General***',
42: 'Field Marshal', 43: 'Field Marshal*', 44: 'Field Marshal**', 45: 'Field Marshal***',
46: 'Supreme Marshal', 47: 'Supreme Marshal*', 48: 'Supreme Marshal**', 49: 'Supreme Marshal***',
50: 'National Force', 51: 'National Force*', 52: 'National Force**', 53: 'National Force***',
54: 'World Class Force', 55: 'World Class Force*', 56: 'World Class Force**', 57: 'World Class Force***',
58: 'Legendary Force', 59: 'Legendary Force*', 60: 'Legendary Force**', 61: 'Legendary Force***',
62: 'God of War', 63: 'God of War*', 64: 'God of War**', 65: 'God of War***',
66: 'Titan', 67: 'Titan*', 68: 'Titan**', 69: 'Titan***',
70: 'Legends I', 71: 'Legends II', 72: 'Legends III', 73: 'Legends IV', 74: 'Legends V', 75: 'Legends VI',
76: 'Legends VII', 77: 'Legends VIII', 78: 'Legends IX', 79: 'Legends X', 80: 'Legends XI', 81: 'Legends XII',
82: 'Legends XIII', 83: 'Legends XIV', 84: 'Legends XV', 85: 'Legends XVI', 86: 'Legends XVII', 87: 'Legends XVIII',
88: 'Legends XIX', 89: 'Legends XX',
GROUND_RANK_NAMES: Dict[int, str] = {
1: "Recruit",
2: "Private",
3: "Private*",
4: "Private**",
5: "Private***",
6: "Corporal",
7: "Corporal*",
8: "Corporal**",
9: "Corporal***",
10: "Sergeant",
11: "Sergeant*",
12: "Sergeant**",
13: "Sergeant***",
14: "Lieutenant",
15: "Lieutenant*",
16: "Lieutenant**",
17: "Lieutenant***",
18: "Captain",
19: "Captain*",
20: "Captain**",
21: "Captain***",
22: "Major",
23: "Major*",
24: "Major**",
25: "Major***",
26: "Commander",
27: "Commander*",
28: "Commander**",
29: "Commander***",
30: "Lt Colonel",
31: "Lt Colonel*",
32: "Lt Colonel**",
33: "Lt Colonel***",
34: "Colonel",
35: "Colonel*",
36: "Colonel**",
37: "Colonel***",
38: "General",
39: "General*",
40: "General**",
41: "General***",
42: "Field Marshal",
43: "Field Marshal*",
44: "Field Marshal**",
45: "Field Marshal***",
46: "Supreme Marshal",
47: "Supreme Marshal*",
48: "Supreme Marshal**",
49: "Supreme Marshal***",
50: "National Force",
51: "National Force*",
52: "National Force**",
53: "National Force***",
54: "World Class Force",
55: "World Class Force*",
56: "World Class Force**",
57: "World Class Force***",
58: "Legendary Force",
59: "Legendary Force*",
60: "Legendary Force**",
61: "Legendary Force***",
62: "God of War",
63: "God of War*",
64: "God of War**",
65: "God of War***",
66: "Titan",
67: "Titan*",
68: "Titan**",
69: "Titan***",
70: "Legends I",
71: "Legends II",
72: "Legends III",
73: "Legends IV",
74: "Legends V",
75: "Legends VI",
76: "Legends VII",
77: "Legends VIII",
78: "Legends IX",
79: "Legends X",
80: "Legends XI",
81: "Legends XII",
82: "Legends XIII",
83: "Legends XIV",
84: "Legends XV",
85: "Legends XVI",
86: "Legends XVII",
87: "Legends XVIII",
88: "Legends XIX",
89: "Legends XX",
}
GROUND_RANK_POINTS: Dict[int, int] = {
1: 0, 2: 15, 3: 45, 4: 80, 5: 120, 6: 170, 7: 250, 8: 350, 9: 450, 10: 600, 11: 800, 12: 1000,
13: 1400, 14: 1850, 15: 2350, 16: 3000, 17: 3750, 18: 5000, 19: 6500, 20: 9000, 21: 12000,
22: 15500, 23: 20000, 24: 25000, 25: 31000, 26: 40000, 27: 52000, 28: 67000, 29: 85000,
30: 110000, 31: 140000, 32: 180000, 33: 225000, 34: 285000, 35: 355000, 36: 435000, 37: 540000,
38: 660000, 39: 800000, 40: 950000, 41: 1140000, 42: 1350000, 43: 1600000, 44: 1875000,
45: 2185000, 46: 2550000, 47: 3000000, 48: 3500000, 49: 4150000, 50: 4900000, 51: 5800000,
52: 7000000, 53: 9000000, 54: 11500000, 55: 14500000, 56: 18000000, 57: 22000000, 58: 26500000,
59: 31500000, 60: 37000000, 61: 43000000, 62: 50000000, 63: 100000000, 64: 200000000,
65: 500000000, 66: 1000000000, 67: 2000000000, 68: 4000000000, 69: 10000000000, 70: 20000000000,
71: 30000000000, 72: 40000000000, 73: 50000000000, 74: 60000000000, 75: 70000000000,
76: 80000000000, 77: 90000000000, 78: 100000000000, 79: 110000000000, 80: 120000000000,
81: 130000000000, 82: 140000000000, 83: 150000000000, 84: 160000000000, 85: 170000000000,
86: 180000000000, 87: 190000000000, 88: 200000000000, 89: 210000000000
1: 0,
2: 15,
3: 45,
4: 80,
5: 120,
6: 170,
7: 250,
8: 350,
9: 450,
10: 600,
11: 800,
12: 1000,
13: 1400,
14: 1850,
15: 2350,
16: 3000,
17: 3750,
18: 5000,
19: 6500,
20: 9000,
21: 12000,
22: 15500,
23: 20000,
24: 25000,
25: 31000,
26: 40000,
27: 52000,
28: 67000,
29: 85000,
30: 110000,
31: 140000,
32: 180000,
33: 225000,
34: 285000,
35: 355000,
36: 435000,
37: 540000,
38: 660000,
39: 800000,
40: 950000,
41: 1140000,
42: 1350000,
43: 1600000,
44: 1875000,
45: 2185000,
46: 2550000,
47: 3000000,
48: 3500000,
49: 4150000,
50: 4900000,
51: 5800000,
52: 7000000,
53: 9000000,
54: 11500000,
55: 14500000,
56: 18000000,
57: 22000000,
58: 26500000,
59: 31500000,
60: 37000000,
61: 43000000,
62: 50000000,
63: 100000000,
64: 200000000,
65: 500000000,
66: 1000000000,
67: 2000000000,
68: 4000000000,
69: 10000000000,
70: 20000000000,
71: 30000000000,
72: 40000000000,
73: 50000000000,
74: 60000000000,
75: 70000000000,
76: 80000000000,
77: 90000000000,
78: 100000000000,
79: 110000000000,
80: 120000000000,
81: 130000000000,
82: 140000000000,
83: 150000000000,
84: 160000000000,
85: 170000000000,
86: 180000000000,
87: 190000000000,
88: 200000000000,
89: 210000000000,
}
GROUND_RANKS: Dict[int, Rank] = {i: Rank(i, GROUND_RANK_NAMES[i], GROUND_RANK_POINTS[i], False) for i in range(1, 90)}
INDUSTRIES = Industries()
TERRAINS: Dict[int, str] = {0: 'Standard', 1: 'Industrial', 2: 'Urban', 3: 'Suburbs', 4: 'Airport', 5: 'Plains',
6: 'Wasteland', 7: 'Mountains', 8: 'Beach', 9: 'Swamp', 10: 'Mud', 11: 'Hills',
12: 'Jungle', 13: 'Forest', 14: 'Desert'}
TERRAINS: Dict[int, str] = {
0: "Standard",
1: "Industrial",
2: "Urban",
3: "Suburbs",
4: "Airport",
5: "Plains",
6: "Wasteland",
7: "Mountains",
8: "Beach",
9: "Swamp",
10: "Mud",
11: "Hills",
12: "Jungle",
13: "Forest",
14: "Desert",
}

View File

@ -1,33 +1,56 @@
import datetime
import inspect
import os
import re
import sys
import time
import traceback
import unicodedata
import warnings
from base64 import b64encode
from decimal import Decimal
from logging import Logger
from pathlib import Path
from typing import Any, Dict, List, Union
import pytz
import requests
from requests import Response
from . import __version__, constants
from erepublik import __version__, constants
try:
import simplejson as json
except ImportError:
import json
__all__ = ['VERSION', 'calculate_hit', 'caught_error', 'date_from_eday', 'eday_from_date', 'deprecation',
'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',
'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',
'json_decode_object_hook', 'json_load', 'json_loads']
__all__ = [
"VERSION",
"calculate_hit",
"date_from_eday",
"eday_from_date",
"deprecation",
"get_final_hit_dmg",
"write_file",
"get_air_hit_dmg_value",
"get_file",
"get_ground_hit_dmg_value",
"get_sleep_seconds",
"good_timedelta",
"slugify",
"interactive_sleep",
"json",
"localize_dt",
"localize_timestamp",
"normalize_html_json",
"now",
"silent_sleep",
"json_decode_object_hook",
"json_load",
"json_loads",
"json_dump",
"json_dumps",
"b64json",
"ErepublikJSONEncoder",
]
VERSION: str = __version__
@ -75,7 +98,7 @@ def date_from_eday(eday: int) -> datetime.date:
def get_sleep_seconds(time_until: datetime.datetime) -> int:
""" time_until aware datetime object Wrapper for sleeping until """
"""time_until aware datetime object Wrapper for sleeping until"""
sleep_seconds = int((time_until - now()).total_seconds())
return sleep_seconds if sleep_seconds > 0 else 0
@ -103,32 +126,11 @@ def interactive_sleep(sleep_seconds: int):
silent_sleep = time.sleep
def _write_log(msg, timestamp: bool = True, should_print: bool = False):
erep_time_now = now()
txt = f"[{erep_time_now.strftime('%F %T')}] {msg}" if timestamp else msg
if not os.path.isdir('log'):
os.mkdir('log')
with open(f'log/{erep_time_now.strftime("%F")}.log', 'a', encoding='utf-8') as f:
f.write(f'{txt}\n')
if should_print:
print(txt)
def write_interactive_log(*args, **kwargs):
kwargs.pop('should_print', None)
_write_log(should_print=True, *args, **kwargs)
def write_silent_log(*args, **kwargs):
kwargs.pop('should_print', None)
_write_log(should_print=False, *args, **kwargs)
def get_file(filepath: str) -> str:
file = Path(filepath)
if file.exists():
if file.is_dir():
return str(file / 'new_file.txt')
return str(file / "new_file.txt")
else:
version = 1
try:
@ -150,168 +152,19 @@ def get_file(filepath: str) -> str:
def write_file(filename: str, content: str) -> int:
filename = get_file(filename)
with open(filename, 'ab') as f:
with open(filename, "ab") as f:
ret = f.write(content.encode("utf-8"))
return ret
def write_request(response: requests.Response, is_error: bool = False):
from erepublik import Citizen
# Remove GET args from url name
url = response.url
last_index = url.index("?") if "?" in url else len(response.url)
name = slugify(response.url[len(Citizen.url):last_index])
html = response.text
try:
json.loads(html)
ext = 'json'
except json.decoder.JSONDecodeError:
ext = 'html'
if not is_error:
filename = f"debug/requests/{now().strftime('%F_%H-%M-%S')}_{name}.{ext}"
write_file(filename, html)
else:
return dict(name=f"{now().strftime('%F_%H-%M-%S')}_{name}.{ext}", content=html.encode('utf-8'),
mimetype="application/json" if ext == 'json' else "text/html")
def send_email(name: str, content: List[Any], player=None, local_vars: Dict[str, Any] = None,
promo: bool = False, captcha: bool = False):
if local_vars is None:
local_vars = {}
from erepublik import Citizen
file_content_template = '<html><head><title>{title}</title></head><body>{body}</body></html>'
if isinstance(player, Citizen) and player.r:
resp = write_request(player.r, is_error=True)
else:
resp = dict(name='None.html', mimetype='text/html',
content=file_content_template.format(body='<br/>'.join(content), title='Error'))
if promo:
resp = dict(name=f"{name}.html", mimetype='text/html',
content=file_content_template.format(title='Promo', body='<br/>'.join(content)))
subject = f"[eBot][{now().strftime('%F %T')}] Promos: {name}"
elif captcha:
resp = dict(name=f'{name}.html', mimetype='text/html',
content=file_content_template.format(title='ReCaptcha', body='<br/>'.join(content)))
subject = f"[eBot][{now().strftime('%F %T')}] RECAPTCHA: {name}"
else:
subject = f"[eBot][{now().strftime('%F %T')}] Bug trace: {name}"
body = "".join(traceback.format_stack()) + \
"\n\n" + \
"\n".join(content)
data = dict(send_mail=True, subject=subject, bugtrace=body)
if promo:
data.update(promo=True)
elif captcha:
data.update(captcha=True)
else:
data.update(bug=True)
files = [('file', (resp.get('name'), resp.get('content'), resp.get('mimetype'))), ]
filename = f'log/{now().strftime("%F")}.log'
if os.path.isfile(filename):
files.append(('file', (filename[4:], open(filename, 'rb'), 'text/plain')))
if local_vars:
if 'state_thread' in local_vars:
local_vars.pop('state_thread', None)
if isinstance(local_vars.get('self'), Citizen):
local_vars['self'] = repr(local_vars['self'])
if isinstance(local_vars.get('player'), Citizen):
local_vars['player'] = repr(local_vars['player'])
if isinstance(local_vars.get('citizen'), Citizen):
local_vars['citizen'] = repr(local_vars['citizen'])
from erepublik.classes import ErepublikJSONEncoder
files.append(('file', ('local_vars.json', json.dumps(local_vars, cls=ErepublikJSONEncoder),
"application/json")))
if isinstance(player, Citizen):
files.append(('file', ('instance.json', player.to_json(indent=True), "application/json")))
requests.post('https://pasts.72.lv', data=data, files=files)
def normalize_html_json(js: str) -> str:
js = re.sub(r' \'(.*?)\'', lambda a: f'"{a.group(1)}"', js)
js = re.sub(r'(\d\d):(\d\d):(\d\d)', r'\1\2\3', js)
js = re.sub(r" \'(.*?)\'", lambda a: f'"{a.group(1)}"', js)
js = re.sub(r"(\d\d):(\d\d):(\d\d)", r"\1\2\3", js)
js = re.sub(r'([{\s,])(\w+)(:)(?!"})', r'\1"\2"\3', js)
js = re.sub(r',\s*}', '}', js)
js = re.sub(r",\s*}", "}", js)
return js
def caught_error(e: Exception):
process_error(str(e), 'Unclassified', sys.exc_info(), interactive=False)
def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None,
interactive: bool = None):
"""
Process error logging and email sending to developer
:param interactive: Should print interactively
:type interactive: bool
:param log_info: String to be written in output
:type log_info: str
:param name: String Instance name
:type name: str
:param exc_info: tuple output from sys.exc_info()
:type exc_info: tuple
:param citizen: Citizen instance
:type citizen: Citizen
:param commit_id: Caller's code version's commit id
:type commit_id: str
"""
type_, value_, traceback_ = exc_info
content = [log_info]
content += [f"eRepublik version {VERSION}"]
if commit_id:
content += [f"Commit id {commit_id}"]
content += [str(value_), str(type_), ''.join(traceback.format_tb(traceback_))]
if interactive:
write_interactive_log(log_info)
elif interactive is not None:
write_silent_log(log_info)
trace = inspect.trace()
if trace:
local_vars = trace[-1][0].f_locals
if local_vars.get('__name__') == '__main__':
local_vars.update(commit_id=local_vars.get('COMMIT_ID'), interactive=local_vars.get('INTERACTIVE'),
version=local_vars.get('__version__'), config=local_vars.get('CONFIG'))
else:
local_vars = dict()
send_email(name, content, citizen, local_vars=local_vars)
def process_warning(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None):
"""
Process error logging and email sending to developer
:param log_info: String to be written in output
:param name: String Instance name
:param exc_info: tuple output from sys.exc_info()
:param citizen: Citizen instance
:param commit_id: Code's version by commit id
"""
type_, value_, traceback_ = exc_info
content = [log_info]
if commit_id:
content += [f'Commit id: {commit_id}']
content += [str(value_), str(type_), ''.join(traceback.format_tb(traceback_))]
trace = inspect.trace()
if trace:
local_vars = trace[-1][0].f_locals
else:
local_vars = dict()
send_email(name, content, citizen, local_vars=local_vars)
def slugify(value, allow_unicode=False) -> str:
"""
Function copied from Django2.2.1 django.utils.text.slugify
@ -321,47 +174,51 @@ def slugify(value, allow_unicode=False) -> str:
"""
value = str(value)
if allow_unicode:
value = unicodedata.normalize('NFKC', value)
value = unicodedata.normalize("NFKC", value)
else:
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = re.sub(r'[^\w\s-]', '_', value).strip().lower()
return re.sub(r'[-\s]+', '-', value)
value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
value = re.sub(r"[^\w\s-]", "_", value).strip().lower()
return re.sub(r"[-\s]+", "-", value)
def calculate_hit(strength: float, rang: int, tp: bool, elite: bool, ne: bool, booster: int = 0,
weapon: int = 200, is_deploy: bool = False) -> Decimal:
def calculate_hit(
strength: float, rang: int, tp: bool, elite: bool, ne: bool, booster: int = 0, weapon: int = 200, is_deploy: bool = False
) -> Decimal:
dec = 3 if is_deploy else 0
base_str = (1 + Decimal(str(round(strength, 3))) / 400)
base_rnk = (1 + Decimal(str(rang / 5)))
base_wpn = (1 + Decimal(str(weapon / 100)))
base_str = 1 + Decimal(str(round(strength, 3))) / 400
base_rnk = 1 + Decimal(str(rang / 5))
base_wpn = 1 + Decimal(str(weapon / 100))
dmg = 10 * base_str * base_rnk * base_wpn
dmg = get_final_hit_dmg(dmg, rang, tp=tp, elite=elite, ne=ne, booster=booster)
return Decimal(round(dmg, dec))
def get_ground_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False,
booster: int = 0, weapon_power: int = 200) -> Decimal:
r = requests.get(f'https://www.erepublik.com/en/main/citizen-profile-json/{citizen_id}').json()
rang = r['military']['militaryData']['ground']['rankNumber']
strength = r['military']['militaryData']['ground']['strength']
elite = r['citizenAttributes']['level'] > 100
def get_ground_hit_dmg_value(
citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False, booster: int = 0, weapon_power: int = 200
) -> Decimal:
r = requests.get(f"https://www.erepublik.com/en/main/citizen-profile-json/{citizen_id}").json()
rang = r["military"]["militaryData"]["ground"]["rankNumber"]
strength = r["military"]["militaryData"]["ground"]["strength"]
elite = r["citizenAttributes"]["level"] > 100
if natural_enemy:
true_patriot = True
return calculate_hit(strength, rang, true_patriot, elite, natural_enemy, booster, weapon_power)
def get_air_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False, booster: int = 0,
weapon_power: int = 0) -> Decimal:
r = requests.get(f'https://www.erepublik.com/en/main/citizen-profile-json/{citizen_id}').json()
rang = r['military']['militaryData']['aircraft']['rankNumber']
elite = r['citizenAttributes']['level'] > 100
def get_air_hit_dmg_value(
citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False, booster: int = 0, weapon_power: int = 0
) -> Decimal:
r = requests.get(f"https://www.erepublik.com/en/main/citizen-profile-json/{citizen_id}").json()
rang = r["military"]["militaryData"]["aircraft"]["rankNumber"]
elite = r["citizenAttributes"]["level"] > 100
return calculate_hit(0, rang, true_patriot, elite, natural_enemy, booster, weapon_power)
def get_final_hit_dmg(base_dmg: Union[Decimal, float, str], rang: int,
tp: bool = False, elite: bool = False, ne: bool = False, booster: int = 0) -> Decimal:
def get_final_hit_dmg(
base_dmg: Union[Decimal, float, str], rang: int, tp: bool = False, elite: bool = False, ne: bool = False, booster: int = 0
) -> Decimal:
dmg = Decimal(str(base_dmg))
if elite:
@ -378,63 +235,54 @@ def deprecation(message):
warnings.warn(message, DeprecationWarning, stacklevel=2)
def wait_for_lock(function):
def wrapper(instance, *args, **kwargs):
if not instance.concurrency_available.wait(600):
e = 'Concurrency not freed in 10min!'
instance.write_log(e)
if instance.debug:
instance.report_error(e)
return None
else:
instance.concurrency_available.clear()
try:
ret = function(instance, *args, **kwargs)
except Exception as e:
instance.concurrency_available.set()
raise e
instance.concurrency_available.set()
return ret
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
"""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)
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')
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'])
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)
# 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)
# kwargs.update(object_hook=json_decode_object_hook)
return json.loads(s, **kwargs)
def json_dump(obj, fp, *args, **kwargs):
if not kwargs.get("cls"):
kwargs.update(cls=ErepublikJSONEncoder)
return json.dump(obj, fp, *args, **kwargs)
def json_dumps(obj, *args, **kwargs):
if not kwargs.get("cls"):
kwargs.update(cls=ErepublikJSONEncoder)
return json.dumps(obj, *args, **kwargs)
def b64json(obj: Union[Dict[str, Union[int, List[str]]], List[str]]):
if isinstance(obj, list):
return b64encode(json.dumps(obj).encode('utf-8')).decode('utf-8')
return b64encode(json.dumps(obj, separators=(",", ":")).encode("utf-8")).decode("utf-8")
elif isinstance(obj, (int, str)):
return obj
elif isinstance(obj, dict):
@ -442,5 +290,44 @@ def b64json(obj: Union[Dict[str, Union[int, List[str]]], List[str]]):
obj[k] = b64json(v)
else:
from .classes import ErepublikException
raise ErepublikException(f'Unhandled object type! obj is {type(obj)}')
return b64encode(json.dumps(obj).encode('utf-8')).decode('utf-8')
raise ErepublikException(f"Unhandled object type! obj is {type(obj)}")
return b64encode(json.dumps(obj, separators=(",", ":")).encode("utf-8")).decode("utf-8")
class ErepublikJSONEncoder(json.JSONEncoder):
def default(self, o):
try:
from erepublik.citizen import Citizen
if isinstance(o, Decimal):
return float(f"{o:.02f}")
elif isinstance(o, datetime.datetime):
return dict(
__type__="datetime",
date=o.strftime("%Y-%m-%d"),
time=o.strftime("%H:%M:%S"),
tzinfo=str(o.tzinfo) if o.tzinfo else None,
)
elif isinstance(o, datetime.date):
return dict(__type__="date", date=o.strftime("%Y-%m-%d"))
elif isinstance(o, datetime.timedelta):
return dict(
__type__="timedelta", days=o.days, seconds=o.seconds, microseconds=o.microseconds, total_seconds=o.total_seconds()
)
elif isinstance(o, Response):
return dict(headers=dict(o.__dict__["headers"]), url=o.url, text=o.text, status_code=o.status_code)
elif hasattr(o, "as_dict"):
return o.as_dict
elif isinstance(o, set):
return list(o)
elif isinstance(o, Citizen):
return o.to_json()
elif isinstance(o, Logger):
return str(o)
elif hasattr(o, "__dict__"):
return o.__dict__
else:
return super().default(o)
except Exception as e: # noqa
return str(e)

View File

@ -26,9 +26,7 @@ def main():
player.login()
now = player.now.replace(second=0, microsecond=0)
dt_max = constants.max_datetime
tasks = {
'eat': now,
}
tasks = {}
if player.config.work:
tasks.update({'work': now})
if player.config.train:
@ -61,7 +59,6 @@ def main():
if tasks.get('wam', dt_max) <= now:
player.write_log("Doing task: Work as manager")
success = player.work_as_manager()
player.eat()
if success:
next_time = utils.good_timedelta(now.replace(hour=14, minute=0, second=0, microsecond=0),
timedelta(days=1))
@ -70,19 +67,8 @@ def main():
tasks.update({'wam': next_time})
if tasks.get('eat', dt_max) <= now:
player.write_log("Doing task: eat")
player.eat()
if player.energy.food_fights > player.energy.limit // 10:
next_minutes = 12
else:
next_minutes = (player.energy.limit - 5 * player.energy.interval) // player.energy.interval * 6
next_time = player.energy.reference_time + timedelta(minutes=next_minutes)
tasks.update({'eat': next_time})
if tasks.get('ot', dt_max) <= now:
player.update_job_info()
player.write_log("Doing task: work overtime")
if now > player.my_companies.next_ot_time:
player.work_ot()

4
pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[tool.black]
line-length = 140
target-version = ['py38', 'py39']

View File

@ -1,20 +1,21 @@
bump2version==1.0.1
coverage==5.4
edx-sphinx-theme==1.6.1
flake8==3.8.4
ipython>=7.19.0
coverage==5.5
edx-sphinx-theme==3.0.0
flake8==3.9.2
ipython>=7.25.0
jedi!=0.18.0
isort==5.7.0
pip==21.0
pre-commit==2.9.3
pur==5.3.0
PyInstaller==4.2
isort==5.9.2
pip==21.1.3
pre-commit==2.13.0
pur==5.4.2
PyInstaller==4.4
PySocks==1.7.1
pytest==6.2.2
pytz>=2020.5
requests>=2.25.1
responses==0.12.1
setuptools==52.0.0
Sphinx==3.4.3
twine==3.3.0
pytest==6.2.4
pytz==2021.1
requests==2.26.0
requests-toolbelt==0.9.1
responses==0.13.3
setuptools==57.4.0
Sphinx==4.1.1
twine==3.4.1
wheel==0.36.2

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.24.0.3
current_version = 0.25.1.4
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)?
@ -7,27 +7,29 @@ serialize = {major}.{minor}.{patch}.{dev}
{major}.{minor}.{patch}
[bumpversion:file:setup.py]
search = version='{current_version}'
replace = version='{new_version}'
search = version="{current_version}"
replace = version="{new_version}"
[bumpversion:file:erepublik/__init__.py]
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bdist_wheel]
universal = 1
[flake8]
exclude = docs,.git,log,debug,venv
max-line-length = 120
ignore = D100,D101,D102,D103
line_length = 140
max-line-length = 140
ignore = D100,D101,D102,D103,E203
[pycodestyle]
max-line-length = 120
line_length = 140
max-line-length = 140
exclude = .git,log,debug,venv, build
[mypy]
python_version = 3.7
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = False
warn_unused_ignores = True
@ -36,4 +38,4 @@ warn_unused_configs = True
[isort]
multi_line_output = 2
line_length = 120
line_length = 140

View File

@ -5,51 +5,51 @@
from setuptools import find_packages, setup
with open('README.rst') as readme_file:
with open("README.rst") as readme_file:
readme = readme_file.read()
with open('HISTORY.rst') as history_file:
with open("HISTORY.rst") as history_file:
history = history_file.read()
requirements = [
'pytz>=2020.0',
'requests>=2.24.0,<2.26.0',
'PySocks==1.7.1'
"PySocks==1.7.1",
"pytz==2021.1",
"requests==2.26.0",
"requests-toolbelt==0.9.1",
]
setup_requirements = []
test_requirements = [
"pytest==6.1.2",
"responses==0.12.1"
"pytest==6.2.4",
]
setup(
author="Eriks Karls",
author_email='eriks@72.lv',
author_email="eriks@72.lv",
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
description="Python package for automated eRepublik playing",
entry_points={},
install_requires=requirements,
license="MIT license",
long_description=readme + '\n\n' + history,
long_description=readme + "\n\n" + history,
include_package_data=True,
keywords='erepublik',
name='eRepublik',
packages=find_packages(include=['erepublik']),
python_requires='>=3.7, <4',
keywords="erepublik",
name="eRepublik",
packages=find_packages(include=["erepublik"]),
python_requires=">=3.8, <4",
setup_requires=setup_requirements,
test_suite='tests',
test_suite="tests",
tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/',
version='0.24.0.3',
url="https://github.com/eeriks/erepublik/",
version="0.25.1.4",
zip_safe=False,
)

View File

@ -17,14 +17,13 @@ class TestErepublik(unittest.TestCase):
self.citizen.config.interactive = False
def test_should_do_levelup(self):
self.citizen.energy.recovered = 1900
self.citizen.energy.recoverable = 2940
self.citizen.energy.energy = 5950
self.citizen.energy.interval = 30
self.citizen.energy.limit = 3000
self.citizen.energy.limit = 6000
self.citizen.details.xp = 14850
self.assertTrue(self.citizen.should_do_levelup)
self.citizen.energy.recoverable = 1000
self.citizen.energy.energy = 1000
self.assertFalse(self.citizen.should_do_levelup)
def test_should_travel_to_fight(self):
@ -33,40 +32,38 @@ class TestErepublik(unittest.TestCase):
self.citizen.config.always_travel = False
self.assertFalse(self.citizen.should_travel_to_fight())
self.citizen.energy.recovered = 1900
self.citizen.energy.recoverable = 2940
self.citizen.energy.energy = 5960
self.citizen.energy.interval = 30
self.citizen.energy.limit = 3000
self.citizen.energy.limit = 6000
self.citizen.details.xp = 14850
self.assertTrue(self.citizen.should_travel_to_fight())
self.citizen.details.xp = 15000
self.citizen.energy.energy = 5000
self.assertFalse(self.citizen.should_travel_to_fight())
self.citizen.energy.recovered = 3000
self.citizen.energy.recoverable = 2910
self.citizen.energy.energy = 5910
self.assertTrue(self.citizen.should_travel_to_fight())
self.citizen.energy.recoverable = 2900
self.citizen.energy.energy = 5900
self.assertFalse(self.citizen.should_travel_to_fight())
# self.citizen.next_reachable_energy and self.citizen.config.next_energy
self.citizen.config.next_energy = True
self.citizen.energy.limit = 5000
self.citizen.energy.limit = 10000
self.citizen.details.next_pp = [5000, 5250, 5750, 6250, 6750]
self.citizen.details.pp = 4900
self.citizen.energy.recovered = 4000
self.citizen.energy.recoverable = 4510
self.citizen.energy.energy = 8510
self.assertEqual(self.citizen.next_reachable_energy, 850)
self.citizen.energy.recoverable = 4490
self.citizen.energy.energy = 8490
self.assertTrue(self.citizen.should_travel_to_fight())
self.assertEqual(self.citizen.next_reachable_energy, 350)
self.citizen.energy.recovered = 100
self.citizen.energy.recoverable = 150
self.citizen.energy.energy = 250
self.assertFalse(self.citizen.should_travel_to_fight())
self.assertEqual(self.citizen.next_reachable_energy, 0)
def test_should_fight(self):
def is_wc_close():
return self.citizen.max_time_till_full_ff > self.citizen.time_till_week_change
self.citizen.config.fight = False
self.assertEqual(self.citizen.should_fight(), (0, "Fighting not allowed!", False))
@ -76,62 +73,64 @@ class TestErepublik(unittest.TestCase):
self.citizen.energy.limit = 3000
self.citizen.details.xp = 24705
if not is_wc_close:
self.assertEqual(self.citizen.should_fight(), (0, 'Level up', False))
self.assertEqual(self.citizen.should_fight(), (0, "Level up", False))
self.citizen.energy.recovered = 3000
self.citizen.energy.recoverable = 2950
self.citizen.energy.energy = 5950
self.citizen.energy.interval = 30
self.assertEqual(self.citizen.should_fight(), (900, 'Level up', True))
self.assertEqual(self.citizen.should_fight(), (900, "Level up", True))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (900, 'Level up', True))
self.assertEqual(self.citizen.should_fight(), (900, "Level up", True))
self.citizen.my_companies.ff_lockdown = 0
# Level up reachable
self.citizen.details.xp = 24400
self.assertEqual(self.citizen.should_fight(), (305, 'Fighting for close Levelup. Doing 305 hits', True))
self.assertEqual(self.citizen.should_fight(), (305, "Fighting for close Levelup. Doing 305 hits", True))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (305, 'Fighting for close Levelup. Doing 305 hits', True))
self.assertEqual(self.citizen.should_fight(), (305, "Fighting for close Levelup. Doing 305 hits", True))
self.citizen.my_companies.ff_lockdown = 0
self.citizen.details.xp = 21000
self.assertEqual(self.citizen.should_fight(), (75, 'Obligatory fighting for at least 75pp', True))
self.assertEqual(self.citizen.should_fight(), (75, "Obligatory fighting for at least 75pp", True))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (75, 'Obligatory fighting for at least 75pp', True))
self.assertEqual(self.citizen.should_fight(), (75, "Obligatory fighting for at least 75pp", True))
self.citizen.my_companies.ff_lockdown = 0
self.citizen.details.pp = 80
# All-in (type = all-in and full ff)
self.citizen.config.all_in = True
self.assertEqual(self.citizen.should_fight(), (595, 'Fighting all-in. Doing 595 hits', False))
self.assertEqual(self.citizen.should_fight(), (595, "Fighting all-in. Doing 595 hits", False))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (
435, 'Fight count modified (old count: 595 | FF: 595 | WAM ff_lockdown: 160 | New count: 435)', False
))
self.assertEqual(
self.citizen.should_fight(),
(435, "Fight count modified (old count: 595 | FF: 595 | WAM ff_lockdown: 160 | New count: 435)", False),
)
self.citizen.my_companies.ff_lockdown = 0
self.citizen.config.air = True
self.citizen.energy.recoverable = 1000
self.assertEqual(self.citizen.should_fight(), (400, 'Fighting all-in in AIR. Doing 400 hits', False))
self.citizen.energy.energy = 4000
self.assertEqual(self.citizen.should_fight(), (400, "Fighting all-in in AIR. Doing 400 hits", False))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (
240, 'Fight count modified (old count: 400 | FF: 400 | WAM ff_lockdown: 160 | New count: 240)', False
))
self.assertEqual(
self.citizen.should_fight(),
(240, "Fight count modified (old count: 400 | FF: 400 | WAM ff_lockdown: 160 | New count: 240)", False),
)
self.citizen.my_companies.ff_lockdown = 0
self.citizen.config.all_in = False
self.citizen.config.next_energy = True
self.citizen.energy.limit = 5000
self.citizen.energy.limit = 10000
self.citizen.details.next_pp = [100, 150, 250, 400, 500]
self.assertEqual(self.citizen.should_fight(), (320, 'Fighting for +1 energy. Doing 320 hits', False))
self.assertEqual(self.citizen.should_fight(), (320, "Fighting for +1 energy. Doing 320 hits", False))
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), (
160, 'Fight count modified (old count: 320 | FF: 400 | WAM ff_lockdown: 160 | New count: 160)', False
))
self.assertEqual(
self.citizen.should_fight(),
(160, "Fight count modified (old count: 320 | FF: 400 | WAM ff_lockdown: 160 | New count: 160)", False),
)
self.citizen.my_companies.ff_lockdown = 0
self.citizen.energy.limit = 3000
self.citizen.details.next_pp = [19250, 20000]
self.citizen.config.next_energy = False
# 1h worth of energy
self.citizen.energy.recoverable = 2910
self.assertEqual(self.citizen.should_fight(), (30, 'Fighting for 1h energy. Doing 30 hits', True))
self.citizen.energy.energy = 5910
self.assertEqual(self.citizen.should_fight(), (30, "Fighting for 1h energy. Doing 30 hits", True))