Compare commits

...

64 Commits

Author SHA1 Message Date
76bd40c655 Bump version: 0.19.0 → 0.19.1 2020-01-13 21:33:58 +02:00
15e6deebda Battle initialization without valid data should be avoided to not run into strange and hard to trace bugs.
Jsonification updates - if simplejson is available some packages are importing simplejson with try-except and later throwing simplejson errors which should be cought when calling .json() on every request.
Fixed error logging
2020-01-13 21:33:50 +02:00
69d0e7df0a Bump version: 0.18.3 → 0.19.0 2020-01-13 10:40:33 +02:00
4f92894ab6 DocUpdate 2020-01-13 10:40:22 +02:00
9c64bfac0f Merge branch 'inventory_updates'
* inventory_updates:
  Python 3.8, isort, requirement update
  Representation of Citizen class
  Created method for current products on sale. Updated inventory to also include products on sale

# Conflicts:
#	erepublik/citizen.py
2020-01-13 10:31:05 +02:00
1f07f2e270 Update:
Citizen.set_default_weapon() - eRepublik should return list with all available weapon qualities, but when a battle is just launched, they return only dict with barehands
Citizen.fight() - no longer calls self.set_default_weapon()
Citizen.find_battle_and_fight() - now calls self.set_default_weapon() just before fighting
Citizen.update_war_info() - returns previous battle list if responses 'last_updated' isn't more than 30s old

New:
Citizen.get_battle_for_war(war_id) - returns Battle instance for specific war, if battle is active for given war
2020-01-13 10:28:42 +02:00
71d204843d Python 3.8, isort, requirement update 2020-01-09 12:03:11 +02:00
d9305214eb Representation of Citizen class 2020-01-07 19:55:31 +02:00
5556d5f772 Created method for current products on sale. Updated inventory to also include products on sale 2020-01-07 16:28:42 +02:00
1c47d169d2 Bump version: 0.18.2 → 0.18.3 2020-01-07 11:32:59 +02:00
ef44787bad make clean-pyc removes log/ and debug/ run 'artifacts' 2020-01-07 11:32:48 +02:00
42431134e1 UA update 2020-01-07 11:30:40 +02:00
bedaeeefd1 Battle division update 2020-01-07 11:15:40 +02:00
bbf304aa99 Bump version: 0.18.1 → 0.18.2 2020-01-05 10:53:39 +02:00
a2447959e7 fight must receive battle id as int, added warnings support 2020-01-05 10:53:26 +02:00
700bd8d98e Bump version: 0.18.0 → 0.18.1 2020-01-02 22:43:01 +02:00
3599dc40fc More logging, Citizen.get_raw_surplus() fixed and moved to Citizen.my_companies.get_wam_raw_usage() 2020-01-02 22:42:40 +02:00
6ba727a781 promo spam loop 2020-01-02 18:49:38 +02:00
7f1829a5d7 Bump version: 0.17.3 → 0.18.0 2019-12-18 16:26:10 +02:00
e374aa8a54 Implemented division switching,
improved multi bomb deploy with auto traveling,
Citizen.fight() simplified battle data gathering logic -> Citizen.shoot logic improved
Citizen.all_battles are now defaultdict with default value of empty/invalid battle, for times when trying to do things with battle which is not in all_battle dict
2019-12-18 16:25:52 +02:00
7edfa3b004 Bump version: 0.17.2 → 0.17.3 2019-12-18 11:45:02 +02:00
12aee23739 Variable and method redeclaration 2019-12-18 11:44:18 +02:00
b7f8182ef5 Bump version: 0.17.1 → 0.17.2 2019-12-13 19:32:29 +02:00
39093accd0 Type hinting. Class parameter defined lists where shared accross instances. 2019-12-13 19:30:43 +02:00
aba8c15fd3 AutoPost medals 2019-12-03 15:22:35 +02:00
f294506a2d Updated wars list, added default weapon choosing (q7 - ground, bare hands - air) 2019-12-03 09:52:53 +02:00
fd56c6c389 By default sort battles by time 2019-12-03 09:44:14 +02:00
4f613ee5ac remvoed unused variables 2019-12-03 09:43:55 +02:00
a7dd528597 Citizen get() and post() signature update, check if server isn't compaining about request flooding 2019-12-03 09:42:51 +02:00
24c755d414 code style 2019-12-03 09:41:07 +02:00
13b639dc5a Bump version: 0.17.0 → 0.17.1 2019-11-21 14:04:49 +02:00
ec1141a46e set serialization 2019-11-21 14:04:43 +02:00
77170433c2 Bump version: 0.16.1 → 0.17.0 2019-11-21 11:12:49 +02:00
4736f70203 History update 2019-11-21 11:12:43 +02:00
48a27997ac Bump version: 0.16.0 → 0.16.1 2019-11-21 11:10:15 +02:00
90bec82630 12th anniversary minimal methods 2019-11-21 11:06:29 +02:00
aedfbf4465 12th anniversary endpoints 2019-11-21 10:42:55 +02:00
66f459c692 Inventory structure update 2019-10-30 19:42:05 +02:00
ef27960ff1 no message 2019-10-30 19:35:40 +02:00
c48af9a891 Thread stopping 2019-10-30 18:16:18 +02:00
1abfdb71ac Code cleanup and serialization improvements 2019-10-30 16:55:33 +02:00
e060f67666 More inventory structure updates 2019-10-29 16:06:07 +02:00
06d8d1c0b5 Telegram threading queue has been messing with error reporting 2019-10-29 16:05:22 +02:00
adda8dcb54 Structure requests by year/month/date folders, to keep requests in cleaner format.
The same medal kind (Maverick div BHs) can have different reward value - group them by kind-reward.
Citizen.post bugfix (with no data and json arguments) TODO: Must check where post is called without data or json
2019-10-28 14:17:45 +02:00
c7f084436d Inventory structure update 2019-10-18 18:18:39 +03:00
94a87091a4 Allow full energy reports once every half an hour 2019-10-17 19:15:17 +03:00
c0b97f112d TelegramBot.send_message should always append to send queue 2019-10-16 15:10:38 +03:00
3d895bd085 Damage calculation 2019-10-16 15:09:28 +03:00
d548d1bbf1 Hit calculation can be static 2019-10-15 20:03:38 +03:00
b1eefcc662 CSRF Attack Detecked loop on POST requests. 2019-10-15 20:03:22 +03:00
41798c446c Return successfully transfered item count 2019-10-15 11:28:56 +03:00
074da3adbe Telegram reporter queue bug 2019-10-14 19:21:00 +03:00
6c9a9e920d Delay telegram notification sending by appending multiple messages to queue and after minute of inactivity clear the queue by sending all messages 2019-10-14 13:44:31 +03:00
ffa2fc109c Travel for fighting fixed 2019-10-14 13:14:30 +03:00
f7f4028f32 Revert "Travel for fighting"
This reverts commit 07c8881092.
2019-10-14 13:03:36 +03:00
e91705ce90 no message 2019-10-14 13:03:19 +03:00
ca65a1ffe1 iPython indexer infinite loop and crash 2019-10-14 12:54:59 +03:00
07c8881092 Travel for fighting 2019-10-13 23:50:45 +03:00
25e534f783 Always print should_fight message 2019-10-08 09:32:00 +03:00
daa071f0f5 RTD build image url fixed, setup.py typo 2019-10-08 09:31:09 +03:00
2f8120bd0d Telegram formatting 2019-10-01 09:59:37 +03:00
6b2c073abe Damage booster activisation update and bugfix 2019-10-01 09:59:22 +03:00
c298d66086 Don't allow to fight before WeekChange even if force_fight (levelup, 75pp etc) 2019-10-01 09:58:37 +03:00
bf971972bf History update with version number 2019-09-29 09:47:15 +03:00
17 changed files with 1227 additions and 525 deletions

View File

@ -19,3 +19,8 @@ insert_final_newline = false
[Makefile]
indent_style = tab
[*.py]
line_length=120
multi_line_output=0
balanced_wrapping=True

View File

@ -1,6 +1,6 @@
* eRepublik script version:
* Python version:
* Operating System:
* eRepublik script version:
* Python version:
* Operating System:
### Description
@ -9,7 +9,7 @@ Tell us what happened, what went wrong, and what you expected to happen.
### What I Did
```
``` python
Paste the command(s) you ran and the output.
If there was a crash, please include the traceback here.
```

2
.gitignore vendored
View File

@ -85,7 +85,7 @@ celerybeat-schedule
# virtualenv
.venv
venv/
venv*/
ENV/
# Spyder project settings

View File

@ -5,7 +5,7 @@ python:
- 3.7
- 3.6
# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
# Command to install dependencies, e.g. pip install -r requirements_dev.txt --use-mirrors
install: pip install -U tox-travis
# Command to run tests, e.g. python setup.py test

View File

@ -23,12 +23,6 @@ If you are reporting a bug, please include:
* Any details about your local setup that might be helpful in troubleshooting.
* Detailed steps to reproduce the bug.
Fix Bugs
~~~~~~~~
Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
wanted" is open to whoever wants to implement it.
Implement Features
~~~~~~~~~~~~~~~~~~
@ -59,7 +53,7 @@ Get Started!
Ready to contribute? Here's how to set up `erepublik` for local development.
1. Fork the `erepublik_script` repo on GitHub.
1. Fork the `erepublik` repo on GitHub.
2. Clone your fork locally::
$ git clone git@github.com:your_name_here/erepublik.git
@ -103,7 +97,7 @@ Before you submit a pull request, check that it meets these guidelines:
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 3.7.1. Check
https://travis-ci.org/eeriks/erepublik_script/pull_requests
https://travis-ci.org/eeriks/erepublik/pull_requests
and make sure that the tests pass for all supported Python versions.
Tips
@ -112,7 +106,7 @@ Tips
To run a subset of tests::
$ python -m unittest tests.test_erepublik_script
$ python -m unittest tests.test_erepublik
Deploying
---------

View File

@ -2,11 +2,40 @@
History
=======
0.19.0 (2020-01-13)
-------------------
* Created method for current products on sale.
* Updated inventory to also include products on sale
* set_default_weapon() - eRepublik should return list with all available weapon qualities, but when a battle is just launched, they return only dict with barehands
* fight() - no longer calls self.set_default_weapon()
* find_battle_and_fight() - now calls self.set_default_weapon() just before fighting
* update_war_info() - returns previous battle list if responses 'last_updated' isn't more than 30s old
* get_battle_for_war(war_id) - returns Battle instance for specific war, if battle is active for given war
* Citizen.get_raw_surplus() fixed and moved to Citizen.my_companies.get_wam_raw_usage()
* Implemented division switching
* improved multi bomb deploy with auto traveling,
* Citizen.fight() simplified battle data gathering logic -> Citizen.shoot logic improved
0.17.0 (2019-11-21)
-------------------
* 12th anniversary's endpoints added
* Telegram message queue optimisation
* WC end fighting energy bugfix
* More strict fighting limiting before week change
* Improved and fixed ground damage booster usage
0.16.0 (2019-09-29)
-------------------
* Telegram notification integration
* Improved serialization to JSON
* When failing to do WAM because of not enough food - buy food
* Buy food buys 48h worth instead of 24h energy
0.15.3 (2019-08-24)
-------------------
@ -25,6 +54,7 @@ History
* Wall post comment endpoints updated with comment create endpoints.
0.1.0 (2019-07-19)
------------------

View File

@ -43,6 +43,8 @@ clean-pyc: ## remove Python file artifacts
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
rm -rf debug/
rm -rf log/
clean-test: ## remove test and coverage artifacts
rm -fr .tox/

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,24 +2,27 @@ import datetime
import decimal
import hashlib
import random
import sys
import threading
import time
import traceback
from collections import deque
from json import JSONDecodeError, loads, JSONEncoder
from typing import Any, Dict, List, Union, Mapping, Iterable, Tuple
from collections import defaultdict, deque
from typing import Any, Dict, Iterable, List, Mapping, Tuple, Union
from requests import Response, Session, post
from erepublik import utils
try:
import simplejson as json
except ImportError:
import json
class ErepublikException(Exception):
def __init__(self, message):
super().__init__(message)
class ErepublikNetworkException(Exception):
class ErepublikNetworkException(ErepublikException):
def __init__(self, message, request):
super().__init__(message)
self.request = request
@ -41,7 +44,7 @@ class MyCompanies:
"""
:param holdings: Parsed JSON to dict from en/economy/myCompanies
"""
self.holdings = {}
self.holdings.clear()
template = dict(id=0, num_factories=0, region_id=0, companies=[])
for holding_id, holding in holdings.items():
@ -58,18 +61,20 @@ class MyCompanies:
"""
:param companies: Parsed JSON to dict from en/economy/myCompanies
"""
self.companies = {}
self.companies.clear()
template = dict(id=None, quality=0, is_raw=False, resource_bonus=0, effective_bonus=0, raw_usage=0,
production=0, base_production=0, wam_enabled=False, can_work_as_manager=False,
base_production=0, wam_enabled=False, can_work_as_manager=False, industry_id=0, todays_works=0,
preset_own_work=0, already_worked=False, can_assign_employees=False, preset_works=0,
todays_works=0, holding_company_id=None, is_assigned_to_holding=False,
cannot_work_as_manager_reason=False, industry_id=0)
holding_company_id=None, is_assigned_to_holding=False, cannot_work_as_manager_reason=False)
for c_id, company in companies.items():
tmp = {}
for key in template.keys():
if key in ['id', 'holding_company_id']:
company[key] = int(company[key])
elif key == "raw_usage":
if not company.get("is_raw") and company.get('upgrades'):
company[key] = company.get('upgrades').get(str(company["quality"])).get('raw_usage')
tmp.update({key: company[key]})
self.companies.update({int(c_id): tmp})
@ -77,9 +82,8 @@ class MyCompanies:
for company_id, company_data in self.companies.items():
if company_id not in self.holdings[company_data['holding_company_id']]['companies']:
self.holdings[company_data['holding_company_id']]['companies'].append(company_id)
else:
for holding_id in self.holdings:
self.holdings[holding_id]['companies'].sort()
for holding_id in self.holdings:
self.holdings[holding_id]['companies'].sort()
def get_employable_factories(self) -> Dict[int, int]:
ret = {}
@ -161,13 +165,38 @@ class MyCompanies:
raise ErepublikException("Wrong function call")
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
def get_wam_raw_usage(self) -> Dict[str, float]:
frm = 0.00
wrm = 0.00
for company in self.companies.values():
if company['wam_enabled']:
effective_bonus = float(company["effective_bonus"])
base_prod = float(company["base_production"])
raw = base_prod * effective_bonus / 100
if not company["is_raw"]:
raw *= -company["raw_usage"]
if company["industry_id"] in [1, 7, 8, 9, 10, 11]:
frm += raw
elif company["industry_id"] in [2, 12, 13, 14, 15, 16]:
wrm += raw
return {'frm': int(frm * 1000) / 1000, 'wrm': int(wrm * 1000) / 1000}
def __str__(self):
name = []
for holding_id in sorted(self.holdings.keys()):
if not holding_id:
name.append(f"Unassigned - {len(self.holdings[0]['companies'])}")
else:
name.append(f"{holding_id} - {len(self.holdings[holding_id]['companies'])}")
return " | ".join(name)
# @property
# def __dict__(self):
# ret = {}
# for key in dir(self):
# if not key.startswith('_'):
# ret[key] = getattr(self, key)
# return ret
class SlowRequests(Session):
@ -175,19 +204,19 @@ class SlowRequests(Session):
timeout = datetime.timedelta(milliseconds=500)
uas = [
# Chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36',
# FireFox
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0',
]
debug = False
@ -202,7 +231,7 @@ class SlowRequests(Session):
@property
def __dict__(self):
return dict(last_time=self.last_time, timeout=self.timeout, user_agent=self.headers['User-Agent'],
request_log_name=self.request_log_name)
request_log_name=self.request_log_name, debug=self.debug)
def request(self, method, url, *args, **kwargs):
self._slow_down_requests()
@ -221,7 +250,10 @@ class SlowRequests(Session):
def _log_request(self, url, method, data=None, json=None, params=None, **kwargs):
if self.debug:
args = {}
args.update({'kwargs': kwargs})
kwargs.pop('allow_redirects', None)
if kwargs:
args.update({'kwargs': kwargs})
if data:
args.update({"data": data})
@ -245,18 +277,17 @@ class SlowRequests(Session):
self._log_request(hist_resp.request.url, "REDIRECT")
self._log_response(hist_resp.request.url, hist_resp, redirect=True)
# TODO: Must thoroughly check response writing on windows systems
file_data = {
"path": 'debug/requests',
"time": self.last_time.strftime('%Y-%m-%d_%H-%M-%S'),
"time": self.last_time.strftime('%Y/%m/%d/%H-%M-%S'),
"name": utils.slugify(url[len(Citizen.url):]),
"extra": "_REDIRECT" if redirect else ""
}
try:
loads(resp.text)
json.loads(resp.text)
file_data.update({"ext": "json"})
except JSONDecodeError:
except json.JSONDecodeError:
file_data.update({"ext": "html"})
filename = 'debug/requests/{time}_{name}{extra}.{ext}'.format(**file_data)
@ -270,7 +301,7 @@ class Config:
work = True
train = True
wam = False
auto_sell: List[str] = list()
auto_sell: List[str] = None
auto_sell_all = False
employees = False
fight = False
@ -294,17 +325,52 @@ class Config:
telegram_chat_id = 0
telegram_token = ""
def __init__(self):
self.auto_sell = []
@property
def wt(self):
return self.work and self.train
def reset(self):
self.work = True
self.train = True
self.wam = False
self.auto_sell = list()
self.auto_sell_all = False
self.employees = False
self.fight = False
self.air = False
self.ground = False
self.all_in = False
self.next_energy = False
self.boosters = False
self.travel_to_fight = False
self.always_travel = False
self.epic_hunt = False
self.epic_hunt_ebs = False
self.rw_def_side = False
self.interactive = True
self.continuous_fighting = False
self.auto_buy_raw = False
self.force_wam = False
self.sort_battles_time = True
self.force_travel = False
self.telegram = True
self.telegram_chat_id = 0
self.telegram_token = ""
@property
def __dict__(self) -> Dict[str, Union[bool, str, List[str]]]:
ret = {}
for key in dir(self):
if not key.startswith('_') and key not in ['email', 'password']:
ret[key] = getattr(self, key)
return ret
def __dict__(self):
return dict(email=self.email, work=self.work, train=self.train, wam=self.wam,
auto_sell=self.auto_sell, auto_sell_all=self.auto_sell_all, employees=self.employees,
fight=self.fight, air=self.air, ground=self.ground, all_in=self.all_in,
next_energy=self.next_energy, boosters=self.boosters, travel_to_fight=self.travel_to_fight,
always_travel=self.always_travel, epic_hunt=self.epic_hunt, epic_hunt_ebs=self.epic_hunt_ebs,
rw_def_side=self.rw_def_side, interactive=self.interactive,
continuous_fighting=self.continuous_fighting, auto_buy_raw=self.auto_buy_raw,
force_wam=self.force_wam, sort_battles_time=self.sort_battles_time, force_travel=self.force_travel,
telegram=self.telegram, telegram_chat_id=self.telegram_chat_id, telegram_token=self.telegram_token)
class Energy:
@ -351,22 +417,14 @@ class Energy:
def available(self):
return self.recovered + self.recoverable
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
class Details(object):
class Details:
xp = 0
cc = 0
pp = 0
pin = None
gold = 0
next_pp: List[int] = []
next_pp: List[int] = None
citizen_id = 0
citizenship = 0
current_region = 0
@ -377,6 +435,9 @@ class Details(object):
daily_task_reward = False
mayhem_skills = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, }
def __init__(self):
self.next_pp = []
@property
def xp_till_level_up(self):
if self.xp >= 10000:
@ -403,14 +464,6 @@ class Details(object):
next_level_up = (1 + (self.xp // 10)) * 10
return next_level_up - self.xp
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
class Politics:
is_party_member: bool = False
@ -421,16 +474,8 @@ class Politics:
is_congressman: bool = False
is_country_president: bool = False
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
class House(object):
class House:
quality = None
unactivated_count = 0
active_untill = utils.good_timedelta(utils.now(), -datetime.timedelta(days=1))
@ -455,8 +500,8 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
"""
self._req = SlowRequests()
def post(self, url: str, *args, **kwargs) -> Response:
return self._req.post(url, *args, **kwargs)
def post(self, url: str, data=None, json=None, **kwargs) -> Response:
return self._req.post(url, data, json, **kwargs)
def get(self, url: str, **kwargs) -> Response:
return self._req.get(url, **kwargs)
@ -467,6 +512,9 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
def _get_military_battlefield_choose_side(self, battle: int, side: int) -> Response:
return self.get("{}/military/battlefield-choose-side/{}/{}".format(self.url, battle, side))
def _get_military_show_weapons(self, battle: int) -> Response:
return self.get("{}/military/show-weapons".format(self.url), params={'_token': self.token, 'battleId': battle})
def _get_candidate_party(self, party_slug: str) -> Response:
return self.post("{}/candidate/{}".format(self.url, party_slug))
@ -532,9 +580,8 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
def _get_military_campaigns(self) -> Response:
return self.get("{}/military/campaigns-new/".format(self.url))
def _get_military_show_weapons(self, battle_id: int) -> Response:
params = {"_token": self.token, "battleId": battle_id}
return self.get("{}/military/show-weapons".format(self.url), params=params)
def _get_military_campaigns_json_list(self) -> Response:
return self.get("{}/military/campaignsJson/list".format(self.url))
def _get_military_unit_data(self, unit_id: int, **kwargs) -> Response:
params = {"groupId": unit_id, "panel": "members", **kwargs}
@ -590,6 +637,10 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
data = dict(_token=self.token, sideCountryId=side_id, battleId=battle_id)
return self.post("{}/main/battlefieldTravel".format(self.url), data=data)
def _post_main_battlefield_change_division(self, battle_id: int, division_id: int) -> Response:
data = dict(_token=self.token, battleZoneId=division_id, battleId=battle_id)
return self.post("{}/main/battlefieldTravel".format(self.url), data=data)
def _post_main_buy_gold_items(self, currency: str, item: str, amount: int) -> Response:
data = dict(itemId=item, currency=currency, amount=amount, _token=self.token)
return self.post("{}/main/buyGoldItems".format(self.url), data=data)
@ -760,6 +811,10 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
data = dict(type=kind, quality=quality, duration=duration, battleId=battle, _token=self.token)
return self.post("{}/military/fight-activateBooster".format(self.url), data=data)
def _post_military_change_weapon(self, battle: int, battle_zone: int, weapon_level: int,) -> Response:
data = dict(battleId=battle, _token=self.token, battleZoneId=battle_zone, customizationLevel=weapon_level)
return self.post("{}/military/change-weapon".format(self.url), 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')
return self.post("{}/login".format(self.url), data=data)
@ -783,21 +838,16 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
data.update(page=page)
return self.post("{}/military/battle-console".format(self.url), data=data)
def _post_military_change_weapon(self, battle_id: int, battle_zone_id: int, customization_level: int) -> Response:
data = dict(_token=self.token, battleZoneId=battle_zone_id, battleId=battle_id,
customizationLevel=customization_level)
return self.post("{}/military/change-weapon".format(self.url), data=data)
def _post_military_deploy_bomb(self, battle_id: int, bomb_id: int) -> Response:
data = dict(battleId=battle_id, bombId=bomb_id, _token=self.token)
return self.post("{}/military/deploy-bomb".format(self.url), data=data)
def _post_military_fight_air(self, battle_id: int, side_id: int) -> Response:
data = dict(sideId=side_id, battleId=battle_id, _token=self.token)
def _post_military_fight_air(self, battle_id: int, side_id: int, zone_id: int) -> Response:
data = dict(sideId=side_id, battleId=battle_id, _token=self.token, battleZoneId=zone_id)
return self.post("{}/military/fight-shoooot/{}".format(self.url, battle_id), data=data)
def _post_military_fight_ground(self, battle_id: int, side_id: int) -> Response:
data = dict(sideId=side_id, battleId=battle_id, _token=self.token)
def _post_military_fight_ground(self, battle_id: int, side_id: int, zone_id: int) -> Response:
data = dict(sideId=side_id, battleId=battle_id, _token=self.token, battleZoneId=zone_id)
return self.post("{}/military/fight-shooot/{}".format(self.url, battle_id), data=data)
def _post_military_group_missions(self) -> Response:
@ -897,29 +947,61 @@ Class for unifying eRepublik known endpoints and their required/optional paramet
data = {"_token": self.token, "post_message": body}
return self.post("{}/main/wall-post/create/json".format(self.url), data=data)
def _post_main_wall_post_automatic(self, **kwargs) -> Response:
kwargs.update(_token=self.token)
return self.post("{}/main/wall-post/create/json".format(self.url), data=kwargs)
def _post_main_wall_post_retrieve(self) -> Response:
data = {"_token": self.token, "page": 1, "switchedFrom": False}
return self.post("{}/main/wall-post/retrieve/json".format(self.url), data=data)
# 12th anniversary endpoints
def _get_anniversary_quest_data(self) -> Response:
return self.get("{}/main/anniversaryQuestData".format(self.url))
def _post_map_rewards_unlock(self, node_id: int) -> Response:
data = {'nodeId': node_id, '_token': self.token}
return self.post("{}/main/map-rewards-unlock".format(self.url), 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}
return self.post("{}/main/map-rewards-speedup".format(self.url), data=data)
def _post_map_rewards_claim(self, node_id: int) -> Response:
data = {'nodeId': node_id, '_token': self.token}
return self.post("{}/main/map-rewards-claim".format(self.url), 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=utils.COUNTRY_LINK[attack_country_id])
return self.post("{}/{}/new-war".format(self.url, utils.COUNTRY_LINK[self_country_id]), 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)
return self.post("{}/{}/new-donation".format(self.url, utils.COUNTRY_LINK[country_id]), data=data)
class Reporter:
__to_update: List[Dict[Any, Any]] = []
__to_update: List[Dict[Any, Any]] = None
name: str = ""
email: str = ""
citizen_id: int = 0
key: str = ""
allowed: bool = False
@property
def __dict__(self):
return dict(name=self.name, email=self.email, citizen_id=self.citizen_id, key=self.key, allowed=self.allowed,
queue=self.__to_update)
def __init__(self):
self._req = Session()
self.url = "https://api.erep.lv"
self._req.headers.update({"user-agent": "Bot reporter v2"})
self.__to_update = []
self.__registered: bool = False
@property
def __dict__(self):
return dict(allowed=self.allowed, __to_update=self.__to_update)
def do_init(self, name: str = "", email: str = "", citizen_id: int = 0):
self.name: str = name
self.email: str = email
@ -979,16 +1061,16 @@ class Reporter:
self.__to_update.append(json_data)
class MyJSONEncoder(JSONEncoder):
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
from erepublik.citizen import Citizen
if isinstance(o, decimal.Decimal):
return float("{:.02f}".format(o))
elif isinstance(o, datetime.datetime):
return dict(__type__='datetime', year=o.year, month=o.month, day=o.day, hour=o.hour, minute=o.minute,
second=o.second, microsecond=o.microsecond, tzinfo=o.tzinfo.zone if o.tzinfo else None)
return dict(__type__='datetime', date=o.strftime("%Y-%m-%d"), time=o.strftime("%H:%M:%S"),
tzinfo=o.tzinfo.zone if o.tzinfo else None)
elif isinstance(o, datetime.date):
return dict(__type__='date', year=o.year, month=o.month, day=o.day)
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())
@ -996,7 +1078,7 @@ class MyJSONEncoder(JSONEncoder):
return dict(headers=o.headers.__dict__, url=o.url, text=o.text)
elif hasattr(o, '__dict__'):
return o.__dict__
elif isinstance(o, deque):
elif isinstance(o, (deque, set)):
return list(o)
elif isinstance(o, Citizen):
return o.to_json()
@ -1015,73 +1097,83 @@ class BattleSide:
self.allies = [int(ally) for ally in allies]
self.deployed = [int(ally) for ally in deployed]
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
class BattleDivision:
end: datetime.datetime
epic: bool
dom_pts: Dict[str, int] = None
wall: Dict[str, Union[int, float]] = None
dom_pts: Dict[str, int]
wall: Dict[str, Union[int, float]]
battle_zone_id: int
def_medal: Dict[str, int]
inv_medal: Dict[str, int]
@property
def div_end(self) -> bool:
return utils.now() >= self.end
def __init__(self, end: datetime.datetime, epic: bool, inv_pts: int, def_pts: int, wall_for: int, wall_dom: float):
self.end = end
self.epic = epic
self.dom_pts = dict({"inv": inv_pts, "def": def_pts})
self.wall = dict({"for": wall_for, "dom": wall_dom})
def __init__(self, **kwargs):
"""Battle division helper class
:param kwargs: must contain keys:
div_id: int, end: datetime.datetime, epic: bool, inv_pts: int, def_pts: int,
wall_for: int, wall_dom: float, def_medal: Tuple[int, int], inv_medal: Tuple[int, int]
"""
self.battle_zone_id = kwargs.get("div_id", 0)
self.end = kwargs.get("end", 0)
self.epic = kwargs.get("epic", 0)
self.dom_pts = dict({"inv": kwargs.get("inv_pts", 0), "def": kwargs.get("def_pts", 0)})
self.wall = dict({"for": kwargs.get("wall_for", 0), "dom": kwargs.get("wall_dom", 0)})
self.def_medal = {"id": kwargs.get("def_medal", 0)[0], "dmg": kwargs.get("def_medal", 0)[1]}
self.inv_medal = {"id": kwargs.get("inv_medal", 0)[0], "dmg": kwargs.get("inv_medal", 0)[1]}
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
def id(self):
return self.battle_zone_id
class Battle(object):
id: int = 0
war_id: int = 0
zone_id: int = 0
is_rw: bool = False
is_dict_lib: bool = False
start: datetime.datetime = None
invader: BattleSide = None
defender: BattleSide = None
div: Dict[int, BattleDivision] = None
class Battle:
id: int
war_id: int
zone_id: int
is_rw: bool
is_dict_lib: bool
start: datetime.datetime
invader: BattleSide
defender: BattleSide
div: Dict[int, BattleDivision]
@property
def is_air(self) -> bool:
return not bool(self.zone_id % 4)
def __init__(self, battle: Dict[str, Any]):
self.id = int(battle.get('id', 0))
self.war_id = int(battle.get('war_id', 0))
self.zone_id = int(battle.get('zone_id', 0))
"""Object representing eRepublik battle.
:param battle: Dict object for single battle from '/military/campaignsJson/list' response's 'battles' object
"""
self.id = int(battle.get('id'))
self.war_id = int(battle.get('war_id'))
self.zone_id = int(battle.get('zone_id'))
self.is_rw = bool(battle.get('is_rw'))
self.is_as = bool(battle.get('is_as'))
self.is_dict_lib = bool(battle.get('is_dict')) or bool(battle.get('is_lib'))
self.start = datetime.datetime.fromtimestamp(int(battle.get('start', 0)), tz=utils.erep_tz)
self.invader = BattleSide(battle.get('inv', {}).get('id'), battle.get('inv', {}).get('points'),
[row.get('id') for row in battle.get('inv', {}).get('ally_list')],
[row.get('id') for row in battle.get('inv', {}).get('ally_list') if row['deployed']])
self.invader = BattleSide(
battle.get('inv', {}).get('id'), battle.get('inv', {}).get('points'),
[row.get('id') for row in battle.get('inv', {}).get('ally_list')],
[row.get('id') for row in battle.get('inv', {}).get('ally_list') if row['deployed']]
)
self.defender = BattleSide(battle.get('def', {}).get('id'), battle.get('def', {}).get('points'),
[row.get('id') for row in battle.get('def', {}).get('ally_list')],
[row.get('id') for row in battle.get('def', {}).get('ally_list') if row['deployed']])
self.defender = BattleSide(
battle.get('def', {}).get('id'), battle.get('def', {}).get('points'),
[row.get('id') for row in battle.get('def', {}).get('ally_list')],
[row.get('id') for row in battle.get('def', {}).get('ally_list') if row['deployed']]
)
self.div = {}
self.div = defaultdict(BattleDivision)
for div, data in battle.get('div', {}).items():
div = int(data.get('div'))
if data.get('end'):
@ -1089,11 +1181,18 @@ class Battle(object):
else:
end = utils.localize_dt(datetime.datetime.max - datetime.timedelta(days=1))
battle_div = BattleDivision(
end=end, epic=data.get('epic_type') in [1, 5],
inv_pts=data.get('dom_pts').get("inv"), def_pts=data.get('dom_pts').get("def"),
wall_for=data.get('wall').get("for"), wall_dom=data.get('wall').get("dom")
)
if not data['stats']['def']:
def_medal = (0, 0)
else:
def_medal = (data['stats']['def']['citizenId'], data['stats']['def']['damage'])
if not data['stats']['inv']:
inv_medal = (0, 0)
else:
inv_medal = (data['stats']['inv']['citizenId'], data['stats']['inv']['damage'])
battle_div = BattleDivision(end=end, epic=data.get('epic_type') in [1, 5], div_id=data.get('id'),
inv_pts=data.get('dom_pts').get("inv"), def_pts=data.get('dom_pts').get("def"),
wall_for=data.get('wall').get("for"), wall_dom=data.get('wall').get("dom"),
def_medal=def_medal, inv_medal=inv_medal)
self.div.update({div: battle_div})
@ -1101,20 +1200,14 @@ class Battle(object):
now = utils.now()
is_started = self.start < utils.now()
if is_started:
time_part = "{}".format(now - self.start)
time_part = " {}".format(now - self.start)
else:
time_part = "- {}".format(self.start - now)
return "Battle {} | {:>21.21}:{:<21.21} | Round {:2} | Start {}".format(
self.id, utils.COUNTRIES[self.invader.id], utils.COUNTRIES[self.defender.id], self.zone_id, time_part
)
time_part = "-{}".format(self.start - now)
@property
def __dict__(self):
ret = {}
for key in dir(self):
if not key.startswith('_'):
ret[key] = getattr(self, key)
return ret
return f"Battle {self.id} | " \
f"{utils.ISO_CC[self.invader.id]} : {utils.ISO_CC[self.defender.id]} | " \
f"Round {self.zone_id:2} | " \
f"Round time {time_part}"
class EnergyToFight:
@ -1150,33 +1243,50 @@ class EnergyToFight:
class TelegramBot:
__initialized = False
__queue: List[str] = []
__queue: List[str]
chat_id = 0
api_url = ""
player_name = ""
__last_time: datetime.datetime = None
__thread_stopper: threading.Event
_last_time: datetime.datetime
_last_full_energy_report: datetime.datetime
_next_time: datetime.datetime
_threads: List[threading.Thread]
def __init__(self, stop_event: threading.Event = None):
self._threads = []
self.__queue = []
self.__thread_stopper = threading.Event() if stop_event is None else stop_event
@property
def __dict__(self):
return dict(chat_id=self.chat_id, api_url=self.api_url, player=self.player_name, last_time=self._last_time,
next_time=self._next_time, queue=self.__queue, initialized=self.__initialized,
has_threads=bool(len(self._threads)))
def do_init(self, chat_id: int, token: str, player_name: str = ""):
self.chat_id = chat_id
self.api_url = "https://api.telegram.org/bot{}/sendMessage".format(token)
self.player_name = player_name
self.__initialized = True
self.__last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5))
self._last_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-5))
self._last_full_energy_report = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=-30))
if self.__queue:
self.send_message("\n\n\n\n".join(self.__queue))
def send_message(self, message: str) -> bool:
self.__queue.append(message)
if not self.__initialized:
self.__queue.append(message)
return True
if self.player_name:
message = f"Player *{self.player_name}*\n" + message
if utils.good_timedelta(utils.now(), datetime.timedelta(seconds=-1)) <= self.__last_time:
tb = traceback.extract_stack()
message += "\n\n```\n{}\n```".format("\n".join([' File "{}", line {}, in {}\n'.format(l.filename, l.lineno, l.name) for l in tb]))
response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown"))
self.__last_time = utils.now()
return response.json().get('ok')
self._threads = [t for t in self._threads if t.is_alive()]
self._next_time = utils.good_timedelta(utils.now(), datetime.timedelta(minutes=1))
if not self._threads:
name = "telegram_{}send".format(f"{self.player_name}_" if self.player_name else "")
send_thread = threading.Thread(target=self.__send_messages, name=name)
send_thread.start()
self._threads.append(send_thread)
return True
def report_free_bhs(self, battles: List[Tuple[int, int, int, int, datetime.timedelta]]):
battle_links = []
@ -1195,8 +1305,26 @@ class TelegramBot:
self.send_message("Free BHs:\n" + "\n".join(battle_links))
def report_full_energy(self, available: int, limit: int, interval: int):
message = f"Full energy ({available}hp/{limit}hp +{interval}hp/6min)"
self.send_message(message)
if (utils.now() - self._last_full_energy_report).total_seconds() >= 30 * 60:
self._last_full_energy_report = utils.now()
message = f"Full energy ({available}hp/{limit}hp +{interval}hp/6min)"
self.send_message(message)
def report_medal(self, msg):
self.send_message(f"New award: *{msg}*")
def __send_messages(self):
while self._next_time > utils.now():
if self.__thread_stopper.is_set():
break
self.__thread_stopper.wait(utils.get_sleep_seconds(self._next_time))
message = "\n\n\n\n".join(self.__queue)
if self.player_name:
message = f"Player *{self.player_name}*\n" + message
response = post(self.api_url, json=dict(chat_id=self.chat_id, text=message, parse_mode="Markdown"))
self._last_time = utils.now()
if response.json().get('ok'):
self.__queue = []
return True
return False

View File

@ -1,6 +1,5 @@
import datetime
import inspect
import json
import os
import re
import sys
@ -8,16 +7,26 @@ import time
import traceback
import unicodedata
from pathlib import Path
from typing import Union, Any, List, NoReturn, Mapping
from typing import Any, List, Mapping, NoReturn, Optional, Union
import pytz
import requests
__all__ = ["FOOD_ENERGY", "COMMIT_ID", "COUNTRIES", "erep_tz",
try:
import simplejson as json
except ImportError:
import json
__all__ = ["FOOD_ENERGY", "COMMIT_ID", "COUNTRIES", "erep_tz", 'COUNTRY_LINK',
"now", "localize_dt", "localize_timestamp", "good_timedelta", "eday_from_date", "date_from_eday",
"get_sleep_seconds", "interactive_sleep", "silent_sleep",
"write_silent_log", "write_interactive_log", "get_file", "write_file",
"send_email", "normalize_html_json", "process_error", ]
"send_email", "normalize_html_json", "process_error", "process_warning", 'report_promo', 'calculate_hit']
if not sys.version_info >= (3, 7):
raise AssertionError('This script requires Python version 3.7 and higher\n'
'But Your version is v{}.{}.{}'.format(*sys.version_info))
FOOD_ENERGY = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20)
COMMIT_ID = "7b92e19"
@ -93,6 +102,15 @@ COUNTRY_LINK = {1: 'Romania', 9: 'Brazil', 11: 'France', 12: 'Germany', 13: 'Hun
81: 'Republic-of-China-Taiwan', 166: 'United-Arab-Emirates', 167: 'Albania', 69: 'Bosnia-Herzegovina',
169: 'Armenia', 83: 'Belarus', 84: 'New-Zealand', 164: 'Saudi-Arabia', 170: 'Nigeria', }
ISO_CC = {1: 'ROU', 9: 'BRA', 10: 'ITA', 11: 'FRA', 12: 'DEU', 13: 'HUN', 14: 'CHN', 15: 'ESP', 23: 'CAN', 24: 'USA',
26: 'MEX', 27: 'ARG', 28: 'VEN', 29: 'GBR', 30: 'CHE', 31: 'NLD', 32: 'BEL', 33: 'AUT', 34: 'CZE', 35: 'POL',
36: 'SVK', 37: 'NOR', 38: 'SWE', 39: 'FIN', 40: 'UKR', 41: 'RUS', 42: 'BGR', 43: 'TUR', 44: 'GRC', 45: 'JPN',
47: 'KOR', 48: 'IND', 49: 'IDN', 50: 'AUS', 51: 'ZAF', 52: 'MDA', 53: 'PRT', 54: 'IRL', 55: 'DNK', 56: 'IRN',
57: 'PAK', 58: 'ISR', 59: 'THA', 61: 'SVN', 63: 'HRV', 64: 'CHL', 65: 'SRB', 66: 'MYS', 67: 'PHL', 68: 'SGP',
69: 'BiH', 70: 'EST', 71: 'LVA', 72: 'LTU', 73: 'PRK', 74: 'URY', 75: 'PRY', 76: 'BOL', 77: 'PER', 78: 'COL',
79: 'MKD', 80: 'MNE', 81: 'TWN', 82: 'CYP', 83: 'BLR', 84: 'NZL', 164: 'SAU', 165: 'EGY', 166: 'UAE',
167: 'ALB', 168: 'GEO', 169: 'ARM', 170: 'NGA', 171: 'CUB'}
def now() -> datetime.datetime:
return datetime.datetime.now(erep_tz).replace(microsecond=0)
@ -113,6 +131,15 @@ def localize_dt(dt: Union[datetime.date, datetime.datetime]) -> datetime.datetim
def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime:
"""Normalize timezone aware datetime object after timedelta to correct jumps over DST switches
:param dt: Timezone aware datetime object
:type dt: datetime.datetime
:param td: timedelta object
:type td: datetime.timedelta
:return: datetime object with correct timezone when jumped over DST
:rtype: datetime.datetime
"""
return erep_tz.normalize(dt + td)
@ -288,10 +315,43 @@ def normalize_html_json(js: str) -> str:
def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None,
interactive: bool = False):
interactive: Optional[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: Code's version by commit id
:type commit_id: str
"""
type_, value_, traceback_ = exc_info
content = [log_info]
if commit_id:
content += ["Commit id: %s" % 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:
trace = trace[-1][0].f_locals
else:
trace = dict()
send_email(name, content, citizen, local_vars=trace)
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()
@ -299,19 +359,17 @@ def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commi
:param commit_id: Code's version by commit id
"""
type_, value_, traceback_ = exc_info
bugtrace = [] if not commit_id else ["Commit id: %s" % commit_id, ]
bugtrace += [str(value_), str(type_), ''.join(traceback.format_tb(traceback_))]
content = [log_info]
if commit_id:
content += ["Commit id: %s" % commit_id]
content += [str(value_), str(type_), ''.join(traceback.format_tb(traceback_))]
if interactive:
write_interactive_log(log_info)
else:
write_silent_log(log_info)
trace = inspect.trace()
if trace:
trace = trace[-1][0].f_locals
else:
trace = dict()
send_email(name, bugtrace, citizen, local_vars=trace)
send_email(name, content, citizen, local_vars=trace)
def report_promo(kind: str, time_untill: datetime.datetime) -> NoReturn:
@ -332,3 +390,43 @@ def slugify(value, allow_unicode=False) -> str:
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) -> float:
base_dmg = 10 * (1 + strength / 400) * (1 + rang / 5) * (1 + weapon / 100)
dmg = int(base_dmg * 10 + 5) // 10
booster_multiplier = (100 + booster) / 100
booster_dmg = dmg * booster_multiplier
dmg = int(booster_dmg * 10 + 5) // 10
elite = 1.1 if elite else 1
elite_dmg = dmg * elite
dmg = int(elite_dmg)
legend = 1 if (not tp or rang < 70) else 1 + (rang - 69) / 10
legend_dmg = dmg * legend
dmg = int(legend_dmg)
return dmg * (1.1 if ne else 1)
def ground_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False,
booster: int = 0, weapon_power: int = 200) -> float:
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 air_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_patriot: bool = False, booster: int = 0,
weapon_power: int = 0) -> float:
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)

View File

@ -0,0 +1,96 @@
import threading
from datetime import timedelta
from erepublik import Citizen, utils
CONFIG = {
'email': 'player@email.com',
'password': 'Pa$5w0rd!',
'interactive': True,
'fight': True,
'debug': True,
'start_battles': {
121672: {"auto_attack": False, "regions": [661]},
125530: {"auto_attack": False, "regions": [259]},
125226: {"auto_attack": True, "regions": [549]},
124559: {"auto_attack": True, "regions": [176]}
}
}
def _battle_launcher(player: Citizen):
"""Launch battles. Check every 5th minute (0,5,10...45,50,55) if any battle could be started on specified regions
and after launching wait for 90 minutes before starting next attack so that all battles aren't launched at the same
time. If player is allowed to fight, do 100 hits on the first round in players division.
:param player: Logged in Citizen instance
":type player: Citizen
"""
global CONFIG
finished_war_ids = {*[]}
war_data = CONFIG.get('start_battles', {})
war_ids = {int(war_id) for war_id in war_data.keys()}
next_attack_time = player.now
next_attack_time = next_attack_time.replace(minute=next_attack_time.minute // 5 * 5, second=0)
while not player.stop_threads.is_set():
try:
attacked = False
player.update_war_info()
running_wars = {b.war_id for b in player.all_battles.values()}
for war_id in war_ids - finished_war_ids - running_wars:
war = war_data[str(war_id)]
war_regions = set(war.get('regions'))
auto_attack = war.get('auto_attack')
status = player.get_war_status(war_id)
if status.get('ended', False):
CONFIG['start_battles'].pop(str(war_id), None)
finished_war_ids.add(war_id)
continue
elif not status.get('can_attack'):
continue
if auto_attack or (player.now.hour > 20 or player.now.hour < 2):
for reg in war_regions:
if attacked:
break
if reg in status.get('regions', {}).keys():
player.launch_attack(war_id, reg, status.get('regions', {}).get(reg))
attacked = True
hits = 100
if player.energy.food_fights >= hits and player.config.fight:
for _ in range(120):
player.update_war_info()
battle_id = player.get_war_status(war_id).get("battle_id")
if battle_id is not None and battle_id in player.all_battles:
player.fight(battle_id, player.details.citizenship, hits)
break
player.sleep(1)
if attacked:
break
if attacked:
break
war_ids -= finished_war_ids
if attacked:
next_attack_time = utils.good_timedelta(next_attack_time, timedelta(hours=1, minutes=30))
else:
next_attack_time = utils.good_timedelta(next_attack_time, timedelta(minutes=5))
player.stop_threads.wait(utils.get_sleep_seconds(next_attack_time))
except:
player.report_error("Task error: start_battles")
def main():
player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False)
player.config.interactive = CONFIG['interactive']
player.config.fight = CONFIG['fight']
player.set_debug(CONFIG.get('debug', False))
player.login()
if CONFIG.get('start_battles'):
name = "{}-start_battles-{}".format(player.name, threading.active_count() - 1)
state_thread = threading.Thread(target=_battle_launcher, args=(player,), name=name)
state_thread.start()
if __name__ == "__main__":
main()

102
examples/eat_work_train.py Normal file
View File

@ -0,0 +1,102 @@
from datetime import timedelta
from erepublik import Citizen, utils
CONFIG = {
'email': 'player@email.com',
'password': 'Pa$5w0rd!',
'interactive': True,
'debug': True
}
def main():
player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False)
player.config.interactive = CONFIG['interactive']
player.config.fight = CONFIG['fight']
player.set_debug(CONFIG.get('debug', False))
player.login()
now = player.now.replace(second=0, microsecond=0)
dt_max = now.replace(year=9999)
tasks = {
'eat': now,
}
if player.config.work:
tasks.update({'work': now})
if player.config.train:
tasks.update({'train': now})
if player.config.ot:
tasks.update({'ot': now})
if player.config.wam:
tasks.update({'wam': now.replace(hour=14, minute=0)})
while True:
player.update_all()
if tasks.get('work', dt_max) <= now:
player.write_log("Doing task: work")
player.update_citizen_info()
player.work()
if player.config.ot:
tasks['ot'] = now
player.collect_daily_task()
next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1))
tasks.update({'work': next_time})
if tasks.get('train', dt_max) <= now:
player.write_log("Doing task: train")
player.update_citizen_info()
player.train()
player.collect_daily_task()
next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1))
tasks.update({'train': next_time})
if tasks.get('wam', dt_max) <= now:
player.write_log("Doing task: Work as manager")
success = player.work_wam()
player.eat()
if success:
next_time = utils.good_timedelta(now.replace(hour=14, minute=0, second=0, microsecond=0),
timedelta(days=1))
else:
next_time = utils.good_timedelta(now.replace(second=0, microsecond=0), timedelta(minutes=30))
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.write_log("Doing task: ot")
if now > player.my_companies.next_ot_time:
player.work_ot()
next_time = now + timedelta(minutes=60)
else:
next_time = player.my_companies.next_ot_time
tasks.update({'ot': next_time})
closest_next_time = dt_max
next_tasks = []
for task, next_time in sorted(tasks.items(), key=lambda s: s[1]):
next_tasks.append("{}: {}".format(next_time.strftime('%F %T'), task))
if next_time < closest_next_time:
closest_next_time = next_time
sleep_seconds = int(utils.get_sleep_seconds(closest_next_time))
if sleep_seconds <= 0:
player.write_log(f"Loop detected! Offending task: '{next_tasks[0]}'")
player.write_log("My next Tasks and there time:\n" + "\n".join(sorted(next_tasks)))
player.write_log("Sleeping until (eRep): {} (sleeping for {}s)".format(
closest_next_time.strftime("%F %T"), sleep_seconds))
seconds_to_sleep = sleep_seconds if sleep_seconds > 0 else 0
player.sleep(seconds_to_sleep)
if __name__ == "__main__":
main()

View File

@ -1,14 +1,16 @@
pip==19.1.1
bumpversion==0.5.3
wheel==0.33.4
watchdog==0.9.0
flake8==3.7.8
tox==3.13.2
coverage==4.5.3
Sphinx==2.2.0
twine==2.0.0
ipython
PyInstaller
pytz==2019.1
coverage==5.0.2
edx-sphinx-theme==1.5.0
flake8==3.7.9
ipython==7.11.1
isort==4.3.21
pip==19.3.1
PyInstaller==3.5
pytz==2019.3
requests==2.22.0
edx-sphinx-theme
setuptools==44.0.0
Sphinx==2.3.1
tox==3.14.3
twine==3.1.1
watchdog==0.9.0
wheel==0.33.6

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.16.0
current_version = 0.19.1
commit = True
tag = True

View File

@ -11,11 +11,11 @@ with open('README.rst') as readme_file:
with open('HISTORY.rst') as history_file:
history = history_file.read()
requirements = ['pytz>=2019.2', 'requests>=2.22']
requirements = ['pytz==2019.3', 'requests==2.22.0']
setup_requirements = [ ]
setup_requirements = []
test_requirements = [ ]
test_requirements = []
setup(
author="Eriks Karls",
@ -27,6 +27,7 @@ setup(
'Natural Language :: English',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
],
description="Python package for automated eRepublik playing",
entry_points={},
@ -42,6 +43,6 @@ setup(
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/',
version='0.16.0',
version='0.19.1',
zip_safe=False,
)

View File

@ -79,9 +79,9 @@ class TestErepublik(unittest.TestCase):
self.citizen.energy.recovered = 3000
self.citizen.energy.recoverable = 2950
self.citizen.energy.interval = 30
self.assertEqual(self.citizen.should_fight(), 895)
self.assertEqual(self.citizen.should_fight(), 900)
self.citizen.my_companies.ff_lockdown = 160
self.assertEqual(self.citizen.should_fight(), 895)
self.assertEqual(self.citizen.should_fight(), 900)
self.citizen.my_companies.ff_lockdown = 0
# Level up reachable