+ {{data.citizenData[post.authorId].politicalTitle}} + {{data.citizenData[post.authorId].name}} + + {{post.createdAtTimeAgo}} +
+ + +{{toUnicode(id)}}+ +
From f9eac109c255c28a0cc945d05d7dd437ef76a70d Mon Sep 17 00:00:00 2001
From: Eriks Karls {{settings.msgs}} {{settings.msgs}} {{settings.msgs}} {{settings.msgs}} {{settings.msgs}}
+
+
+
+
+
+
+ AR
+
+ BE
+
+ BG
+
+ BR
+
+ CN
+
+ DE
+
+ ES
+
+ FA
+
+ FR
+
+ GR
+
+ HE
+
+ HR
+
+ HU
+
+ ID
+
+ IT
+
+ KA
+
+ KO
+
+ MK
+
+ MS
+
+ PL
+
+ PT
+
+ RO
+
+ RU
+
+ SL
+
+ SQ
+
+ SR
+
+ TR
+
+ TW
+
+ UK
+
+ UR
+
A new world is emerging. Your country needs YOU!
+
+ Features
+
+
+
+ Conquer your country's neighbours and extend its territories
+
+ Build a company and develop your economic empire
+
+ Fight against real people on the battlefield
+ Top countries
+
+
+
+ Indonesia
+ 4554
+
+ Serbia
+ 2973
+
+ Brazil
+ 2819
+
+ Hungary
+ 2450
+
+ Argentina
+ 2183
+
What others are saying
+
+
+
+ “eRepublik creates multiplayer global strategy game”
+
+
+
“eRepublik offers a real second life”
+
+
+
“eRepublik takes strategy games to the Web”
+
+
+
Error!
+ Daily Order completed
+
+
+
+
+
My contributions
+ Campaign of the day
+ Latvia's Campaigns
+ Allies' campaigns
+ All campaigns
+
+
+
+
+
+
+
+
+
+
+
+
+ vs
+
+
+
+
+
+
+ {{campaign.region.name}}
+
+
+
+
+
+ {{feedData.unreadCount}}
+
+ {{feedData.name}}
+
+
+
+
+
+
+
+
+ {{data.citizenData[post.authorId].politicalTitle}}
+ {{data.citizenData[post.authorId].name}}
+
+ {{post.createdAtTimeAgo}}
+
+
+
+
+
+
+ {{_getTranslation('wallTexts','comment')}}
+ ·
+
+
+ {{post.numVotes}}
+
+
{{toUnicode(id)}}
+
+ List of eRepublik shortcuts
+
+
+ (press ESC to close)
+Error!
+ Daily Order completed
+
+
+
+
+
My contributions
+ Campaign of the day
+ Latvia's Campaigns
+ Allies' campaigns
+ All campaigns
+
+
+
+
+
+
+
+
+
+
+
+
+ vs
+
+
+
+
+
+
+ {{campaign.region.name}}
+
+
+
+
+
+ {{feedData.unreadCount}}
+
+ {{feedData.name}}
+
+
+
+
+
+
+
+
+ {{data.citizenData[post.authorId].politicalTitle}}
+ {{data.citizenData[post.authorId].name}}
+
+ {{post.createdAtTimeAgo}}
+
+
+
+
+
+
+ {{_getTranslation('wallTexts','comment')}}
+ ·
+
+
+ {{post.numVotes}}
+
+
{{toUnicode(id)}}
+
+
+
+
+
+
+
+ {{post.commentsData.citizenData[comment.authorId].name}}
+
+
+
+
+ {{toUnicode(id)}}
+
+
+
+
+
List of eRepublik shortcuts
+
+
+ (press ESC to close)
+Error!
+ Daily Order completed
+
+
+
+
+
My contributions
+ Campaign of the day
+ Latvia's Campaigns
+ Allies' campaigns
+ All campaigns
+
+
+
+
+
+
+
+
+
+
+
+
+ vs
+
+
+
+
+
+
+ {{campaign.region.name}}
+
+
+
+
+
+ {{feedData.unreadCount}}
+
+ {{feedData.name}}
+
+
+
+
+
+
+
+
+ {{data.citizenData[post.authorId].politicalTitle}}
+ {{data.citizenData[post.authorId].name}}
+
+ {{post.createdAtTimeAgo}}
+
+
+
+
+
+
+ {{_getTranslation('wallTexts','comment')}}
+ ·
+
+
+ {{post.numVotes}}
+
+
{{toUnicode(id)}}
+
+
+
+
+
+
+
+ {{post.commentsData.citizenData[comment.authorId].name}}
+
+
+
+
+ {{toUnicode(id)}}
+
+
+
+
+
List of eRepublik shortcuts
+
+
+ (press ESC to close)
+Error!
+ Daily Order completed
+
+
+
+
+
My contributions
+ Campaign of the day
+ Latvia's Campaigns
+ Allies' campaigns
+ All campaigns
+
+
+
+
+
+
+
+
+
+
+
+
+ vs
+
+
+
+
+
+
+ {{campaign.region.name}}
+
+
+
+
+
+ {{feedData.unreadCount}}
+
+ {{feedData.name}}
+
+
+
+
+
+
+
+
+ {{data.citizenData[post.authorId].politicalTitle}}
+ {{data.citizenData[post.authorId].name}}
+
+ {{post.createdAtTimeAgo}}
+
+
+
+
+
+
+ {{_getTranslation('wallTexts','comment')}}
+ ·
+
+
+ {{post.numVotes}}
+
+
{{toUnicode(id)}}
+
+
+
+
+
+
+
+ {{post.commentsData.citizenData[comment.authorId].name}}
+
+
+
+
+ {{toUnicode(id)}}
+
+
+
+
+
List of eRepublik shortcuts
+
+
+ (press ESC to close)
+
One raw material occupies 100 storage spaces","token":"raw_food","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":13.71},"7_1_partial":{"name":"Food Raw Materials (Under Construction)","id":"7_1_partial","industryId":7,"quality":"1_partial","amount":"13.71%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/7/default.png","tooltip":"Raw material needed to produce food
One raw material occupies 100 storage spaces","token":"raw_food","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"12_1":{"name":"Weapon Raw Materials","id":"12_1","industryId":12,"quality":1,"amount":421,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/12/default.png","tooltip":"Raw material needed to produce weapons
One raw material occupies 100 storage spaces","token":"raw_weapon","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":77.86},"12_1_partial":{"name":"Weapon Raw Materials (Under Construction)","id":"12_1_partial","industryId":12,"quality":"1_partial","amount":"77.86%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/12/default.png","tooltip":"Raw material needed to produce weapons
One raw material occupies 100 storage spaces","token":"raw_weapon","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"17_1":{"name":"House Raw Materials","id":"17_1","industryId":17,"quality":1,"amount":2,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/17/default.png","tooltip":"Raw material needed to produce houses
One raw material occupies 100 storage spaces","token":"sand_q1","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":79.45},"17_1_partial":{"name":"House Raw Materials (Under Construction)","id":"17_1_partial","industryId":17,"quality":"1_partial","amount":"79.45%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/17/default.png","tooltip":"Raw material needed to produce houses
One raw material occupies 100 storage spaces","token":"sand_q1","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"24_1":{"name":"Aircraft Weapons Raw Materials","id":"24_1","industryId":24,"quality":1,"amount":0,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/24/default.png","tooltip":"Raw material needed to produce Air-to-Air Missiles
One raw material occupies 100 storage spaces","token":"magnesium_q1","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":29},"24_1_partial":{"name":"Aircraft Weapons Raw Materials (Under Construction)","id":"24_1_partial","industryId":24,"quality":"1_partial","amount":"29%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/24/default.png","tooltip":"Raw material needed to produce Air-to-Air Missiles
One raw material occupies 100 storage spaces","token":"magnesium_q1","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0}}}},"inventoryStatus":{"totalStorage":1796000,"usedStorage":165873,"color":"green"}}
\ No newline at end of file
diff --git a/debug/requests/2019-07-18_23-40-33_economy-mycompanies.html b/debug/requests/2019-07-18_23-40-33_economy-mycompanies.html
new file mode 100644
index 0000000..d2b1225
--- /dev/null
+++ b/debug/requests/2019-07-18_23-40-33_economy-mycompanies.html
@@ -0,0 +1,8403 @@
+
+
+
+Error!
+ Daily Order completed
+
+
+
+
+
+
+ Companies
+
+
+ Training grounds
+
+
+ Storage
+
+
Advanced buildings
+
+
My job
+
+
+
+
+
+
+
+
+
+ x {{data.overTime.points}}
+
+ My companies How to manage your companies
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
List of eRepublik shortcuts
+
+
+ (press ESC to close)
+\n\t\t\t\tLVL\n\t\t\t\n\t\t\tExchange rate:\n\t\t\t\n\t\t\t\t1\n\t\t\t\t
\n\t\t\t\tLVL\n\t\t\t\t\n\t\t\t=\n\t\t\t\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\tGOLD\n\t\t\t\n\t\t\tSave\n\t\t","buy_mode":"\n\n
\n \n\t\t\t
"}
\ No newline at end of file
diff --git a/debug/requests/2019-07-18_23-40-35_main-weekly-challenge-data.json b/debug/requests/2019-07-18_23-40-35_main-weekly-challenge-data.json
new file mode 100644
index 0000000..a3f5161
--- /dev/null
+++ b/debug/requests/2019-07-18_23-40-35_main-weekly-challenge-data.json
@@ -0,0 +1 @@
+{"error":false,"enabled":true,"type":{"anniversary":false,"flavorPacks":false,"springChallenge":false,"summerChallenge":false,"halloweenChallenge":false},"timeLeft":346765,"nextReward":{"maxReward":false,"type":"icon_energy_booster","text":"+1 Energy recovery until the end of Day 4,262"},"maxRewardId":0,"player":{"avatar":"//cdnt.erepublik.net/7efiav4XZ4SMvXAtgEk1NciUmAg=/55x55/smart/avatars/Citizens/2009/07/08/4b57b9ebb0232f0d6c3f6f2c21b8ab95.jpg?c022b6df6f643263dba839cb35b7a9ab","name":"inpoc1","prestigePoints":14170},"progress":0.9141935483871,"rewards":{"normal":[{"id":59,"collectedBefore":58,"percent":0.74193548387097,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":60,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_booster"},{"id":61,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":62,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_booster"},{"id":63,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":64,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_booster"},{"id":65,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":66,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_booster"},{"id":67,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":68,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_booster"},{"id":69,"collectedBefore":0,"percent":0.016129032258065,"label":"You have already collected this reward","tooltip":"","status":"rewarded","icon":"energy_bars"},{"id":70,"collectedBefore":0,"percent":0.016129032258065,"label":"Reach 14,250 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","tooltip":"Reach 14250 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","status":"","icon":"energy_booster"},{"id":71,"collectedBefore":0,"percent":0.016129032258065,"label":"Reach 14,500 Prestige Points to unlock the following reward: 10 Energy Bars","tooltip":"Reach 14500 Prestige Points to unlock the following reward: 10 Energy Bars","status":"","icon":"energy_bars"},{"id":72,"collectedBefore":0,"percent":0.032258064516129,"label":"Reach 15,000 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","tooltip":"Reach 15000 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","status":"","icon":"energy_booster"},{"id":73,"collectedBefore":0,"percent":0.016129032258065,"label":"Reach 15,250 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","tooltip":"Reach 15250 Prestige Points to unlock the following reward: +1 Energy recovery until the end of Day 4,262","status":"","icon":"energy_booster"},{"id":74,"collectedBefore":0,"percent":0.016129032258065,"label":"Reach 15,500 Prestige Points to unlock the following reward: 15 Energy Bars","tooltip":"Reach 15500 Prestige Points to unlock the following reward: 15 Energy Bars","status":"","icon":"energy_bars"}],"extra":[]}}
\ No newline at end of file
diff --git a/debug/requests/2019-07-18_23-40-56_economy-inventory-items.json b/debug/requests/2019-07-18_23-40-56_economy-inventory-items.json
new file mode 100644
index 0000000..a4887ed
--- /dev/null
+++ b/debug/requests/2019-07-18_23-40-56_economy-inventory-items.json
@@ -0,0 +1 @@
+{"inventoryItems":{"activeEnhancements":{"title":"Active Enhancements","id":"activeEnhancements","items":{"4_1_active":{"name":"House Q1","id":"4_1_active","industryId":4,"quality":1,"amount":1,"activable":0,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":0,"activationData":[],"active":{"uses":168,"time_left":582271},"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/4/q1_55x55_stars.png","tooltip":"Houses increase your maximum Energy and your Energy recovery rate while you are located in your residence City.","token":"house_q1","attributes":{"durability":{"id":"durability","name":"Durability","type":"hours","value":168},"energyPool":{"id":"energyPool","name":"Energy","type":"energy","value":50},"overtimePoints":{"id":"overtimePoints","name":"Overtime Points","type":"hour","value":1},"recoveryRate":{"id":"recoveryRate","name":"Energy recovery","type":"6 minutes","value":2}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"activationCost":0,"activationMessage":"You must establish residence before activationg this house","maxQuality":5},"4_2_active":{"name":"House Q2","id":"4_2_active","industryId":4,"quality":2,"amount":1,"activable":0,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":0,"activationData":[],"active":{"uses":168,"time_left":582272},"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/4/q2_55x55_stars.png","tooltip":"Houses increase your maximum Energy and your Energy recovery rate while you are located in your residence City.","token":"house_q2","attributes":{"durability":{"id":"durability","name":"Durability","type":"hours","value":168},"energyPool":{"id":"energyPool","name":"Energy","type":"energy","value":80},"overtimePoints":{"id":"overtimePoints","name":"Overtime Points","type":"hour","value":1},"recoveryRate":{"id":"recoveryRate","name":"Energy recovery","type":"6 minutes","value":2}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"activationCost":0,"activationMessage":"You must establish residence before activationg this house","maxQuality":5},"4_3_active":{"name":"House Q3","id":"4_3_active","industryId":4,"quality":3,"amount":1,"activable":0,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":0,"activationData":[],"active":{"uses":168,"time_left":582273},"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/4/q3_55x55_stars.png","tooltip":"Houses increase your maximum Energy and your Energy recovery rate while you are located in your residence City.","token":"house_q3","attributes":{"durability":{"id":"durability","name":"Durability","type":"hours","value":168},"energyPool":{"id":"energyPool","name":"Energy","type":"energy","value":100},"overtimePoints":{"id":"overtimePoints","name":"Overtime Points","type":"hour","value":1},"recoveryRate":{"id":"recoveryRate","name":"Energy recovery","type":"6 minutes","value":2}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"activationCost":0,"activationMessage":"You must establish residence before activationg this house","maxQuality":5},"100_damageBoosters_5_225107043_active":{"name":"+50% Damage","id":"100_damageBoosters_5_225107043_active","industryId":100,"quality":5,"amount":"-","activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+50% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":5,"duration":28347,"fromInventory":true}},"active":{"time_left":28347},"icon":0,"tooltip":"50% Damage Booster for 7 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+50% Damage","type":"use","value":"50%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":28347}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":28347,"canActivateBooster":0,"remaining":28347},"100_powerPackBoosters_20_90889528_active":{"name":"Power Pack Booster","id":"100_powerPackBoosters_20_90889528_active","industryId":100,"quality":20,"amount":"-","activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":0,"activationData":{"tooltip":"+20 Energy / 6 minutes","url":"/en/economy/activateBooster","params":{"type":"power_pack","quality":20,"duration":1727405,"fromInventory":true}},"active":{"time_left":1727405},"icon":0,"tooltip":"+20 Energy / 6 minutes for 19 days","token":"","attributes":{"energyRecovery":{"id":"energyRecovery","name":"+20 Energy / 6 minutes","type":" / 6 minutes","value":20},"duration":{"id":"duration","name":"Duration","type":"days","value":1727405}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":1,"type":"powerPackBoosters","duration":1727405,"canActivateBooster":0,"remaining":1727405},"100_blitzkriegPackBoosters_2000_84949724_active":{"name":"Blitzkrieg Pack Booster","id":"100_blitzkriegPackBoosters_2000_84949724_active","industryId":100,"quality":2000,"amount":"-","activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":0,"activationData":{"tooltip":"+2000 Energy Building","url":"/en/economy/activateBooster","params":{"type":"blitzkrieg_pack","quality":2000,"duration":540770,"fromInventory":true}},"active":{"time_left":540770},"icon":0,"tooltip":"+2000 Energy Building for 6 days","token":"","attributes":{"energyPool":{"id":"energyPool","name":"Energy","type":"days","value":2000},"duration":{"id":"duration","name":"Duration","type":"days","value":6}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":1,"type":"blitzkriegPackBoosters","duration":540770,"canActivateBooster":0,"remaining":540770}}},"finalProducts":{"title":"Final products","id":"finalProducts","items":{"1_1":{"name":"Food Q1","id":"1_1","industryId":1,"quality":1,"amount":22413,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/1/q1_55x55_stars.png","tooltip":"Consuming food recovers your Energy","token":"food_q1","attributes":{"energyRestore":{"id":"energyRestore","name":"Energy restore","type":"use","value":2}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"maxQuality":7},"1_2":{"name":"Food Q2","id":"1_2","industryId":1,"quality":2,"amount":41217,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/1/q2_55x55_stars.png","tooltip":"Consuming food recovers your Energy","token":"food_q2","attributes":{"energyRestore":{"id":"energyRestore","name":"Energy restore","type":"use","value":4}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"maxQuality":7},"1_4":{"name":"Food Q4","id":"1_4","industryId":1,"quality":4,"amount":793,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/1/q4_55x55_stars.png","tooltip":"Consuming food recovers your Energy","token":"food_q4","attributes":{"energyRestore":{"id":"energyRestore","name":"Energy restore","type":"use","value":8}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"maxQuality":7},"1_5":{"name":"Food Q5","id":"1_5","industryId":1,"quality":5,"amount":308,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/1/q5_55x55_stars.png","tooltip":"Consuming food recovers your Energy","token":"food_q5","attributes":{"energyRestore":{"id":"energyRestore","name":"Energy restore","type":"use","value":10}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"maxQuality":7},"1_10":{"name":"Energy Bar","id":"1_10","industryId":1,"quality":10,"amount":130,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/1/q10.png","tooltip":"Consuming Energy Bars recovers your Energy","token":"energy_bar","attributes":{"energyRestore":{"id":"energyRestore","name":"Energy restore","type":"use","value":100}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0},"2_7":{"name":"Weapon Q7","id":"2_7","industryId":2,"quality":7,"amount":38,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/2/q7_55x55_stars.png","tooltip":"Using weapons improves your damage in battles","token":"weapon_q7","attributes":{"firePower":{"id":"firePower","name":"Fire power","type":"use","value":200},"durability":{"id":"durability","name":"Durability","type":"uses","value":10}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"used":{"durability":{"id":"durability","name":"Durability","type":"uses","value":3}},"maxQuality":7},"4_100":{"name":"Overtime Points","id":"4_100","industryId":4,"quality":100,"amount":4,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"/images/modules/misc/overtime_points_55x55.png","tooltip":"Used for working overtime","token":"house_q100","attributes":{"info":{"id":"info","name":"Receive one Overtime Point every hour for each active house you own","type":"hours","value":0}},"isRaw":0,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0},"100_damageBoosters_5_86400":{"name":"+50% Damage","id":"100_damageBoosters_5_86400","industryId":100,"quality":5,"amount":53,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+50% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":5,"duration":86400,"fromInventory":true}},"active":0,"icon":0,"tooltip":"50% Damage Booster for 24 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+50% Damage","type":"use","value":"50%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":86400}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":86400,"canActivateBooster":1},"100_damageBoosters_5_28800":{"name":"+50% Damage","id":"100_damageBoosters_5_28800","industryId":100,"quality":5,"amount":62,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+50% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":5,"duration":28800,"fromInventory":true}},"active":0,"icon":0,"tooltip":"50% Damage Booster for 8 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+50% Damage","type":"use","value":"50%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":28800}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":28800,"canActivateBooster":1},"100_damageBoosters_10_86400":{"name":"+100% Damage","id":"100_damageBoosters_10_86400","industryId":100,"quality":10,"amount":5,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+100% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":10,"duration":86400,"fromInventory":true}},"active":0,"icon":0,"tooltip":"100% Damage Booster for 24 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+100% Damage","type":"use","value":"100%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":86400}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":86400,"canActivateBooster":1},"100_damageBoosters_10_28800":{"name":"+100% Damage","id":"100_damageBoosters_10_28800","industryId":100,"quality":10,"amount":14,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+100% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":10,"duration":28800,"fromInventory":true}},"active":0,"icon":0,"tooltip":"100% Damage Booster for 8 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+100% Damage","type":"use","value":"100%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":28800}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":28800,"canActivateBooster":1},"100_damageBoosters_10_7200":{"name":"+100% Damage","id":"100_damageBoosters_10_7200","industryId":100,"quality":10,"amount":5,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"+100% Damage","url":"/en/economy/activateBooster","params":{"type":"damage","quality":10,"duration":7200,"fromInventory":true}},"active":0,"icon":0,"tooltip":"100% Damage Booster for 2 hours","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+100% Damage","type":"use","value":"100%"},"duration":{"id":"duration","name":"Duration","type":"hours","value":7200}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"damageBoosters","duration":7200,"canActivateBooster":1},"100_speedBoosters_2_600":{"name":"x5 Damage Accelerator","id":"100_speedBoosters_2_600","industryId":100,"quality":2,"amount":70,"activable":1,"deactivable":0,"activableFromInventory":1,"activableFromBattlefield":1,"activationData":{"tooltip":"x5 Damage Accelerator","url":"/en/economy/activateBooster","params":{"type":"speed","quality":2,"duration":600,"fromInventory":true}},"active":0,"icon":0,"tooltip":"x5 Damage Accelerator for 10 minutes","token":"","attributes":{"damageAcceleration":{"id":"damageAcceleration","name":"Hit 5 times faster","type":"use","value":"x5"},"duration":{"id":"duration","name":"Duration","type":"minutes","value":600}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"speedBoosters","duration":600,"canActivateBooster":1},"100_catchupBoosters_30_60":{"name":"Ghost Booster","id":"100_catchupBoosters_30_60","industryId":100,"quality":30,"amount":150,"activable":1,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":1,"activationData":{"tooltip":"Ghost Booster availability","url":"/en/military/fight-activateBooster","params":{"type":"catchup","quality":30,"duration":60,"fromInventory":true}},"active":0,"icon":0,"tooltip":"30% Ghost Booster for 1 minute","token":"","attributes":{"damageBoost":{"id":"damageBoost","name":"+30% Damage","type":"use","value":"30%"},"duration":{"id":"duration","name":"Duration","type":"minute","value":1}},"isRaw":0,"isPartial":0,"isBooster":1,"isBomb":0,"isPackBooster":0,"type":"catchupBoosters","duration":60,"canActivateBooster":1}}},"rawMaterials":{"title":"Raw materials","id":"rawMaterials","items":{"7_1":{"name":"Food Raw Materials","id":"7_1","industryId":7,"quality":1,"amount":588,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/7/default.png","tooltip":"Raw material needed to produce food \n All offers \n \n\t\t\n\n\t\n Citizen\n \n \n Amount\n \n \n Rate\n \n \n Buy:\n \n \n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tp.e.d.r.a.m\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t410.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t610.650\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tktiniatros21\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t5.50\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t610.665\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tartemis matsas\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t6.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t610.665\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tEnrico Sodomico\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t1.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t611.999\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tRems.\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t75.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t612.000\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tArmat0re\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t20.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t613.000\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tMast3R.B0Y\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t72.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t613.000\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tgustavo35\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t15.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t613.990\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tatalanta BG\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t10.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t613.990\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t\n\t\t \n\t\t\t\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t\t\tAminta Makedonecot\n\t\t\t\t\n\t\t\t\n
\n\t\t\t\n\t\t\t\t6.00\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t
\n\t\t\t\n\t\t\t\t1\n\t\t\t\t \n\t\t\n\t\t\t\t\n\t\t\t
=
\n\t\t\t\n\t\t\t\t614.000\n\t\t\t\tLVL\n\t\t
\n\t\t\t \n\t \n\t\t\t\n\t\t
One raw material occupies 100 storage spaces","token":"raw_food","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":13.71},"7_1_partial":{"name":"Food Raw Materials (Under Construction)","id":"7_1_partial","industryId":7,"quality":"1_partial","amount":"13.71%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/7/default.png","tooltip":"Raw material needed to produce food
One raw material occupies 100 storage spaces","token":"raw_food","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"12_1":{"name":"Weapon Raw Materials","id":"12_1","industryId":12,"quality":1,"amount":421,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/12/default.png","tooltip":"Raw material needed to produce weapons
One raw material occupies 100 storage spaces","token":"raw_weapon","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":77.86},"12_1_partial":{"name":"Weapon Raw Materials (Under Construction)","id":"12_1_partial","industryId":12,"quality":"1_partial","amount":"77.86%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/12/default.png","tooltip":"Raw material needed to produce weapons
One raw material occupies 100 storage spaces","token":"raw_weapon","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"17_1":{"name":"House Raw Materials","id":"17_1","industryId":17,"quality":1,"amount":2,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/17/default.png","tooltip":"Raw material needed to produce houses
One raw material occupies 100 storage spaces","token":"sand_q1","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":79.45},"17_1_partial":{"name":"House Raw Materials (Under Construction)","id":"17_1_partial","industryId":17,"quality":"1_partial","amount":"79.45%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/17/default.png","tooltip":"Raw material needed to produce houses
One raw material occupies 100 storage spaces","token":"sand_q1","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0},"24_1":{"name":"Aircraft Weapons Raw Materials","id":"24_1","industryId":24,"quality":1,"amount":0,"activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/24/default.png","tooltip":"Raw material needed to produce Air-to-Air Missiles
One raw material occupies 100 storage spaces","token":"magnesium_q1","attributes":[],"isRaw":1,"isPartial":0,"isBooster":0,"isBomb":0,"isPackBooster":0,"underCostruction":29},"24_1_partial":{"name":"Aircraft Weapons Raw Materials (Under Construction)","id":"24_1_partial","industryId":24,"quality":"1_partial","amount":"29%","activable":0,"deactivable":0,"activableFromInventory":0,"activableFromBattlefield":0,"activationData":0,"active":0,"activationTooltip":"","icon":"//www.erepublik.net/images/icons/industry/24/default.png","tooltip":"Raw material needed to produce Air-to-Air Missiles
One raw material occupies 100 storage spaces","token":"magnesium_q1","attributes":[],"isRaw":1,"isPartial":1,"isBooster":0,"isBomb":0,"isPackBooster":0}}}},"inventoryStatus":{"totalStorage":1796000,"usedStorage":165873,"color":"green"}}
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..6376cdf
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = python -msphinx
+SPHINXPROJ = erepublik_script
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/authors.rst b/docs/authors.rst
new file mode 100644
index 0000000..e122f91
--- /dev/null
+++ b/docs/authors.rst
@@ -0,0 +1 @@
+.. include:: ../AUTHORS.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100755
index 0000000..dbd31f2
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# erepublik_script documentation build configuration file, created by
+# sphinx-quickstart on Fri Jun 9 13:47:02 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another
+# directory, add these directories to sys.path here. If the directory is
+# relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('..'))
+
+import erepublik_script
+
+# -- General configuration ---------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'eRepublik script'
+copyright = u"2019, Eriks Karls"
+author = u"Eriks Karls"
+
+# The version info for the project you're documenting, acts as replacement
+# for |version| and |release|, also used in various other places throughout
+# the built documents.
+#
+# The short X.Y version.
+version = erepublik_script.__version__
+# The full version, including alpha/beta/rc tags.
+release = erepublik_script.__version__
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output -------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a
+# theme further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+# -- Options for HTMLHelp output ---------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'erepublik_scriptdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass
+# [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'erepublik_script.tex',
+ u'eRepublik script Documentation',
+ u'Eriks Karls', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'erepublik_script',
+ u'eRepublik script Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'erepublik_script',
+ u'eRepublik script Documentation',
+ author,
+ 'erepublik_script',
+ 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+
+
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..e582053
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1 @@
+.. include:: ../CONTRIBUTING.rst
diff --git a/docs/history.rst b/docs/history.rst
new file mode 100644
index 0000000..2506499
--- /dev/null
+++ b/docs/history.rst
@@ -0,0 +1 @@
+.. include:: ../HISTORY.rst
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..244cf0a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,20 @@
+Welcome to eRepublik script's documentation!
+======================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ readme
+ installation
+ usage
+ modules
+ contributing
+ authors
+ history
+
+Indices and tables
+==================
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..af81116
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,51 @@
+.. highlight:: shell
+
+============
+Installation
+============
+
+
+Stable release
+--------------
+
+To install eRepublik script, run this command in your terminal:
+
+.. code-block:: console
+
+ $ pip install erepublik_script
+
+This is the preferred method to install eRepublik script, as it will always install the most recent stable release.
+
+If you don't have `pip`_ installed, this `Python installation guide`_ can guide
+you through the process.
+
+.. _pip: https://pip.pypa.io
+.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
+
+
+From sources
+------------
+
+The sources for eRepublik script can be downloaded from the `Github repo`_.
+
+You can either clone the public repository:
+
+.. code-block:: console
+
+ $ git clone git://github.com/eeriks/erepublik_script
+
+Or download the `tarball`_:
+
+.. code-block:: console
+
+ $ curl -OL https://github.com/eeriks/erepublik_script/tarball/master
+
+Once you have a copy of the source, you can install it with:
+
+.. code-block:: console
+
+ $ python setup.py install
+
+
+.. _Github repo: https://github.com/eeriks/erepublik_script
+.. _tarball: https://github.com/eeriks/erepublik_script/tarball/master
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..a11302f
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,36 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=python -msphinx
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+set SPHINXPROJ=erepublik_script
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The Sphinx module was not found. Make sure you have Sphinx installed,
+ echo.then set the SPHINXBUILD environment variable to point to the full
+ echo.path of the 'sphinx-build' executable. Alternatively you may add the
+ echo.Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd
diff --git a/docs/readme.rst b/docs/readme.rst
new file mode 100644
index 0000000..72a3355
--- /dev/null
+++ b/docs/readme.rst
@@ -0,0 +1 @@
+.. include:: ../README.rst
diff --git a/docs/usage.rst b/docs/usage.rst
new file mode 100644
index 0000000..0765a49
--- /dev/null
+++ b/docs/usage.rst
@@ -0,0 +1,7 @@
+=====
+Usage
+=====
+
+To use eRepublik script in a project::
+
+ import erepublik_script
diff --git a/erepublik_script/__init__.py b/erepublik_script/__init__.py
new file mode 100644
index 0000000..fd19371
--- /dev/null
+++ b/erepublik_script/__init__.py
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 -*-
+
+"""Top-level package for eRepublik script."""
+
+__author__ = """Eriks Karls"""
+__email__ = 'eriks@72.lv'
+__version__ = '0.1.0'
+
+import json
+import os
+import random
+import sys
+import threading
+from collections import defaultdict
+from datetime import timedelta
+from typing import List, Tuple
+
+from erepublik_script import classes, utils
+from erepublik_script.citizen import Citizen
+
+__all__ = ["Citizen"]
+
+INTERACTIVE = True
+CONFIG = defaultdict(bool)
+
+
+def main():
+ player = None
+ try: # If errors before player is initialized
+ while True:
+ player = Citizen(email=CONFIG['email'], password=CONFIG['password'])
+ if player.logged_in:
+ break
+ utils.silent_sleep(2)
+ player.config.work = CONFIG['work']
+ player.config.train = CONFIG['train']
+ player.config.ot = CONFIG['ot']
+ player.config.wam = bool(CONFIG['wam'])
+ player.config.employees = bool(CONFIG['employ'])
+ player.config.auto_sell = CONFIG.get('auto_sell', [])
+ player.config.auto_sell_all = CONFIG.get('auto_sell_all', False)
+ player.config.auto_buy_raw = CONFIG.get('auto_buy_raw', False)
+ player.config.force_wam = CONFIG.get('force_wam', False)
+ player.config.fight = CONFIG['fight']
+ player.config.air = CONFIG['air']
+ player.config.ground = CONFIG['ground']
+ player.config.all_in = CONFIG['all_in']
+ player.config.next_energy = CONFIG['next_energy']
+ player.config.boosters = CONFIG['boosters']
+ player.config.travel_to_fight = CONFIG['travel_to_fight']
+ player.config.always_travel = CONFIG.get('always_travel', False)
+ player.config.epic_hunt = CONFIG['epic_hunt']
+ player.config.epic_hunt_ebs = CONFIG['epic_hunt_ebs']
+ player.config.rw_def_side = CONFIG['rw_def_side']
+ player.config.random_sleep = CONFIG['random_sleep']
+ player.config.continuous_fighting = CONFIG['continuous_fighting']
+ player.config.interactive = CONFIG['interactive']
+ player.reporter.allowed = not CONFIG.get('reporting_is_not_allowed')
+
+ player.set_debug(CONFIG.get('debug', False))
+ while True:
+ try:
+ player.update_all()
+ break
+ except:
+ utils.silent_sleep(2)
+
+ now = utils.now()
+ dt_max = now.replace(year=9999)
+ tasks = {
+ 'eat': now,
+ }
+ wam_hour = employ_hour = 14
+ 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.fight:
+ tasks.update({'fight': now})
+ if player.config.wam:
+ wam_hour = 14
+ if not isinstance(CONFIG['wam'], bool):
+ try:
+ wam_hour = abs(int(CONFIG['wam'])) % 24
+ except ValueError:
+ pass
+ tasks.update({'wam': now.replace(hour=wam_hour, minute=0, second=0, microsecond=0)})
+ if player.config.employees:
+ employ_hour = 8
+ if not isinstance(CONFIG['employ'], bool):
+ try:
+ employ_hour = abs(int(CONFIG['employ'])) % 24
+ except ValueError:
+ pass
+ tasks.update({'employ': now.replace(hour=employ_hour, minute=0, second=0, microsecond=0)})
+
+ if player.config.epic_hunt:
+ tasks['epic_hunt'] = now
+
+ if CONFIG.get("renew_houses", True):
+ tasks['renew_houses'] = now
+
+ if CONFIG.get('start_battles'):
+ """ {'start_battle': {war_id: {'regions': [region_id, ],
+ 'timing': ['at', 'hh:mm' | 'before', 'hh:mm' (before autoattack) |
+ 'auto' (after round for citizenship country's oldest battle or at 00:00)
+ 'rw', (after first round of RW if you are occupying)]}} """
+ player.allowed_battles = CONFIG.get('start_battles', dict())
+ raise classes.ErepublikException("Battle starting is not implemented")
+
+ if player.reporter.allowed:
+ report = dict(CONFIG)
+ report.pop("email", None)
+ report.pop("password", None)
+ report.update(
+ VERSION=utils.VERSION,
+ COMMIT_ID=utils.COMMIT_ID
+ )
+ player.reporter.report_action("ACTIVE_CONFIG", json_val=report)
+ # -1 because main thread is counted in
+ name = "{}-state_updater-{}".format(player.name, threading.active_count() - 1)
+ state_thread = threading.Thread(target=player.state_update_repeater, name=name)
+ state_thread.start()
+
+ if CONFIG.get("congress", True):
+ tasks['congress'] = now.replace(hour=1, minute=30, second=0)
+
+ if CONFIG.get("party_president", False):
+ tasks['party_president'] = now.replace(hour=1, minute=30, second=0)
+
+ contribute_cc = int(CONFIG.get("contribute_cc", 0))
+ if contribute_cc:
+ tasks['contribute_cc'] = now.replace(hour=2, minute=0, second=0)
+
+ if CONFIG.get("gold_buy"):
+ tasks['gold_buy'] = now.replace(hour=23, minute=57, second=0, microsecond=0)
+
+ error_count = 0
+ while error_count < 3:
+ try:
+ now = utils.now()
+ 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 = 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 = 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 = now.replace(hour=wam_hour, minute=0, second=0, microsecond=0) + timedelta(days=1)
+ else:
+ next_time = 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('fight', dt_max) <= now or player.energy.is_energy_full:
+ fight_energy_debug_log: List[Tuple[int, str]] = []
+ player.write_log("Doing task: fight")
+ player.write_log(player.health_info)
+
+ if player.should_fight():
+ player.find_battle_and_fight()
+ else:
+ player.collect_weekly_reward()
+ energy = classes.EnergyToFight(player.details.xp_till_level_up * 10 - player.energy.limit + 50)
+ fight_energy_debug_log.append((
+ energy.i,
+ f"Levelup reachable {player.details.xp_till_level_up} * 10 - {player.energy.limit} + 50"
+ ))
+
+ # Do levelup
+ energy.check(player.details.xp_till_level_up * 10 + 50)
+ fight_energy_debug_log.append((
+ energy.i, f"Levelup {player.details.xp_till_level_up} * 10 + 50"
+ ))
+
+ # if levelup is close stop queueing other fighting
+ if not player.is_levelup_close:
+
+ # Obligatory need 75pp
+ if player.details.pp < 75:
+ energy.check(75 - player.details.pp)
+ fight_energy_debug_log.append((energy.i, f"Obligatory need 75pp: 75 - {player.details.pp}"))
+
+ if player.config.continuous_fighting and player.has_battle_contribution:
+ energy.check(player.energy.interval)
+ fight_energy_debug_log.append((energy.i, f"continuous_fighting: {player.energy.interval}"))
+
+ # All-in
+ if player.config.all_in:
+ energy.check(player.energy.limit * 2 - 3 * player.energy.interval)
+ fight_energy_debug_log.append((
+ energy.i, f"All-in: {player.energy.limit} * 2 - 3 * {player.energy.interval}"
+ ))
+ elif player.energy.limit * 2 - 3 * player.energy.interval >= player.energy.recovered:
+ # 1h worth of energy
+ energy.check(player.energy.limit * 2 - 3 * player.energy.interval)
+ fight_energy_debug_log.append(
+ (energy.i, f"1h worth of energy: {player.energy.interval} * 10"
+ ))
+
+ # All-in for AIR battles
+ if all([player.config.air, player.config.all_in,
+ player.energy.available >= player.energy.limit]):
+ energy.check(player.energy.limit)
+ fight_energy_debug_log.append((
+ energy.i, f"All-in for AIR battles: {player.energy.limit}"
+ ))
+
+ # Get to next Energy +1
+ if player.next_reachable_energy and player.config.next_energy:
+ energy.check(player.next_reachable_energy * 10)
+ fight_energy_debug_log.append((
+ energy.i, f"Get to next Energy +1: {player.next_reachable_energy} * 10"
+ ))
+
+ energy = energy.i - player.energy.available
+ next_minutes = max([6, abs(energy) // player.energy.interval * 6])
+ # utils.write_silent_log("\n".join([f"{energy} {info}" for energy, info in fight_energy_debug_log]))
+ next_time = player.energy.reference_time + timedelta(minutes=next_minutes)
+ tasks.update({'fight': 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})
+
+ if tasks.get('employ', dt_max) <= now:
+ player.write_log("Doing task: Employee work")
+ next_time = utils.now().replace(hour=employ_hour, minute=0, second=0) + timedelta(days=1)
+ next_time = next_time if player.work_employees() else tasks.get('employ') + timedelta(minutes=30)
+ tasks.update({'employ': next_time})
+
+ if tasks.get('epic_hunt', dt_max) <= now:
+ player.write_log("Doing task: EPIC check")
+ player.check_epic_battles()
+ if player.active_fs:
+ next_time = now + timedelta(minutes=1)
+ else:
+ next_time = tasks.get('eat')
+ tasks.update({'epic_hunt': next_time})
+
+ if tasks.get('gold_buy', dt_max) <= now:
+ player.write_log("Doing task: auto buy 10g")
+ for offer in player.get_monetary_offers():
+ if offer['amount'] >= 10 and player.details.cc >= 20 * offer["price"]:
+ # TODO: check allowed amount to buy
+ if player.buy_monetary_market_offer(offer=offer['offer_id'], amount=10, currency=62):
+ break
+
+ next_time = tasks.get('gold_buy') + timedelta(days=1)
+ tasks.update({'gold_buy': next_time})
+
+ if tasks.get('congress', dt_max) <= now:
+ if 1 <= now.day < 16:
+ next_time = now.replace(day=16)
+ elif 16 <= now.day < 24:
+ player.write_log("Doing task: candidate for congress")
+ player.candidate_for_congress()
+ if not now.month == 12:
+ next_time = now.replace(month=now.month + 1, day=16)
+ else:
+ next_time = now.replace(year=now.year + 1, month=1, day=16)
+ else:
+ if not now.month == 12:
+ next_time = now.replace(month=now.month + 1, day=16)
+ else:
+ next_time = now.replace(year=now.year + 1, month=1, day=16)
+ tasks.update({'congress': next_time.replace(hour=1, minute=30, second=0, microsecond=0)})
+
+ if tasks.get('party_president', dt_max) <= now:
+ if not now.day == 15:
+ player.write_log("Doing task: candidate for party president")
+ player.candidate_for_party_presidency()
+ if not now.month == 12:
+ next_time = now.replace(month=now.month + 1)
+ else:
+ next_time = now.replace(year=now.year + 1, month=1)
+ else:
+ if not now.month == 12:
+ next_time = now.replace(month=now.month + 1)
+ else:
+ next_time = now.replace(year=now.year + 1, month=1)
+ tasks.update(party_president=next_time.replace(day=16, hour=0, minute=0, second=0, microsecond=0))
+
+ if tasks.get('contribute_cc', dt_max) <= now:
+ if not now.weekday():
+ player.update_money()
+ cc = (player.details.cc // contribute_cc) * contribute_cc
+ player.write_log("Doing task: Contribute {}cc to Latvia".format(cc))
+ player.contribute_cc_to_country(cc)
+ next_time = now + timedelta(days=7 - now.weekday())
+ next_time = next_time.replace(hour=2, minute=0, second=0)
+ tasks.update({'contribute_cc': next_time})
+
+ if tasks.get('renew_houses', dt_max) <= now:
+ player.write_log("Doing task: Renew houses")
+ end_times = player.renew_houses()
+ if end_times:
+ tasks.update(renew_houses=min(end_times.values()) - timedelta(hours=24))
+ else:
+ player.write_log("No houses found! Forcing q1 usage...")
+ end_times = player.buy_and_activate_house(1)
+ if not end_times:
+ tasks.update(renew_houses=now + timedelta(hours=6))
+ else:
+ tasks.update(renew_houses=min(end_times.values()) - timedelta(hours=24))
+
+ 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
+ random_seconds = random.randint(0, 121) if player.config.random_sleep else 0
+ sleep_seconds = int(utils.get_sleep_seconds(closest_next_time))
+ if sleep_seconds <= 0:
+ raise classes.ErepublikException(f"Loop detected! Offending task: '{next_tasks[0]}'")
+ closest_next_time += timedelta(seconds=random_seconds)
+ player.write_log("My next Tasks and there time:\n" + "\n".join(sorted(next_tasks)))
+ player.write_log("Sleeping until (eRep): {} (sleeping for {}s + random {}s)".format(
+ closest_next_time.strftime("%F %T"), sleep_seconds, random_seconds))
+ seconds_to_sleep = sleep_seconds + random_seconds if sleep_seconds > 0 else 0
+ player.sleep(seconds_to_sleep)
+
+ except classes.ErepublikNetworkException:
+ player.write_log('Network ERROR detected. Sleeping for 1min...')
+ player.sleep(60)
+ except (KeyboardInterrupt, SystemExit):
+ sys.exit(1)
+ except classes.ErepublikException as e:
+ utils.process_error(f"Known error detected! {e}", player.name, sys.exc_info(), player, utils.COMMIT_ID)
+ except:
+ utils.process_error("Unknown error!", player.name, sys.exc_info(), player, utils.COMMIT_ID)
+ error_count += 1
+ if error_count < 3:
+ player.sleep(60)
+ player.stop_threads.set()
+ player.write_log('Too many errors.')
+ except (KeyboardInterrupt, SystemExit):
+ sys.exit(1)
+ except classes.ErepublikException:
+ utils.process_error("[{}] Fatal error.".format(utils.COMMIT_ID), player.name, sys.exc_info(), player,
+ utils.COMMIT_ID)
+ except:
+ if isinstance(player, Citizen):
+ name = player.name
+ elif CONFIG.get('email', None):
+ name = CONFIG['email']
+ else:
+ name = "Uninitialized"
+ utils.process_error("[{}] Fatal error.".format(utils.COMMIT_ID), name, sys.exc_info(), player, utils.COMMIT_ID)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ assert sys.version_info >= (3, 7, 1)
+
+ write_log = utils.write_silent_log
+
+ try:
+ with open('config.json', 'r') as f:
+ CONFIG = json.load(f)
+
+ write_log('Config file found. Checking...')
+ CONFIG = utils.parse_config(CONFIG)
+ except:
+ CONFIG = utils.parse_config()
+
+ with open('config.json', 'w') as f:
+ json.dump(CONFIG, f, indent=True, sort_keys=True)
+ if CONFIG['interactive']:
+ write_log = utils.write_interactive_log
+ else:
+ write_log = utils.write_silent_log
+ write_log('\nTo quit press [ctrl] + [c]', False)
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
+ write_log('Version: ' + utils.VERSION)
+ while True:
+ main()
+ write_log("Restarting after 1h")
+ utils.interactive_sleep(60 * 60)
diff --git a/erepublik_script/citizen.py b/erepublik_script/citizen.py
new file mode 100644
index 0000000..d21ae4d
--- /dev/null
+++ b/erepublik_script/citizen.py
@@ -0,0 +1,1802 @@
+import datetime
+import re
+import sys
+import threading
+import time
+from json import loads, dumps
+from typing import Dict, List, Tuple, Any, Union
+
+import requests
+from requests import Response, RequestException
+
+from erepublik_script import classes, utils
+
+
+class Citizen(classes.CitizenAPI):
+ url: str = "https://www.erepublik.com/en"
+
+ division = 0
+
+ all_battles: Dict[int, classes.Battle] = dict()
+ countries: Dict[int, Dict[str, Union[str, List[int]]]] = dict()
+ __last_war_update_data = {}
+ __last_full_update: datetime.datetime
+
+ active_fs = False
+
+ food = {"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0, "total": 0}
+ inventory = {"used": 0, "total": 0}
+ boosters = {
+ "100_damageBoosters_5_7200": 0, # 2h × 60min × 60sec, +50%
+ "100_damageBoosters_5_28800": 0, # 8h × 60min × 60sec, +50%
+ "100_damageBoosters_5_86400": 0, # 1d × 24h × 60min × 60sec, +50%
+ "100_speedBoosters_1_180": 0, # 3min × 60sec, ×2 kills
+ "100_speedBoosters_2_600": 0, # 10min × 60sec, ×5 kills
+ "100_catchupBoosters_30_60": 0, # 60sec, +30%
+ }
+
+ candies_normal = 0
+ candies_double = 0
+ candies_small = 0
+ tickets = 0
+
+ work_units = 0
+ ot_points = 0
+
+ tg_contract = {}
+ promos = {}
+
+ eday = 0
+
+ r: requests.Response
+ reporter: classes.Reporter
+ token = ""
+ name = "Not logged in!"
+ debug = False
+ __registered = False
+ logged_in = False
+
+ def __init__(self, email: str = "", password: str = ""):
+ super().__init__()
+ self.commit_id = utils.COMMIT_ID
+ self.config = classes.Config()
+ self.config.email = email
+ self.config.password = password
+ self.energy = classes.Energy()
+ self.details = classes.Details()
+ self.politics = classes.Politics()
+ self.my_companies = classes.MyCompanies()
+ self.set_debug(True)
+ self.reporter = classes.Reporter()
+ self.get_csrf_token()
+ self.update_citizen_info()
+ self.reporter.do_init(self.name, email, self.details.citizen_id)
+ self.stop_threads = threading.Event()
+ self.__last_full_update = utils.good_timedelta(self.now, - datetime.timedelta(minutes=5))
+
+ def write_log(self, *args, **kwargs):
+ if self.config.interactive:
+ utils.write_interactive_log(*args, **kwargs)
+ else:
+ utils.write_silent_log(*args, **kwargs)
+
+ def sleep(self, seconds: int):
+ if self.config.interactive:
+ utils.interactive_sleep(seconds)
+ else:
+ time.sleep(seconds)
+
+ def __str__(self) -> str:
+ return "Citizen {}".format(self.name)
+
+ @property
+ def __dict__(self):
+ ret = super().__dict__.copy()
+ ret.pop('reporter', None)
+ ret.pop('stop_threads', None)
+
+ return ret
+
+ def set_debug(self, debug: bool):
+ self.debug = debug
+ self._req.debug = debug
+
+ def set_pin(self, pin: int):
+ self.details.pin = pin
+
+ def get_csrf_token(self):
+ """
+ get_csrf_token is the function which logs you in, and updates csrf tokens
+ (after 15min time of inactivity opening page in eRepublik.com redirects to home page),
+ by explicitly requesting homepage.
+ """
+ resp = self._req.get(self.url)
+ self.r = resp
+ try:
+ self.update_citizen_info(resp.text)
+ except:
+ pass
+ if self._errors_in_response(resp):
+ self.get_csrf_token()
+ return
+
+ html = resp.text
+ self.check_for_new_medals(html)
+ re_token = re.search(r'var csrfToken = \'(\w{32})\'', html)
+ re_login_token = re.search(r'', html)
+ if re_token:
+ self.token = re_token.group(1)
+ elif re_login_token:
+ self.token = re_login_token.group(1)
+ self._login()
+ else:
+ raise classes.ErepublikException("Something went wrong! Can't find token in page! Exiting!")
+
+ def _login(self):
+ # MUST BE CALLED TROUGH self.get_csrf_token()
+ r = self.post_login(self.token, self.config.email, self.config.password)
+ self.r = r
+
+ if r.url == "{}/login".format(self.url):
+ self.write_log("Citizen email and/or password is incorrect!")
+ raise KeyboardInterrupt
+ else:
+ re_name_id = re.search(r'', r.text)
+ self.name = re_name_id.group(2)
+ self.details.citizen_id = re_name_id.group(1)
+
+ self.write_log("Logged in as: {}".format(self.name))
+ self.get_csrf_token()
+ self.logged_in = True
+
+ def _errors_in_response(self, response: requests.Response):
+ if response.status_code >= 400:
+ self.r = response
+ if response.status_code >= 500:
+ self.write_log("eRepublik servers are having internal troubles. Sleeping for 5 minutes")
+ self.sleep(5 * 60)
+ else:
+ raise classes.ErepublikException("HTTP {} error!".format(response.status_code))
+ return bool(re.search(r'body id="error"|Internal Server Error|'
+ r'CSRF attack detected|meta http-equiv="refresh"|not_authenticated', response.text))
+
+ def get(self, url: str, *args, **kwargs) -> Response:
+ if (self.now - self._req.last_time).seconds >= 15 * 60:
+ self.get_csrf_token()
+ if "params" in kwargs:
+ if "_token" in kwargs["params"]:
+ kwargs["params"]["_token"] = self.token
+ if url == self.r.url and not url == self.url: # Don't duplicate requests, except for homepage
+ response = self.r
+ else:
+ try:
+ response = super().get(url, **kwargs)
+ except RequestException as e:
+ self.write_log("Network error while issuing GET request", e)
+ self.sleep(60)
+ return self.get(url, *args, **kwargs)
+
+ try:
+ self.update_citizen_info(html=response.text)
+ except:
+ pass
+
+ if self._errors_in_response(response):
+ self.get_csrf_token()
+ self.get(url, **kwargs)
+ else:
+ self.check_for_new_medals(response.text)
+
+ self.r = response
+ return response
+
+ def post(self, url: str, data: dict = None, json: dict = None, **kwargs) -> Response:
+ if (self.now - self._req.last_time).seconds >= 14 * 60:
+ self.get_csrf_token()
+ if "_token" in data:
+ data["_token"] = self.token
+ if "_token" in json:
+ json["_token"] = self.token
+
+ try:
+ response = super().post(url, data=data, json=json, **kwargs)
+ except RequestException as e:
+ self.write_log("Network error while issuing POST request", e)
+ self.sleep(60)
+ return self.post(url, data, json, **kwargs)
+
+ # response = super().post(url, data=data, json=json, **kwargs)
+ try:
+ resp_data = response.json()
+ if (resp_data.get("error") or not resp_data.get("status")) and resp_data.get("message", "") == "captcha":
+ utils.send_email(self.name, [response.text, ], player=self, captcha=True)
+ except:
+ pass
+
+ if self._errors_in_response(response):
+ self.get_csrf_token()
+ if data:
+ data.update({"_token": self.token})
+ elif json:
+ json.update({"_token": self.token})
+ response = self.post(url, data=data, json=json, **kwargs)
+ else:
+ self.check_for_new_medals(response.text)
+
+ self.r = response
+ return response
+
+ def check_for_new_medals(self, html: str):
+ new_medals = re.findall(r'(New Achievement
.*?
Congratulations, you have reached experience level (\d+)
", html) + if levelup: + level = levelup.group(1) + msg = "Level up! Current level {}".format(level) + self.write_log(msg) + self.reporter.report_action("LEVEL_UP", value=level) + + def update_all(self, force_update=False): + # Do full update max every 5 min + if utils.good_timedelta(self.__last_full_update, datetime.timedelta(minutes=5)) > self.now and not force_update: + return + else: + self.__last_full_update = self.now + self.update_citizen_info() + self.update_war_info() + self.update_inventory() + self.update_companies() + self.update_money() + self.update_weekly_challenge() + self.send_state_update() + + def update_citizen_info(self, html: str = None): + """ + Gets main page and updates most information about player + """ + if html is None: + self.get_main() + html = self.r.text + ugly_js = re.search(r"promotions: (\[{?.*}?]),\s+", html).group(1) + promos = loads(utils.normalize_html_json(ugly_js)) + self.promos = {k: v for k, v in self.promos.items() if v > self.now} + send_mail = False + for promo in promos: + promo_name = promo.get("id") + expire = utils.localize_timestamp(int(promo.get("expiresAt"))) + if promo_name not in self.promos: + send_mail = True + self.promos.update({promo_name: expire}) + if promo_name == "trainingContract": + if not self.tg_contract: + self.train() + if not self.tg_contract["free_train"] and self.tg_contract.get("active", False): + if self.details.gold >= 54: + self.buy_tg_contract() + else: + self.write_log("Training ground contract active but don't have enough gold ({}g {}cc)".format( + self.details.gold, self.details.cc + )) + if send_mail: + active_promos = ["{} active until {}".format(k, v.strftime("%F %T")) for k, v in self.promos.items()] + utils.send_email(self.name, active_promos, player=self, promo=True) + + new_date = re.search(r"var new_date = '(\d*)';", html) + if new_date: + self.energy.set_reference_time( + utils.good_timedelta(self.now, datetime.timedelta(seconds=int(new_date.group(1)))) + ) + + ugly_js = re.search(r"var erepublik = ({.*}),\s+", html).group(1) + citizen_js = loads(ugly_js) + citizen = citizen_js.get("citizen", {}) + + self.eday = citizen_js.get("settings").get("eDay") + self.division = int(citizen.get("division", 0)) + + self.energy.interval = citizen.get("energyPerInterval", 0) + self.energy.limit = citizen.get("energyToRecover", 0) + self.energy.recovered = citizen.get("energy", 0) + self.energy.recoverable = citizen.get("energyFromFoodRemaining", 0) + + self.details.current_region = citizen.get("regionLocationId", 0) + self.details.current_country = citizen.get("countryLocationId", 0) # country where citizen is located + self.details.residence_region = citizen.get("residence", {}).get("regionId", 0) + self.details.residence_country = citizen.get("residence", {}).get("countryId", 0) + self.details.citizen_id = citizen.get("citizenId", 0) + self.details.citizenship = int(citizen.get("country", 0)) + self.details.xp = citizen.get("currentExperiencePoints", 0) + self.details.daily_task_done = citizen.get("dailyTasksDone", False) + self.details.daily_task_reward = citizen.get("hasReward", False) + # if citizen.get("dailyOrderDone", False) and not citizen.get("hasDailyOrderReward", False): + # self.post_military_group_missions(self.token) + # self.get_citizen_daily_assistant() + + self.details.next_pp.sort() + for id_, skill in citizen.get("mySkills", {}).items(): + self.details.mayhem_skills.update({int(skill["terrain_id"]): int(skill["skill_points"])}) + + if citizen.get('party', []): + party = citizen.get('party') + self.politics.is_party_member = True + self.politics.party_id = party.get('party_id') + self.politics.is_party_president = bool(party.get('is_party_president')) + self.politics.party_slug = "{}-{}".format(party.get("stripped_title"), party.get('party_id')) + + def update_money(self, page: int = 0, currency: int = 62) -> Response: + """ + Gets monetary market offers to get exact amount of CC and Gold available + """ + if currency not in [1, 62]: + currency = 62 + resp = self.post_economy_exchange_retrieve(self.token, False, page, currency) + self.details.cc = float(resp.json().get("ecash").get("value")) + self.details.gold = float(resp.json().get("gold").get("value")) + return resp + + def update_job_info(self): + ot = self.get_job_data().json().get("overTime", {}) + if ot: + self.my_companies.next_ot_time = utils.localize_timestamp(int(ot.get("nextOverTime", 0))) + self.ot_points = ot.get("points", 0) + + def update_companies(self): + html = self.get_economy_my_companies().text + page_details = loads(re.search(r"var pageDetails\s+= ({.*});", html).group(1)) + self.my_companies.work_units = int(page_details.get("total_works", 0)) + + have_holdings = re.search(r"var holdingCompanies\s+= ({.*}});", html) + have_companies = re.search(r"var companies\s+= ({.*}});", html) + if have_holdings and have_companies: + self.my_companies.prepare_companies(loads(have_companies.group(1))) + self.my_companies.prepare_holdings(loads(have_holdings.group(1))) + self.my_companies.update_holding_companies() + + def update_inventory(self) -> dict: + j = self.get_economy_inventory_items().json() + + self.inventory.update({"used": j.get("inventoryStatus").get("usedStorage"), + "total": j.get("inventoryStatus").get("totalStorage")}) + final = j.get("inventoryItems").get("finalProducts").get("items") + self.food.update({ + "q1": final.get("1_1", {"amount": 0}).get("amount", 0), + "q2": final.get("1_2", {"amount": 0}).get("amount", 0), + "q3": final.get("1_3", {"amount": 0}).get("amount", 0), + "q4": final.get("1_4", {"amount": 0}).get("amount", 0), + "q5": final.get("1_5", {"amount": 0}).get("amount", 0), + "q6": final.get("1_6", {"amount": 0}).get("amount", 0), + "q7": final.get("1_7", {"amount": 0}).get("amount", 0), + }) + self.boosters.update({ + "100_damageBoosters_5_7200": final.get("100_damageBoosters_5_7200", {"amount": 0}).get("amount", 0), + "100_damageBoosters_5_28800": final.get("100_damageBoosters_5_28800", {"amount": 0}).get("amount", 0), + "100_damageBoosters_5_86400": final.get("100_damageBoosters_5_86400", {"amount": 0}).get("amount", 0), + "100_speedBoosters_1_180": final.get("100_speedBoosters_1_180", {"amount": 0}).get("amount", 0), + "100_speedBoosters_2_600": final.get("100_speedBoosters_2_600", {"amount": 0}).get("amount", 0), + "100_catchupBoosters_30_60": final.get("100_catchupBoosters_30_60", {"amount": 0}).get("amount", 0), + }) + self.candies_normal = final.get("1_10", {"amount": 0}).get("amount", 0) + self.candies_double = final.get("1_11", {"amount": 0}).get("amount", 0) + self.candies_small = final.get("1_12", {"amount": 0}).get("amount", 0) + self.ot_points = final.get("4_100", {"amount": 0}).get("amount", 0) + self.tickets = final.get("3_5", {"amount": 0}).get("amount", 0) + + self.food["total"] = sum([self.food[q] * utils.FOOD_ENERGY[q] for q in utils.FOOD_ENERGY]) + return j + + def update_weekly_challenge(self): + data = self.get_weekly_challenge_data().json() + self.details.pp = data.get("player", {}).get("prestigePoints", 0) + self.details.next_pp = [] + for reward in data.get("rewards", {}).get("normal", {}): + status = reward.get("status", "") + if status == "rewarded": + continue + elif status == "completed": + data = { + "_token": self.token, + "rewardId": reward.get("id", 0) + } + self.post_weekly_challenge_reward(self.token, reward.get("id", 0)) + elif reward.get("icon", "") == "energy_booster": + pps = re.search(r"Reach (\d+) Prestige Points to unlock the following reward: \+1 Energy", + reward.get("tooltip", "")) + if pps: + self.details.next_pp.append(int(pps.group(1))) + + def update_war_info(self) -> Dict[Any, Any]: + if not self.details.current_country: + self.update_citizen_info() + + resp_json = self.get_military_campaigns().json() + if resp_json.get("countries"): + for c_id, c_data in resp_json.get("countries").items(): + if int(c_id) not in self.countries: + self.countries.update({ + int(c_id): {"name": c_data.get("name"), "allies": c_data.get("allies")} + }) + else: + self.countries[int(c_id)].update(allies=c_data.get("allies")) + self.__last_war_update_data = resp_json + if resp_json.get("battles"): + for battle_id, battle_data in resp_json.get("battles", {}).items(): + self.all_battles.update({int(battle_id): classes.Battle(battle_data)}) + return self.__last_war_update_data + + def eat(self): + """ + Try to eat food + """ + self.update_citizen_info() + self.update_inventory() + if self.details.xp_till_level_up > (self.energy.recovered - 50) // 10: + if self.food["total"] > self.energy.interval: + if self.energy.limit - self.energy.recovered > self.energy.interval or not self.energy.recoverable % 2: + self._eat("blue") + else: + self.write_log("I don't want to eat right now!") + else: + self.write_log("I'm out of food! But I'll try to buy some!\n{}".format(self.food)) + self.buy_food() + self.update_inventory() + if self.food["total"] > self.energy.interval: + self.eat() + else: + self.write_log("I failed to buy food") + else: + self.write_log("I'm not allowed to eat because I have levelup coming up!") + self.write_log(self.health_info) + + def eat_ebs(self): + self.write_log("Eating energy bar") + self.update_citizen_info() + if self.energy.recoverable: + self._eat("blue") + self._eat("orange") + self.write_log(self.health_info) + + def _eat(self, colour: str = "blue") -> Response: + response = self.post_eat(self.token, colour) + r_json = response.json() + next_recovery = r_json.get("food_remaining_reset").split(":") + self.energy.set_reference_time( + utils.good_timedelta(self.now, + datetime.timedelta(seconds=int(next_recovery[1]) * 60 + int(next_recovery[2]))) + ) + self.energy.recovered = r_json.get("health") + self.energy.recoverable = r_json.get("food_remaining") + for q, amount in r_json.get("units_consumed").items(): + if "q{}".format(q) in self.food: + self.food["q{}".format(q)] -= amount + elif q == "10": + self.candies_normal -= amount + elif q == "11": + self.candies_double -= amount + elif q == "12": + self.candies_small -= amount + return response + + @property + def health_info(self): + self.update_citizen_info() + ret = "{}/{} + {}, {}hp/6m. {}xp until level up".format( + self.energy.recovered, + self.energy.limit, + self.energy.recoverable, + self.energy.interval, + self.details.xp_till_level_up + ) + return ret + + @property + def now(self) -> datetime.datetime: + """ + Returns aware datetime object localized to US/Pacific (eRepublik time) + :return: datetime.datetime + """ + return utils.now() + + def check_epic_battles(self): + active_fs = False + for battle_id in self.sorted_battles(self.config.sort_battles_time): + battle = self.all_battles.get(battle_id) + if not battle.is_air: + my_div: classes.BattleDivision = battle.div.get(self.division) + if my_div.epic and my_div.end > self.now: + if self.energy.food_fights > 50: + inv_allies = battle.invader.deployed + [battle.invader.id] + def_allies = battle.defender.deployed + [battle.defender.id] + all_allies = inv_allies + def_allies + if self.details.current_country not in all_allies: + self._travel(battle.defender.id, self.get_country_travel_region(battle.defender.id)) + side = battle.defender.id + else: + if self.details.current_country in inv_allies: + side = battle.invader.id + elif self.details.current_country in def_allies: + side = battle.defender.id + else: + self.write_log( + "Country {} not in all allies list ({}) and also not in inv allies ({}) nor def " + "allies ({})".format(self.details.current_country, all_allies, + inv_allies, def_allies)) + break + error_count = 0 + while self.energy.food_fights > 5 and error_count < 20: + errors = self.fight(battle_id, side_id=side, is_air=False, + count=self.energy.food_fights - 5) + if errors: + error_count += errors + if self.config.epic_hunt_ebs: + self.eat_ebs() + self.travel_to_residence() + break + elif bool(my_div.epic): + active_fs = True + + self.active_fs = active_fs + + def sorted_battles(self, sort_by_time: bool = False) -> List[int]: + r = self.update_war_info() + cs_battles_air: List[int] = [] + cs_battles_ground: List[int] = [] + deployed_battles_air: List[int] = [] + deployed_battles_ground: List[int] = [] + ally_battles_air: List[int] = [] + ally_battles_ground: List[int] = [] + other_battles_air: List[int] = [] + other_battles_ground: List[int] = [] + for bid, battle in sorted(self.all_battles.items(), key=lambda b: b[1].start if sort_by_time else b[0], + reverse=sort_by_time): + battle_sides = [battle.invader.id, battle.defender.id] + + # CS Battles + if self.details.citizenship in battle_sides: + if battle.is_air: + cs_battles_ground.append(battle.id) + else: + cs_battles_air.append(battle.id) + + # Current location battles: + elif self.details.current_country in battle_sides: + if battle.is_air: + deployed_battles_ground.append(battle.id) + else: + deployed_battles_air.append(battle.id) + + # Deployed battles and allied battles: + elif self.details.current_country in battle.invader.allies + battle.defender.allies + battle_sides: + if self.details.current_country in battle.invader.deployed + battle.defender.deployed: + if battle.is_air: + deployed_battles_ground.append(battle.id) + else: + deployed_battles_air.append(battle.id) + # Allied battles: + else: + if battle.is_air: + ally_battles_ground.append(battle.id) + else: + ally_battles_air.append(battle.id) + else: + if battle.is_air: + other_battles_ground.append(battle.id) + else: + other_battles_air.append(battle.id) + + ret_battles = [] + if r.get("citizen_contribution"): + battle_id = r.get("citizen_contribution")[0].get("battle_id", 0) + ret_battles.append(battle_id) + + ret_battles += (cs_battles_air + cs_battles_ground + + deployed_battles_air + deployed_battles_ground + + ally_battles_air + ally_battles_ground + + other_battles_air + other_battles_ground) + return ret_battles + + @property + def has_battle_contribution(self): + return bool(self.update_war_info().get("citizen_contribution", [])) + + def find_battle_and_fight(self): + if self.should_fight(False): + self.write_log("Checking for battles to fight in...") + for battle_id in self.sorted_battles(self.config.sort_battles_time): + battle = self.all_battles.get(battle_id) + div = 11 if battle.is_air else self.division + + allies = battle.invader.deployed + battle.defender.deployed + [battle.invader.id, battle.defender.id] + + travel_needed = self.details.current_country not in allies + + if battle.is_rw: + side_id = battle.defender.id if self.config.rw_def_side else battle.invader.id + else: + side_id = battle.defender.id if (self.details.current_country in battle.defender.allies + + [battle.defender.id, ]) else battle.invader.id + try: + def_points = battle.div.get(div).dom_pts.get('def') + inv_points = battle.div.get(div).dom_pts.get('inv') + except KeyError: + self.report_error(f"Division {div} not available for battle {battle.id}!") + def_points = inv_points = 3600 + kwargs = { + "bid": battle.id, + "air": "air" if battle.is_air else "ground", + "rw": "True" if battle.is_rw else "False", + "def": def_points, + "inv": inv_points, + "travel": "(TRAVEL)" if travel_needed else "", + } + self.write_log("Battle {bid}, type: {air:6}, rw: {rw:5}, " + "points: {def:4}:{inv:<4} {travel}".format(**kwargs)) + + points = def_points <= 1700 and inv_points <= 1700 + b_type = battle.is_air and self.config.air or not battle.is_air and self.config.ground + travel = (self.config.travel_to_fight and self.should_travel_to_fight() or self.config.force_travel) \ + if travel_needed else True + + if not (points and b_type and travel): + continue + + if battle.start > self.now: + self.sleep(utils.get_sleep_seconds(battle.start)) + + if travel_needed: + if battle.is_rw: + self._travel(battle.defender.id, self.get_country_travel_region(battle.defender.id)) + elif self.details.current_country not in battle.invader.allies: + self.travel(battle_id=battle.id) + side_id = battle.invader.id + else: + self._travel(battle.defender.id, self.get_country_travel_region(battle.defender.id)) + side_id = battle.defender.id + + self.fight(battle_id, side_id, battle.is_air) + self.travel_to_residence() + self.collect_weekly_reward() + break + + def fight(self, battle_id: int, side_id: int, is_air: bool = False, count: int = None): + data = dict(sideId=side_id, battleId=battle_id, _token=self.token) + error_count = 0 + ok_to_fight = True + if count is None: + count = self.should_fight(silent=False) + + total_damage = 0 + total_hits = 0 + + while ok_to_fight and error_count < 10: + while all((count > 0, error_count < 10, self.energy.recovered >= 50)): + hits, error, damage = self._shoot(is_air, data) + count -= hits + total_hits += hits + total_damage += damage + error_count += error + else: + self.eat() + if self.energy.recovered < 50 or error_count >= 10 or count <= 0: + self.write_log("Hits: {:>4} | Damage: {}".format(total_hits, total_damage)) + ok_to_fight = False + if total_damage: + self.reporter.report_action(json_val=dict(battle=battle_id, side=side_id, dmg=total_damage, + air=is_air, hits=total_hits), action="FIGHT") + if error_count: + return error_count + + def _shoot(self, air: bool, data: dict): + if air: + response = self.post_military_fight_air(self.token, data['battleId'], data['sideId']) + else: + response = self.post_military_fight_ground(self.token, data['battleId'], data['sideId']) + + if "Zone is not meant for " in response.text: + self.sleep(5) + return 0, 1, 0 + try: + j_resp = response.json() + except: + return 0, 10, 0 + hits = 0 + damage = 0 + err = False + if j_resp.get("error"): + if j_resp.get("message") == "SHOOT_LOCKOUT": + pass + else: + if j_resp.get("message") == "UNKNOWN_SIDE": + self._rw_choose_side(data["battleId"], data["sideId"]) + err = True + elif j_resp.get("message") == "ENEMY_KILLED": + hits = (self.energy.recovered - j_resp["details"]["wellness"]) // 10 + self.energy.recovered = j_resp["details"]["wellness"] + damage = j_resp["user"]["givenDamage"] * (1.1 if j_resp["oldEnemy"]["isNatural"] else 1) + else: + err = True + + return hits, err, damage + + def work_ot(self): + # I"m not checking for 1h cooldown. Beware of nightshift work, if calling more than once every 60min + self.update_job_info() + if self.ot_points >= 24 and self.energy.food_fights > 1: + r = self.post_economy_work_overtime(self.token) + if not r.json().get("status") and r.json().get("message") == "money": + self.resign() + self.find_new_job() + else: + self.reporter.report_action("WORK_OT", r.json()) + elif self.energy.food_fights < 1 and self.ot_points >= 24: + self._eat("blue") + if self.energy.food_fights < 1: + large = max(self.energy.reference_time, self.now) + small = min(self.energy.reference_time, self.now) + self.write_log("I don't have energy to work OT. Will sleep for {}s".format((large - small).seconds)) + self.sleep(int((large - small).total_seconds())) + self._eat("blue") + self.work_ot() + + def work(self): + if self.energy.food_fights >= 1: + response = self.post_economy_work(self.token, "work") + js = response.json() + good_msg = ["already_worked", "captcha"] + if not js.get("status") and not js.get("message") in good_msg: + self.update_citizen_info() + self.work() + else: + self.reporter.report_action("WORK", json_val=js) + else: + self._eat("blue") + if self.energy.food_fights < 1: + large = max(self.energy.reference_time, self.now) + small = min(self.energy.reference_time, self.now) + self.write_log("I don't have energy to work. Will sleep for {}s".format((large - small).seconds)) + self.sleep(int((large - small).total_seconds())) + self._eat("blue") + self.work() + + def train(self): + r = self.get_training_grounds_json() + tg_json = r.json() + self.details.gold = tg_json["page_details"]["gold"] + self.tg_contract.update({"free_train": tg_json["hasFreeTrain"]}) + if tg_json["contracts"]: + self.tg_contract.update(**tg_json["contracts"][0]) + + tgs = [] + for data in sorted(tg_json["grounds"], key=lambda k: k["cost"]): + if data["default"] and not data["trained"]: + tgs.append(data["id"]) + if tgs: + if self.energy.food_fights >= len(tgs): + response = self.post_economy_train(self.token, tgs) + if not response.json().get("status"): + self.update_citizen_info() + self.train() + else: + self.reporter.report_action("TRAIN", response.json()) + else: + self._eat("blue") + if self.energy.food_fights < len(tgs): + large = max(self.energy.reference_time, self.now) + small = min(self.energy.reference_time, self.now) + self.write_log("I don't have energy to train. Will sleep for {} seconds".format( + (large - small).seconds)) + self.sleep(int((large - small).total_seconds())) + self._eat("blue") + self.train() + + def work_employees(self) -> bool: + self.update_companies() + ret = True + work_units_needed = 0 + employee_companies = self.my_companies.get_employable_factories() + for c_id, preset_count in employee_companies.items(): + work_units_needed += preset_count + + if work_units_needed: + if work_units_needed <= self.my_companies.work_units: + self._do_wam_and_employee_work(employee_companies=employee_companies) + self.update_companies() + if self.my_companies.get_employable_factories(): + ret = False + else: + ret = True + + return ret + + def work_wam(self) -> bool: + self.update_citizen_info() + self.update_companies() + # Prevent messing up levelup with wam + if not (self.is_levelup_close and self.config.fight) or self.config.force_wam: + # Check for current region + regions = {} + for holding_id, holding in self.my_companies.holdings.items(): + if self.my_companies.get_holding_wam_companies(holding_id): + regions.update({holding["region_id"]: holding_id}) + + if self.details.current_region in regions: + self._do_wam_and_employee_work(regions.pop(self.details.current_region, None)) + + for holding_id in regions.values(): + self._do_wam_and_employee_work(holding_id) + + self.travel_to_residence() + else: + self.write_log("Did not wam because I would mess up levelup!") + + self.update_companies() + return not self.my_companies.get_total_wam_count() + + def _do_wam_and_employee_work(self, wam_holding_id: int = 0, employee_companies: dict = None) -> bool: + self.update_citizen_info() + if employee_companies is None: + employee_companies = {} + data = { + "action_type": "production", + "_token": self.token, + } + extra = {} + wam_list = [] + if wam_holding_id: + raw_count = self.my_companies.get_holding_wam_count(wam_holding_id, raw_factory=True) + fab_count = self.my_companies.get_holding_wam_count(wam_holding_id, raw_factory=False) + if raw_count + fab_count <= self.energy.food_fights: + raw_factories = None + elif not raw_count and fab_count <= self.energy.food_fights: + raw_factories = False + else: + raw_factories = True + + free_inventory = self.inventory["total"] - self.inventory["used"] + wam_list = self.my_companies.get_holding_wam_companies(wam_holding_id, + raw_factory=raw_factories)[:self.energy.food_fights] + has_space = False + while not has_space and wam_list: + extra_needed = self.my_companies.get_needed_inventory_usage(companies=wam_list) + has_space = extra_needed < free_inventory + if not has_space: + inv_w = len(str(self.inventory["total"])) + self.write_log( + "Inv: {:{inv_w}}/{:{inv_w}} ({:4.2f}), Energy: {}/{} + {} (+{}hp/6min) WAM count {:3}".format( + self.inventory["used"], self.inventory["total"], extra_needed, + self.energy.recovered, self.energy.limit, self.energy.recoverable, self.energy.interval, + len(wam_list), inv_w=inv_w + )) + wam_list.pop(-1) + + if wam_list or employee_companies: + data.update(extra) + if wam_list: + wam_holding = self.my_companies.holdings.get(wam_holding_id) + if not self.details.current_region == wam_holding['region_id']: + self.travel(holding_id=wam_holding_id, region_id=wam_holding['region_id']) + response = self.post_economy_work(self.token, "production", wam=wam_list, employ=employee_companies).json() + self.reporter.report_action("WORK_WAM_EMPLOYEES", response) + if response.get("status"): + if self.config.auto_sell: + for kind, data in response.get("result", {}).get("production", {}).items(): + if kind in self.config.auto_sell and data: + if kind in ["food", "weapon", "house", "airplane"]: + for quality, amount in data.items(): + self.sell_produced_product(kind, quality) + + elif kind.endswith("Raw"): + self.sell_produced_product(kind, 1) + + else: + raise classes.ErepublikException("Unknown kind produced '{kind}'".format(kind=kind)) + elif self.config.auto_buy_raw and re.search(r"not_enough_[^_]*_raw", response.get("message")): + raw_kind = re.search(r"not_enough_(\w+)_raw", response.get("message")) + if raw_kind: + raw_kind = raw_kind.group(1) + result = response.get("result", {}) + amount_remaining = round(result.get("consume") + 0.49) - round(result.get("stock") - 0.49) + industry = "{}Raw".format(raw_kind) + while amount_remaining > 0: + amount = amount_remaining + best_offer = self.get_market_offers(self.details.citizenship, industry, 1) + amount = best_offer['amount'] if amount >= best_offer['amount'] else amount + rj = self.buy_from_market(amount=best_offer['amount'], offer=best_offer['offer_id']).json() + if not rj.get('error'): + amount_remaining -= amount + else: + self.write_log(rj.get('message', "")) + break + else: + return self._do_wam_and_employee_work(wam_holding_id, employee_companies) + + else: + self.write_log("I was not able to wam and or employ because:\n{}".format(response)) + wam_count = self.my_companies.get_total_wam_count() + if wam_count: + self.write_log("Wam ff lockdown is now {}, was {}".format(wam_count, self.my_companies.ff_lockdown)) + self.my_companies.ff_lockdown = wam_count + return bool(wam_count) + + def sell_produced_product(self, kind: str, quality: int = 1, amount: int = 0): + if not amount: + inv_resp = self.update_inventory() + category = "rawMaterials" if kind.endswith("Raw") else "finalProducts" + item = "{}_{}".format(self.available_industries[kind], quality) + amount = inv_resp.get("inventoryItems").get(category).get("items").get(item).get("amount", 0) + + if amount >= 1: + lowest_price = self.get_market_offers(country_id=self.details.citizenship, + product=kind, quality=int(quality)) + + if lowest_price["citizen_id"] == self.details.citizen_id: + price = lowest_price["price"] + else: + price = lowest_price["price"] - 0.01 + + self.post_market_offer(industry=self.available_industries[kind], amount=int(amount), + quality=int(quality), price=price) + + def travel_to_residence(self): + self.update_citizen_info() + res_r = self.details.residence_region + if self.details.residence_country and res_r and not res_r == self.details.current_region: + self._travel(self.details.residence_country, self.details.residence_region) + + def get_country_travel_region(self, country_id: int) -> int or None: + r = self.get_travel_regions(country_id=country_id).json() + regions = r.get("regions") + regs = [] + if regions: + for region in regions.values(): + if region['countryId'] == country_id: # Is not occupied by other country + regs.append((region['id'], region['distanceInKm'])) + if regs: + return min(regs, key=lambda _: int(_[1]))[0] + else: + return None + + def travel(self, holding_id=0, battle_id=0, region_id=0): + r = self.get_travel_regions(holding_id, battle_id, region_id) + regions = r.json()["regions"] + closest_region = 99999 + country_id = int(r.json()["preselectCountryId"]) + region_id = int(r.json()["preselectRegionId"]) + if not any((region_id, country_id)): + for rid, info in regions.items(): + if info.get("distanceInKm", 99999) < closest_region: + closest_region = info.get("distanceInKm") + country_id = info.get("countryId") + region_id = rid + self._travel(country_id, region_id) + + def _travel(self, country_id: int, region_id: int = 0) -> Response: + data = { + "toCountryId": country_id, + "inRegionId": region_id, + "battleId": 0, + } + return self.post_travel(self.token, "moveAction", **data) + + def get_travel_regions(self, holding_id: int = 0, battle_id: int = 0, region_id: int = 0, + country_id: int = 0) -> Response: + data = { + "holdingId": holding_id, + "battleId": battle_id, + "regionId": region_id, + } + data.update(countryId=country_id) + return self.post_travel_data(self.token, **data) + + def parse_notifications(self, page: int = 1) -> list: + response = self.get_message_alerts(page) + notifications = re.findall(r"(.*?)
", response.text, re.M | re.I | re.S) + return notifications + + def delete_notifications(self): + def notification_ids(html): + return re.findall(r"id=\"delete_alert_(\d+)\"", html) + + response = self.get_message_alerts() + while notification_ids(response.text): + response = self.post_messages_alert(self.token, notification_ids(response.text)) + + def collect_weekly_reward(self): + self.update_weekly_challenge() + + def collect_daily_task(self) -> Response or None: + self.update_citizen_info() + if self.details.daily_task_done and not self.details.daily_task_reward: + return self.post_daily_task_reward(self.token) + + def send_mail_to_owner(self) -> Response or None: + if not self.details.citizen_id == 1620414: + self.send_mail("Started", "time {}".format(self.now.strftime("%Y-%m-%d %H-%M-%S")), [1620414, ]) + self.sleep(1) + msg_id = re.search(r"", self.r.text).group(1) + return self.post_delete_message(self.token, [msg_id]) + + def get_market_offers(self, country_id: int = None, product: str = None, quality: int = None) -> dict: + ret = dict() + raw_short_names = dict(frm="foodRaw", wrm="weaponRaw", hrm="houseRaw", arm="airplaneRaw") + q1_industries = ["aircraft"] + list(raw_short_names.values()) + if product in raw_short_names: + quality = 1 + product = raw_short_names.get(product) + + item_data = dict(price=999999., country=0, amount=0, offer_id=0, citizen_id=0) + + items = {"food": dict(q1=item_data.copy(), q2=item_data.copy(), q3=item_data.copy(), q4=item_data.copy(), + q5=item_data.copy(), q6=item_data.copy(), q7=item_data.copy()), + "weapon": dict(q1=item_data.copy(), q2=item_data.copy(), q3=item_data.copy(), q4=item_data.copy(), + q5=item_data.copy(), q6=item_data.copy(), q7=item_data.copy()), + "house": dict(q1=item_data.copy(), q2=item_data.copy(), q3=item_data.copy(), q4=item_data.copy(), + q5=item_data.copy()), "aircraft": dict(q1=item_data.copy()), + "foodRaw": dict(q1=item_data.copy()), "weaponRaw": dict(q1=item_data.copy()), + "houseRaw": dict(q1=item_data.copy()), "airplaneRaw": dict(q1=item_data.copy())} + + countries = [country_id] if country_id else self.countries + if product not in self.available_industries: + self.write_log("Industry '{}' not implemented".format(product)) + return ret + + start_dt = self.now + for country in countries: + if not country_id and not self.get_country_travel_region(country): + continue + for industry in [product] or items: + for q in [quality] if quality else range(1, 8): + if (q > 1 and industry in q1_industries) or (q > 5 and industry == "house"): + break + + str_q = "q%i" % q + + data = {'country': country, 'industry': self.available_industries[industry], 'quality': q} + r = self.post_economy_marketplace(self.token, **data) + rjson = r.json() + obj = items[industry][str_q] + if not rjson.get("error", False): + for offer in rjson["offers"]: + if obj["price"] > float(offer["priceWithTaxes"]): + obj["price"] = float(offer["priceWithTaxes"]) + obj["country"] = int(offer["country_id"]) + obj["amount"] = int(offer["amount"]) + obj["offer_id"] = int(offer["id"]) + obj["citizen_id"] = int(offer["citizen_id"]) + elif obj["price"] == float(offer["priceWithTaxes"]) \ + and obj["amount"] < int(offer["amount"]): + obj["country"] = int(offer["country_id"]) + obj["amount"] = int(offer["amount"]) + obj["offer_id"] = int(offer["id"]) + self.write_log("Scraped market in {}!".format(self.now - start_dt)) + + if quality: + ret = items[product]["q%i" % quality] + elif product: + if product in raw_short_names.values(): + ret = items[product]["q1"] + else: + ret = items[product] + else: + ret = items + return ret + + def buy_food(self) -> Response or None: + self.update_money() + hp_per_quality = {"q1": 2, "q2": 4, "q3": 6, "q4": 8, "q5": 10, "q6": 12, "q7": 20} + hp_needed = 24 * self.energy.interval * 10 - self.food["total"] + local_offers = self.get_market_offers(country_id=self.details.current_country, product="food") + + cheapest_q, cheapest = sorted(local_offers.items(), key=lambda v: v[1]["price"] / hp_per_quality[v[0]])[0] + + if cheapest["amount"] * hp_per_quality[cheapest_q] < hp_needed: + amount = cheapest["amount"] + else: + amount = hp_needed // hp_per_quality[cheapest_q] + + if amount * cheapest["price"] < self.details.cc: + data = dict(offer=cheapest["offer_id"], amount=amount, price=cheapest["price"], + cost=amount * cheapest["price"], quality=cheapest_q, energy=amount * hp_per_quality[cheapest_q]) + self.reporter.report_action("BUY_FOOD", json_val=data) + return self.buy_from_market(cheapest["offer_id"], amount) + else: + s = "Don't have enough money! Needed: {}cc, Have: {}cc".format(amount * cheapest["price"], self.details.cc) + self.write_log(s) + self.reporter.report_action("BUY_FOOD", value=s) + return None + + def get_monetary_offers(self, currency: int = 62) -> list: + if currency not in [1, 62]: + currency = 62 + resp = self.update_money(0, currency).json() + ret = [] + offers = re.findall(r"id='purchase_(\d+)' data-i18n='Buy for' data-currency='GOLD' " + r"data-price='(\d+\.\d+)' data-max='(\d+\.\d+)' trigger='purchase'", + resp["buy_mode"], re.M | re.I | re.S) + + for offer_id, price, amount in offers: + ret.append({"offer_id": int(offer_id), "price": float(price), "amount": float(amount)}) + + return sorted(ret, key=lambda o: (o["price"], -o["amount"])) + + def buy_monetary_market_offer(self, offer: int, amount: float, currency: int) -> bool: + response = self.post_economy_exchange_purchase(self.token, amount, currency, offer) + self.details.cc = float(response.json().get("ecash").get("value")) + self.details.gold = float(response.json().get("gold").get("value")) + self.reporter.report_action("BUY_GOLD", json_val=response.json(), + value="New amount {o.cc}cc, {o.gold}g".format(o=self.details)) + return not response.json().get("error", False) + + def activate_dmg_booster(self, battle_id: int) -> Response or None: + if self.config.boosters: + duration = 0 + if self.boosters.get("100_damageBoosters_5_600", 0) > 0: + duration = 600 + elif self.boosters.get("100_damageBoosters_5_7200", 0) > 1: + duration = 7200 + elif self.boosters.get("100_damageBoosters_5_28800", 0) > 2: + duration = 28800 + elif self.boosters.get("100_damageBoosters_5_86400", 0) > 2: + duration = 86400 + return self.post_fight_activate_booster(self.token, battle_id, 5, duration, "damage") + + def activate_battle_effect(self, battle_id: int, kind: str): + return self.post_activate_battle_effect(self.token, battle_id, kind, self.details.citizen_id) + + def activate_pp_booster(self, battle_id: int): + return self.post_fight_activate_booster(self.token, battle_id, 1, 180, "prestige_points") + + def donate_money(self, citizen_id: int = 1620414, amount: float = 0.0, currency: int = 62): + """ currency: gold = 62, cc = 1 """ + return self.post_economy_donate_money_action(self.token, citizen_id, amount, currency) + + def donate_items(self, citizen_id: int = 1620414, amount: int = 0, industry_id: int = 1, + quality: int = 1) -> Response: + ind = {v: k for k, v in self.available_industries.items()} + self.write_log("D,{},q{},{},{}".format(amount, quality, ind[industry_id], citizen_id)) + return self.post_economy_donate_items_action(self.token, citizen_id, amount, industry_id, quality) + + def candidate_for_congress(self, presentation: str = "") -> Response: + return self.post_candidate_for_congress(self.token, presentation) + + def candidate_for_party_presidency(self) -> Response: + return self.get_candidate_party(self.politics.party_slug) + + def accept_money_donations(self): + for notification in self.parse_notifications(): + don_id = re.search(r"erepublik.functions.acceptRejectDonation\(\"accept\", (\d+)\)", notification) + if don_id: + self.get_money_donation_accept(self.token, int(don_id.group(1))) + self.sleep(5) + + def reject_money_donations(self) -> int: + r = self.get_message_alerts() + count = 0 + donation_ids = re.findall(r"erepublik.functions.acceptRejectDonation\(\"reject\", (\d+)\)", r.text) + while donation_ids: + for don_id in donation_ids: + self.get_money_donation_reject(self.token, int(don_id)) + count += 1 + self.sleep(5) + r = self.get_message_alerts() + donation_ids = re.findall(r"erepublik.functions.acceptRejectDonation\(\"reject\", (\d+)\)", r.text) + return count + + def _rw_choose_side(self, battle_id: int, side_id: int) -> Response: + return self.get_battlefield_choose_side(battle_id, side_id) + + def should_travel_to_fight(self) -> bool: + ret = False + if self.config.always_travel: + return True + if self.should_do_levelup: # Do levelup + ret = True + elif self.config.all_in and self.energy.available > self.energy.limit * 2 - self.energy.interval * 3: + ret = True + # Get to next Energy +1 + elif self.next_reachable_energy and self.config.next_energy: + ret = True + # 1h worth of energy + elif self.energy.available + self.energy.interval * 3 >= self.energy.limit * 2: + ret = True + return ret + + def should_fight(self, silent: bool = True) -> int: + if not self.config.fight: + return 0 + count = 0 + log_msg = "" + force_fight = False + # Do levelup + if self.is_levelup_reachable: + log_msg = "Level up" + if self.should_do_levelup: + count = (self.energy.limit * 3) // 10 + force_fight = True + else: + self.write_log("Waiting for fully recovered energy before leveling up.", False) + + # Levelup reachable + elif self.is_levelup_close: + count = self.details.xp_till_level_up - (self.energy.limit // 10) + 5 + log_msg = "Fighting for close Levelup. Doing %i hits" % count + force_fight = True + + elif self.details.pp < 75: + count = 75 - self.details.pp + log_msg = "Obligatory fighting for at least 75pp" + force_fight = True + + elif self.config.continuous_fighting and self.has_battle_contribution: + count = self.energy.food_fights + log_msg = "Continuing to fight in previous battle" + + # All-in (type = all-in and full ff) + elif self.config.all_in and self.energy.available + self.energy.interval * 3 >= self.energy.limit * 2: + count = self.energy.food_fights + log_msg = "Fighting all-in. Doing %i hits" % count + + # All-in for AIR battles + elif all([self.config.air, self.config.all_in, + self.energy.available >= self.energy.limit]): + count = self.energy.food_fights + log_msg = "Fighting all-in in AIR. Doing %i hits" % count + + # Get to next Energy +1 + elif self.next_reachable_energy and self.config.next_energy: + count = self.next_reachable_energy + log_msg = "Fighting for +1 energy. Doing %i hits" % count + + # 1h worth of energy + elif self.energy.available + self.energy.interval * 3 >= self.energy.limit * 2: + count = self.energy.interval + log_msg = "Fighting for 1h energy. Doing %i hits" % count + + if count > 0 and not force_fight: + if self.my_companies.ff_lockdown and self.details.pp > 75: + if self.energy.food_fights - self.my_companies.ff_lockdown > 0: + log_msg = ("Fight count modified (old count: {} | FF: {} | " + "WAM ff_lockdown: {} | New count: {})").format( + count, self.energy.food_fights, self.my_companies.ff_lockdown, + self.energy.food_fights - self.my_companies.ff_lockdown) + count = self.energy.food_fights - self.my_companies.ff_lockdown + else: + count = 0 + if count <= 0: + log_msg = "Not fighting because WAM needs {} food fights".format(self.my_companies.ff_lockdown) + + if self.max_time_till_full_ff > self.time_till_week_change: + max_count = int((self.time_till_week_change - self.time_till_full_ff).total_seconds()) // 60 + log_msg = "End for Weekly challenge is near ({} | {})".format(max_count, count) + count = count if max_count > count else max_count + + if not silent: + self.write_log(log_msg, False) + + return count if count > 0 else 0 + + @property + def next_reachable_energy(self) -> int: + # Return pps for furthest __reachable__ +1 energy else 0 + max_pp = 0 + for pp_milestone in self.details.next_pp: + pp_milestone = int(pp_milestone) + if self.details.pp + self.energy.food_fights > pp_milestone: # if reachable set max pp + max_pp = pp_milestone + else: # rest are only bigger no need + break + return max_pp - self.details.pp if max_pp else 0 + + @property + def next_wc_start(self) -> datetime.datetime: + days = 1 - self.now.weekday() if 1 - self.now.weekday() > 0 else 1 - self.now.weekday() + 7 + return utils.good_timedelta(self.now.replace(hour=0, minute=0, second=0, microsecond=0), + datetime.timedelta(days=days)) + + @property + def time_till_week_change(self) -> datetime.timedelta: + return self.next_wc_start - self.now + + @property + def time_till_full_ff(self) -> datetime.timedelta: + energy = self.energy.recoverable + self.energy.recovered + if energy >= self.energy.limit * 2: + return datetime.timedelta(0) + minutes_needed = round((self.energy.limit * 2 - energy) / self.energy.interval) * 6 + return (self.energy.reference_time - self.now) + datetime.timedelta(minutes=minutes_needed) + + @property + def max_time_till_full_ff(self) -> datetime.timedelta: + """ + Max required time for 0 to full energy (0/0 -> limit/limit) (last interval rounded up) + :return: + """ + return datetime.timedelta(minutes=round((self.energy.limit * 2 / self.energy.interval) + 0.49) * 6) + + @property + def is_levelup_close(self) -> bool: + """ + If Energy limit * 2 >= xp till levelup * 10 + :return: bool + """ + return self.energy.limit * 2 >= self.details.xp_till_level_up * 10 + + @property + def is_levelup_reachable(self) -> bool: + """ + If Energy limit >= xp till levelup * 10 + :return: bool + """ + return self.energy.limit >= self.details.xp_till_level_up * 10 + + @property + def should_do_levelup(self) -> bool: + """ + If Energy limit >= xp till levelup * 10 + :return: bool + """ + if self.energy.limit - self.energy.interval <= self.energy.recoverable: + if self.is_levelup_reachable: + # 45xp till levelup, 50hp/6min, 500+500/500 energy = 500+450 >= 1000 + # 40xp till levelup, 50hp/6min, 450+500/500 energy = 500+400 >= 950 + # 25xp till levelup, 50hp/6min, 300+500/500 energy = 500+250 >= 800 + # 25xp till levelup, 50hp/6min, 300+200/500 energy = 500+250 >= 500 + return self.energy.limit + self.details.xp_till_level_up * 10 <= self.energy.available + return False + + def get_article_comments(self, article_id: int = 2645676, page_id: int = 0): + return self.post_article_comments(self.token, article_id, page_id) + + def comment_article(self, article_id: int = 2645676, msg: str = None) -> Response: + if msg is None: + msg = self.eday + r = self.get_article_comments(article_id, 2) + r = self.get_article_comments(article_id, r.json()["pages"]) + comments = r.json()["comments"] + if not comments[max(comments.keys())]["isMyComment"]: + r = self.write_article_comment(msg, article_id) + return r + + def write_article_comment(self, message: str, article_id: int, parent_id: int = None): + return self.post_article_comments_create(self.token, message, article_id, parent_id) + + def publish_article(self, title: str, content: str, kind: int): + kinds = {1: "First steps in eRepublik", 2: "Battle orders", 3: "Warfare analysis", + 4: "Political debates and analysis", 5: "Financial business", + 6: "Social interactions and entertainment"} + if kind in kinds: + return self.post_write_article(self.token, title, content, self.details.citizenship, kind) + else: + raise classes.ErepublikException( + "Article kind must be one of:\n{}\n'{}' is not supported".format( + "\n".join(["{}: {}".format(k, v) for k, v in kinds.items()]), + kind + ) + ) + + def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> Response: + if industry not in self.available_industries.values(): + self.write_log("Trying to sell unsupported industry {}".format(industry)) + + data = { + "token": self.token, + "country": self.details.citizenship, + "industry": industry, + "quality": quality, + "amount": amount, + "price": price, + "buy": False, + } + ret = self.post_economy_marketplace_actions(**data) + self.reporter.report_action("SELL_PRODUCT", ret.json()) + return ret + + def buy_from_market(self, offer: int, amount: int) -> Response: + ret = self.post_economy_marketplace_actions(self.token, amount, True, offer=offer) + json_ret = ret.json() + if json_ret.get('error'): + return ret + else: + self.details.cc = ret.json()['currency'] + self.details.gold = ret.json()['gold'] + r_json = ret.json() + r_json.pop("offerUpdate", None) + self.reporter.report_action("BUY_PRODUCT", ret.json()) + return ret + + def get_raw_surplus(self) -> (float, float): + frm = 0.00 + wrm = 0.00 + self.update_companies() + for cdata in sorted(self.my_companies.companies.values()): + if cdata["industry_token"] == "FOOD": + raw = frm + elif cdata["industry_token"] == "WEAPON": + raw = wrm + else: + continue + if cdata["is_raw"]: + raw += float(cdata["base_production"]) * cdata["effective_bonus"] / 100 + else: + raw -= cdata["effective_bonus"] / 100 * cdata["base_production"] * \ + cdata["upgrades"][str(cdata["quality"])]["raw_usage"] + if cdata["industry_token"] == "FOOD": + frm = raw + elif cdata["industry_token"] == "WEAPON": + wrm = raw + return frm, wrm + + def assign_factory_to_holding(self, factory_id: int, holding_id: int) -> Response: + """ + Assigns factory to new holding + :type factory_id: int + :type holding_id: int + :return: Response object + """ + return self.post_economy_assign_to_holding(self.token, factory_id, holding_id) + + def upgrade_factory(self, factory_id: int, level: int) -> Response: + return self.post_economy_upgrade_company(self.token, factory_id, level, self.details.pin) + + def create_factory(self, industry_id: int, building_type: int = 1) -> Response: + """ + param industry_ids: FRM={q1:7, q2:8, q3:9, q4:10, q5:11} WRM={q1:12, q2:13, q3:14, q4:15, q5:16} + HRM={q1:18, q2:19, q3:20, q4:21, q5:22} ARM={q1:24, q2:25, q3:26, q4:27, q5:28} + Factories={Food:1, Weapons:2, House:4, Aircraft:23} <- Building_type 1 + + Storage={1000: 1, 2000: 2} <- Building_type 2 + """ + return self.post_economy_create_company(self.token, industry_id, building_type) + + def dissolve_factory(self, factory_id: int) -> Response: + return self.post_economy_sell_company(self.token, factory_id, self.details.pin, sell=False) + + @property + def available_industries(self) -> Dict[str, int]: + """ + Returns currently available industries as dict(name: id) + :return: dict + """ + return {"food": 1, "weapon": 2, "house": 4, "aircraft": 23, + "foodRaw": 7, "weaponRaw": 12, "houseRaw": 17, "airplaneRaw": 24} + + def get_industry_id(self, industry_name: str) -> int: + """ + Returns industry id + :type industry_name: str + :return: int + """ + return self.available_industries.get(industry_name, 0) + + def buy_tg_contract(self) -> Response: + ret = self.post_buy_gold_items(self.token, 'gold', "TrainingContract2", 1) + self.reporter.report_action("BUY_TG_CONTRACT", ret.json()) + return ret + + def resign(self) -> Response or None: + self.update_job_info() + if self.r.json().get("isEmployee"): + self.reporter.report_action("RESIGN", self.r.json()) + return self.post_economy_resign(self.token) + return None + + def find_new_job(self) -> Response: + r = self.get_economy_job_market_json(self.details.current_country) + jobs = r.json().get("jobs") + data = dict(token=self.token, citizen=0, salary=10) + for posting in jobs: + salary = posting.get("netSalary") + limit = posting.get("salaryLimit", 0) + userid = posting.get("citizen").get("id") + + if (not limit or salary * 3 < limit) and salary > data["salary"]: + data.update({"citizen": userid, "salary": salary}) + self.reporter.report_action("APPLYING_FOR_JOB", jobs, str(data['citizen'])) + return self.post_economy_job_market_apply(**data) + + def add_friend(self, player_id: int) -> Response: + resp = self.get_citizen_hovercard(player_id) + rjson = resp.json() + if not any([rjson["isBanned"], rjson["isDead"], rjson["isFriend"], rjson["isOrg"], rjson["isSelf"]]): + r = self.post_citizen_add_remove_friend(self.token, int(player_id), True) + self.write_log("{:<64} (id:{:>11}) added as friend".format(rjson["name"], player_id)) + return r + return resp + + def get_country_parties(self, country_id: int = None) -> dict: + if country_id is None: + country_id = self.details.citizenship + r = self.get_rankings_parties(country_id) + ret = {} + for name, id_ in re.findall(r'', r.text): + ret.update({int(id_): name}) + return ret + + def get_party_members(self, party_id: int) -> Dict[int, str]: + ret = {} + r = super().get_party_members(party_id) + for id_, name in re.findall(r'', r.text): + ret.update({id_: name}) + return ret + + def get_country_mus(self, country_id: int) -> Dict[int, str]: + ret = {} + r = self.get_leaderboards_damage_rankings(country_id) + for data in r.json()["mu_filter"]: + if data["id"]: + ret.update({data["id"]: data["name"]}) + r = self.get_leaderboards_damage_aircraft_rankings(country_id) + for data in r.json()["mu_filter"]: + if data["id"]: + ret.update({data["id"]: data["name"]}) + return ret + + def get_mu_members(self, mu_id: int) -> Dict[int, str]: + ret = {} + r = self.get_military_unit_data(mu_id) + + for page in range(1, int(r.json()["panelContents"]["pages"]) + 1): + r = self.get_military_unit_data(mu_id, page) + for user in r.json()["panelContents"]["members"]: + if not user["isDead"]: + ret.update({user["citizenId"]: user["name"]}) + return ret + + def send_mail(self, subject: str, msg: str, ids: List[int] = None): + if ids is None: + ids = [1620414, ] + for player_id in ids: + self.post_messages_compose(self.token, subject, msg, [player_id]) + + def add_every_player_as_friend(self): + cities = [] + cities_dict = {} + self.write_log("WARNING! This will take a lot of time.") + rj = self.post_travel_data(self.token, regionId=662, check="getCountryRegions").json() + for region_data in rj.get("regions", {}).values(): + cities.append(region_data['cityId']) + cities_dict.update({region_data['cityId']: region_data['cityName']}) + + cities.sort(key=int) + for city_id in cities: + self.write_log("Adding friends from {} (id: {})".format(cities_dict[city_id], city_id)) + resp = self.get_city_data_residents(city_id).json() + for resident in resp["widgets"]["residents"]["residents"]: + self.add_friend(resident["citizenId"]) + for page in range(2, resp["widgets"]["residents"]["numResults"] // 10 + 2): + r = self.get_city_data_residents(city_id, page) + resp = r.json() + for resident in resp["widgets"]["residents"]["residents"]: + self.add_friend(resident["citizenId"]) + + def schedule_attack(self, war_id: int, region_id: int, at_time: datetime) -> None: + if at_time: + self.sleep(utils.get_sleep_seconds(at_time)) + self.get_csrf_token() + self._launch_battle(war_id, region_id) + + def get_active_wars_with_regions(self): + self.get_country_military(self.countries.get(self.details.citizen_id)["name"]) + raise NotImplementedError + + def _launch_battle(self, war_id: int, region_id: int) -> Response: + return self.post_wars_attack_region(self.token, war_id, region_id) + + def state_update_repeater(self): + try: + start_time = self.now.replace(second=0, microsecond=0) + if start_time.minute <= 30: + start_time = start_time.replace(minute=30) + else: + start_time = start_time.replace(minute=0) + while not self.stop_threads.is_set(): + self.update_citizen_info() + start_time = utils.good_timedelta(start_time, datetime.timedelta(minutes=30)) + self.send_state_update() + self.send_inventory_update() + sleep_seconds = (start_time - self.now).total_seconds() + self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0) + except: + self.report_error() + + def send_state_update(self): + data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp, + inv_total=self.inventory['total'], inv=self.inventory['used'], hp_limit=self.energy.limit, + hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], ) + self.reporter.send_state_update(**data) + + def send_inventory_update(self): + j = self.update_inventory() + active_items = {} + for item in j.get("inventoryItems", {}).get("activeEnhancements", {}).get("items", {}).values(): + active_items.update({item['name']: item['active']['time_left']}) + final_items = {} + + for item in j.get("inventoryItems", {}).get("finalProducts", {}).get("items", {}).values(): + name = item['name'] + if item.get('type') == 'damageBoosters': + delta = item['duration'] + if delta // 3600: + name += f" {delta // 3600}h" + if delta // 60 % 60: + name += f" {delta // 60 % 60}m" + if delta % 60: + name += f" {delta % 60}s" + if item['amount']: + final_items.update({name: item['amount']}) + + raw_materials = {} + for item in j.get("inventoryItems", {}).get("rawMaterials", {}).get("items", {}).values(): + name = item['name'] + if item['isPartial']: + continue + if item['amount']: + raw_materials.update({name: item['amount']}) + + to_report = dict(items=dict(active=active_items, final=final_items, raw=raw_materials), status=self.inventory) + self.reporter.report_action("INVENTORY", json_val=to_report) + + def check_house_durability(self) -> Dict[int, datetime.datetime]: + ret = {} + inv = self.update_inventory() + if "activeEnhancements" in inv.get("inventoryItems", {}): + active = inv.get("inventoryItems", {}).get("activeEnhancements", {}).get("items", {}) + for q in range(1, 6): + if "4_%i_active" % q in active: + till = utils.good_timedelta(self.now, datetime.timedelta( + seconds=active["4_%i_active" % q]["active"]["time_left"])) + ret.update({q: till}) + return ret + + def buy_and_activate_house(self, q: int) -> Dict[int, datetime.datetime]: + inventory = self.update_inventory() + ok_to_activate = False + if not inventory.get("inventoryItems").get("finalProducts", {}).get("items", {}).get("4_{}".format(q)): + offers = [] + countries = [self.details.citizenship, ] + if self.details.current_country != self.details.citizenship: + countries.append(self.details.current_country) + for country in countries: + offers += [self.get_market_offers(country, "house", q)] + global_cheapest = self.get_market_offers(product="house", quality=q) + cheapest_offer = sorted(offers, key=lambda o: o["price"])[0] + region = self.get_country_travel_region(global_cheapest['country']) + if global_cheapest['price'] + 200 < cheapest_offer['price'] and region: + self._travel(global_cheapest['country'], region) + buy = self.buy_from_market(global_cheapest['offer_id'], 1) + else: + buy = self.buy_from_market(cheapest_offer['offer_id'], 1) + if buy.json()["error"]: + msg = "Unable to buy q{} house! \n{}".format(q, buy.json()['message']) + self.write_log(msg) + else: + ok_to_activate = True + else: + ok_to_activate = True + if ok_to_activate: + self.activate_house(q) + return self.check_house_durability() + + def renew_houses(self, forced: bool = False) -> Dict[int, datetime.datetime]: + """ + Renew all houses which endtime is in next 48h + :param forced: if true - renew all houses + :return: + """ + house_durability = self.check_house_durability() + for q, active_till in house_durability.items(): + if utils.good_timedelta(active_till, - datetime.timedelta(hours=48)) <= self.now or forced: + house_durability = self.buy_and_activate_house(q) + self.travel_to_residence() + return house_durability + + def activate_house(self, quality: int) -> datetime.datetime: + active_until = self.now + r = self.post_economy_activate_house(self.token, quality) + if r.json().get("status") and not r.json().get("error"): + house = r.json()["inventoryItems"]["activeEnhancements"]["items"]["4_%i_active" % quality] + active_until = utils.good_timedelta(active_until, datetime.timedelta(seconds=house["active"]["time_left"])) + return active_until + + def collect_anniversary_reward(self) -> Response: + return self.post_collect_anniversary_reward(self.token) + + def get_battle_round_data(self, battle_id: int, round_id: int, division: int = None) -> dict: + battle = self.all_battles.get(battle_id) + if not battle: + return {} + r = self.post_battle_console(self.token, battle_id, battle.zone_id, round_id, division, 1, True) + return {battle.invader.id: r.json().get(str(battle.invader.id)).get("fighterData"), + battle.defender.id: r.json().get(str(battle.defender.id)).get("fighterData")} + + def contribute_cc_to_country(self, amount: int or float = 0) -> bool: + self.update_money() + amount = int(amount) + if self.details.cc < amount or amount < 20: + return False + json = dict(country=71, action='currency', value=amount) + self.reporter.report_action("CONTRIBUTE_CC", json) + r = self.post_country_donate(self.token, **json) + return r.json().get('status') or not r.json().get('error') + + def contribute_food_to_country(self, amount: int = 0, quality: int = 1) -> bool: + self.update_inventory() + amount = amount // 1 + if self.food["q" + str(quality)] < amount or amount < 10: + return False + json = dict(country=71, action='food', value=amount, quality=quality) + self.reporter.report_action("CONTRIBUTE_FOOD", json) + r = self.post_country_donate(self.token, **json) + return r.json().get('status') or not r.json().get('error') + + def contribute_gold_to_country(self, amount: int) -> bool: + self.update_money() + + if self.details.cc < amount: + return False + json = dict(country=71, action='gold', value=amount) + self.reporter.report_action("CONTRIBUTE_GOLD", json) + r = self.post_country_donate(self.token, **json) + return r.json().get('status') or not r.json().get('error') + + def write_on_country_wall(self, message: str) -> bool: + self.get_main() + post_to_wall_as = re.findall(r"""id="post_to_country_as".*?.*""", + self.r.text, re.S | re.M) + if post_to_wall_as: + self.post_country_post_create(self.token, message, max(post_to_wall_as)) + return True + return False + + def report_error(self, msg: str = ""): + utils.process_error(msg, self.name, sys.exc_info(), self, self.commit_id, False) + + def get_battle_top_10(self, battle_id: int) -> Dict[int, List[Tuple[int, int]]]: + battle = self.all_battles.get(battle_id) + round_id = battle.get('zone_id') + division = self.division if round_id % 4 else 11 + + resp = self.post_military_battle_console(self.token, battle_id, round_id, division).json() + resp.pop('rounds', None) + ret = dict() + for country_id, data in resp.items(): + ret.update({int(country_id): []}) + for place in sorted(data.get("fighterData", {}).values(), key=lambda _: -_['raw_value']): + ret[int(country_id)].append((place['citizenId'], place['raw_value'])) + + return ret + + def to_json(self, indent: bool = False) -> str: + return dumps(self.__dict__, cls=utils.MyJSONEncoder, indent=4 if indent else None, sort_keys=True) diff --git a/erepublik_script/classes.py b/erepublik_script/classes.py new file mode 100644 index 0000000..af1147a --- /dev/null +++ b/erepublik_script/classes.py @@ -0,0 +1,1046 @@ +# pylint: disable=fixme, line-too-long, missing-docstring, invalid-name +import datetime +import decimal +import hashlib +import random +import time +from collections import deque +from json import JSONDecodeError, loads, JSONEncoder +from typing import Any, Dict, List, Union + +from requests import Response, Session +from slugify import slugify + +from erepublik_script import utils + + +class ErepublikException(Exception): + def __init__(self, message): + super().__init__(message) + + +class ErepublikNetworkException(Exception): + def __init__(self, message, request): + super().__init__(message) + self.request = request + + +class MyCompanies: + work_units: int = 0 + next_ot_time: datetime.datetime + holdings: Dict[int, Dict] = dict() + companies: Dict[int, Dict] = dict() + ff_lockdown: int = 0 + + def __init__(self): + self.next_ot_time = utils.now() + + def prepare_holdings(self, holdings: dict): + """ + :param holdings: Parsed JSON to dict from en/economy/myCompanies + """ + self.holdings = {} + template = dict(id=0, num_factories=0, region_id=0, companies=[]) + + for holding_id, holding in holdings.items(): + tmp: Dict[str, Union[List[Any], Any]] = {} + for key in template: + if key == 'companies': + tmp.update({key: []}) + else: + tmp.update({key: holding[key]}) + self.holdings.update({int(holding_id): tmp}) + self.holdings.update({0: template}) # unassigned + + def prepare_companies(self, companies: dict): + """ + :param companies: Parsed JSON to dict from en/economy/myCompanies + """ + self.companies = {} + 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, + 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) + + 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]) + tmp.update({key: company[key]}) + self.companies.update({int(c_id): tmp}) + + def update_holding_companies(self): + 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() + + def get_employable_factories(self) -> Dict[int, int]: + ret = {} + for company_id, company in self.companies.items(): + if company.get('preset_works'): + preset_works: int = int(company.get('preset_works', 0)) + ret.update({company_id: preset_works}) + return ret + + def get_total_wam_count(self) -> int: + ret = 0 + for holding_id in self.holdings: + ret += self.get_holding_wam_count(holding_id) + return ret + + def get_holding_wam_count(self, holding_id: int, raw_factory=None) -> int: + """ + Returns amount of wam enabled companies in the holding + :param holding_id: holding id + :param raw_factory: True - only raw, False - only factories, None - both + :return: int + """ + return len(self.get_holding_wam_companies(holding_id, raw_factory)) + + def get_holding_employee_count(self, holding_id): + employee_count = 0 + if holding_id in self.holdings: + for company_id in self.holdings.get(holding_id, {}).get('companies', []): + employee_count += self.companies.get(company_id).get('preset_works', 0) + return employee_count + + def get_holding_wam_companies(self, holding_id: int, raw_factory: bool = None) -> List[int]: + """ + Returns WAM enabled companies in the holding, True - only raw, False - only factories, None - both + :param holding_id: holding id + :param raw_factory: bool or None + :return: list + """ + raw = [] + factory = [] + if holding_id in self.holdings: + for company_id in self.holdings.get(holding_id, {}).get('companies', []): + company = self.companies.get(company_id, {}) + wam_enabled = bool(company.get('wam_enabled', {})) + already_worked = not company.get('already_worked', {}) + cannot_work_war = company.get("cannot_work_as_manager_reason", {}) == "war" + if wam_enabled and already_worked and not cannot_work_war: + if company.get('is_raw', False): + raw.append(company_id) + else: + factory.append(company_id) + if raw_factory is not None and not raw_factory: + return factory + elif raw_factory is not None and raw_factory: + return raw + elif raw_factory is None: + return raw + factory + else: + raise ErepublikException("raw_factory should be True/False/None") + + def get_needed_inventory_usage(self, company_id: int = None, companies: list = None) -> float: + if not any([companies, company_id]): + return 0. + if company_id: + if company_id not in self.companies: + raise ErepublikException("Company ({}) not in all companies list".format(company_id)) + company = self.companies[company_id] + if company.get("is_raw"): + return float(company["base_production"]) * company["effective_bonus"] + else: + products_made = company["base_production"] * company["effective_bonus"] / 100 + # raw_used = products_made * company['upgrades'][str(company['quality'])]['raw_usage'] * 100 + return float(products_made - company['raw_usage']) + if companies: + return float(sum([self.get_needed_inventory_usage(company_id=cid) for cid in companies])) + + raise ErepublikException("Wrong function call") + + +class SlowRequests(Session): + last_time: datetime.datetime + timeout = datetime.timedelta(milliseconds=500) + uas = [ + # Chrome + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/73.0.3683.103 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/72.0.3626.13 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/71.0.3578.98 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.11 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36', + # FireFox + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0', + 'Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0', + ] + debug = False + + def __init__(self): + super().__init__() + 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': random.choice(self.uas) + }) + + @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) + + 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) + return resp + + def _slow_down_requests(self): + ltt = utils.good_timedelta(self.last_time, self.timeout) + if ltt > utils.now(): + seconds = (ltt - utils.now()).total_seconds() + time.sleep(seconds if seconds > 0 else 0) + self.last_time = utils.now() + + def _log_request(self, url, method, data=None, json=None, params=None, **kwargs): + if self.debug: + args = {} + args.update({'kwargs': kwargs}) + if data: + args.update({"data": data}) + + if json: + args.update({"json": json}) + + if params: + args.update({"params": params}) + + body = "[{dt}]\tURL: '{url}'\tMETHOD: {met}\tARGS: {args}\n".format(dt=utils.now().strftime("%F %T"), + url=url, met=method, args=args) + + with open(self.request_log_name, 'ab') as file: + file.write(body.encode("UTF-8")) + + def _log_response(self, url, resp, redirect: bool = False): + from erepublik_script import Citizen + 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) + + # 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'), + "name": slugify(url[len(Citizen.url):]), + "extra": "_REDIRECT" if redirect else "" + } + + try: + loads(resp.text) + file_data.update({"ext": "json"}) + except JSONDecodeError: + file_data.update({"ext": "html"}) + + filename = 'debug/requests/{time}_{name}{extra}.{ext}'.format(**file_data) + with open(utils.get_file(filename), 'wb') as f: + f.write(resp.text.encode('utf-8')) + + +class Config: + email = "" + password = "" + work = True + train = True + wam = False + auto_sell: List[str] = list() + auto_sell_all = False + employees = False + fight = False + air = False + ground = False + all_in = False + next_energy = False + boosters = False + travel_to_fight = False + always_travel = False + epic_hunt = False + epic_hunt_ebs = False + rw_def_side = False + interactive = True + continuous_fighting = False + auto_buy_raw = False + force_wam = False + sort_battles_time = True + force_travel = False + + @property + def wt(self): + return self.work and self.train + + def __dict__(self) -> Dict[str, Union[bool, str, List[str]]]: + return dict( + email=self.email, + password=self.password, + 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, + 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, + ) + + +class Energy: + limit = 500 # energyToRecover + interval = 10 # energyPerInterval + recoverable = 0 # energyFromFoodRemaining + recovered = 0 # energy + _recovery_time = None + + def __init__(self): + self._recovery_time = utils.now() + + def __repr__(self): + return "{:4}/{:4} + {:4}, {:3}hp/6min".format(self.recovered, self.limit, self.recoverable, self.interval) + + def set_reference_time(self, recovery_time: datetime.datetime): + self._recovery_time = recovery_time.replace(microsecond=0) + + @property + def food_fights(self): + return (self.recoverable + self.recovered) // 10 + + @property + def reference_time(self): + if self.is_recovered_full or self._recovery_time < utils.now(): + ret = utils.now() + else: + ret = self._recovery_time + return ret + + @property + def is_recoverable_full(self): + return self.recoverable >= self.limit - self.interval + + @property + def is_recovered_full(self): + return self.recovered >= self.limit - self.interval + + @property + def is_energy_full(self): + return self.is_recoverable_full and self.is_recovered_full + + @property + def available(self): + return self.recovered + self.recoverable + + def __dict__(self): + return dict( + limit=self.limit, + interval=self.interval, + recoverable=self.recoverable, + recovered=self.recovered, + reference_time=self.reference_time + ) + + +class Details(object): + xp = 0 + cc = 0 + pp = 0 + pin = None + gold = 0 + next_pp: List[int] = [] + citizen_id = 0 + citizenship = 0 + current_region = 0 + current_country = 0 + residence_region = 0 + residence_country = 0 + daily_task_done = False + 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, } + + @property + def xp_till_level_up(self): + if self.xp >= 10000: + next_level_up = (1 + (self.xp // 5000)) * 5000 + elif self.xp >= 7000: + next_level_up = 10000 + elif self.xp >= 3000: + next_level_up = (1 + ((self.xp - 1000) // 2000)) * 2000 + 1000 + elif self.xp >= 2000: + next_level_up = 3000 + elif self.xp >= 450: + next_level_up = (1 + (self.xp // 500)) * 500 + elif self.xp >= 370: + next_level_up = (1 + ((self.xp - 10) // 40)) * 40 + 10 + elif self.xp >= 300: + next_level_up = (1 + ((self.xp - 20) // 35)) * 35 + 20 + elif self.xp >= 150: + next_level_up = (1 + (self.xp // 30)) * 30 + elif self.xp >= 50: + next_level_up = (1 + ((self.xp - 10) // 20)) * 20 + 10 + elif self.xp >= 20: + next_level_up = (1 + ((self.xp - 5) // 15)) * 15 + 5 + else: + next_level_up = (1 + (self.xp // 10)) * 10 + return next_level_up - self.xp + + +class Politics: + is_party_member: bool = False + + party_id: int = 0 + party_slug: str = "" + is_party_president: bool = False + is_congressman: bool = False + is_country_president: bool = False + + +class House(object): + quality = None + unactivated_count = 0 + active_untill = utils.good_timedelta(utils.now(), -datetime.timedelta(days=1)) + + def __init__(self, quality: int): + if 0 < quality < 6: + self.quality = quality + + @property + def next_ot_point(self) -> datetime.datetime: + return self.active_untill + + +class CitizenAPI: + url = "https://www.erepublik.com/en" + _req = SlowRequests + + def __init__(self): + self._req = SlowRequests() + + def post(self, url: str, *args, **kwargs) -> Response: + return self._req.post(url, *args, **kwargs) + + def get(self, url: str, **kwargs) -> Response: + return self._req.get(url, **kwargs) + + def get_article_json(self, article_id: int) -> Response: + return self.get("{}/main/articleJson/{}".format(self.url, article_id)) + + def get_battlefield_choose_side(self, battle: int, side: int) -> Response: + return self.get("{}/military/battlefield-choose-side/{}/{}".format(self.url, battle, side)) + + def get_candidate_party(self, party_slug: str) -> Response: + return self.post("{}/candidate/{}".format(self.url, party_slug)) + + def get_citizen_hovercard(self, citizen: int) -> Response: + return self.get("{}/main/citizen-hovercard/{}".format(self.url, citizen)) + + def get_citizen_profile(self, player_id: int): + return self.get("{}/main/citizen-profile-json/{}".format(self.url, player_id)) + + def get_citizen_daily_assistant(self): + return self.get("{}/main/citizenDailyAssistant".format(self.url)) + + def get_city_data_residents(self, city: int, page: int = 1, params: Dict[str, Any] = {}): + return self.get("{}/main/city-data/{}/residents".format(self.url, city), params={"currentPage": page, **params}) + + def get_country_military(self, country: str) -> Response: + return self.get("{}/country/military/{}".format(self.url, country)) + + def get_economy_inventory_items(self) -> Response: + return self.get("{}/economy/inventory-items/".format(self.url)) + + def get_economy_job_market_json(self, country: int) -> Response: + return self.get("{}/economy/job-market-json/{}/1/desc".format(self.url, country)) + + def get_economy_my_companies(self) -> Response: + return self.get("{}/economy/myCompanies".format(self.url)) + + def get_economy_my_market_offers(self) -> Response: + return self.get("{}/economy/myMarketOffers".format(self.url)) + + def get_job_data(self) -> Response: + return self.get("{}/main/job-data".format(self.url)) + + def get_leaderboards_damage_aircraft_rankings(self, country: int, weeks: int = 0, mu: int = 0) -> Response: + data = (country, weeks, mu) + return self.get("{}/main/leaderboards-damage-aircraft-rankings/{}/{}/{}/0".format(self.url, *data)) + + def get_leaderboards_damage_rankings(self, country: int, weeks: int = 0, mu: int = 0, div: int = 0) -> Response: + data = (country, weeks, mu, div) + return self.get("{}/main/leaderboards-damage-rankings/{}/{}/{}/{}".format(self.url, *data)) + + def get_leaderboards_kills_aircraft_rankings(self, country: int, weeks: int = 0, mu: int = 0) -> Response: + data = (country, weeks, mu) + return self.get("{}/main/leaderboards-kills-aircraft-rankings/{}/{}/{}/0".format(self.url, *data)) + + def get_leaderboards_kills_rankings(self, country: int, weeks: int = 0, mu: int = 0, div: int = 0) -> Response: + data = (country, weeks, mu, div) + return self.get("{}/main/leaderboards-kills-rankings/{}/{}/{}/{}".format(self.url, *data)) + + def get_main(self): + return self.get(self.url) + + def get_message_alerts(self, page: int = 1) -> Response: + return self.get_message_alerts(page) + + def get_military_campaigns(self) -> Response: + return self.get("{}/military/campaigns-new/".format(self.url)) + + def get_military_unit_data(self, unit_id: int, page: int = 1) -> Response: + params = {"groupId": unit_id, "panel": "members", "currentPage": page} + return self.get("{}/military/military-unit-data/".format(self.url), params=params) + + def get_money_donation_accept(self, token: str, donation_id: int) -> Response: + return self.get("{}/main/money-donation/accept/{}".format(self.url, donation_id), params={"_token": token}) + + def get_money_donation_reject(self, token: str, donation_id: int) -> Response: + return self.get("{}/main/money-donation/reject/{}".format(self.url, donation_id), params={"_token": token}) + + def get_party_members(self, party: int) -> Response: + return self.get("{}/main/party-members/{}".format(self.url, party)) + + def get_rankings_parties(self, country: int) -> Response: + return self.get("{}/main/rankings-parties/1/{}".format(self.url, country)) + + def get_training_grounds_json(self) -> Response: + return self.get("{}/main/training-grounds-json".format(self.url)) + + def get_weekly_challenge_data(self) -> Response: + return self.get("{}/main/weekly-challenge-data".format(self.url)) + + def post_activate_battle_effect(self, token: str, battle: int, kind: str, citizen_id: int) -> Response: + data = dict(battleId=battle, citizenId=citizen_id, type=kind, _token=token) + return self.post("{}/main/fight-activateBattleEffect".format(self.url), data=data) + + def post_article_comments(self, token: str, article: int, page: int = 0) -> Response: + data = dict(_token=token, articleId=article, page=page) + if page: + data.update({'page': page}) + return self.post("{}/main/articleComments".format(self.url), data=data) + + def post_article_comments_create(self, token: str, message: str, article: int, parent: int = 0) -> Response: + data = dict(_token=token, message=message, articleId=article) + if parent: + data.update({"parentId": parent}) + return self.post("{}/main/articleComments/create".format(self.url), data=data) + + def post_battle_console(self, token: str, battle: int, zone: int, round_id: int, division: int, page: int, + damage: bool) -> Response: + data = dict(battleId=battle, zoneId=zone, action="battleStatistics", round=round_id, division=division, + leftPage=page, rightPage=page, _token=token) + if damage: + data.update({"type": "damage"}) + else: + data.update({"type": "kills"}) + + return self.post("{}/military/battle-console".format(self.url), data=data) + + def post_buy_gold_items(self, token: str, currency: str, item: str, amount: int) -> Response: + data = dict(itemId=item, currency=currency, amount=amount, _token=token) + return self.post("{}/main/buyGoldItems".format(self.url), data=data) + + def post_candidate_for_congress(self, token: str, presentation: str = "") -> Response: + data = dict(_token=token, presentation=presentation) + return self.post("{}/candidate-for-congress".format(self.url), data=data) + + def post_citizen_add_remove_friend(self, token: str, citizen: int, add: bool) -> Response: + data = dict(_token=token, citizenId=citizen, url="//www.erepublik.com/en/main/citizen-addRemoveFriend") + if add: + data.update({"action": "addFriend"}) + else: + data.update({"action": "removeFriend"}) + return self.post("{}/main/citizen-addRemoveFriend".format(self.url), data=data) + + def post_collect_anniversary_reward(self, token: str) -> Response: + return self.post("{}/main/collect-anniversary-reward".format(self.url), data={"_token": token}) + + def post_country_donate(self, token: str, country: int, action: str, value: Union[int, float], quality: int = None): + json = dict(countryId=country, action=action, _token=token, value=value, quality=quality) + return self.post("{}/main/country-donate".format(self.url), data=json, + headers={"Referer": "{}/country/economy/Latvia".format(self.url)}) + + def post_daily_task_reward(self, token: str) -> Response: + return self.post("{}/main/daily-tasks-reward".format(self.url), data=dict(_token=token)) + + def post_delete_message(self, token: str, msg_id: list) -> Response: + data = {"_token": token, "delete_message[]": msg_id} + return self.post("{}/main/messages-delete".format(self.url), data) + + def post_eat(self, token: str, color: str) -> Response: + data = dict(_token=token, buttonColor=color) + return self.post("{}/main/eat".format(self.url), params=data) + + def post_economy_activate_house(self, token: str, quality: int) -> Response: + data = {"action": "activate", "quality": quality, "type": "house", "_token": token} + return self.post("{}/economy/activateHouse".format(self.url), data=data) + + def post_economy_assign_to_holding(self, token: str, factory: int, holding: int) -> Response: + data = dict(_token=token, factoryId=factory, action="assign", holdingCompanyId=holding) + return self.post("{}/economy/assign-to-holding".format(self.url), data=data) + + def post_economy_create_company(self, token: str, industry: int, building_type: int = 1) -> Response: + data = {"_token": token, "company[industry_id]": industry, "company[building_type]": building_type} + return self.post("{}/economy/create-company".format(self.url), data=data, + headers={"Referer": "{}/economy/create-company".format(self.url)}) + + def post_economy_donate_items_action(self, token: str, citizen: int, amount: int, industry: int, + quality: int) -> Response: + data = dict(citizen_id=citizen, amount=amount, industry_id=industry, quality=quality, _token=token) + return self.post("{}/economy/donate-items-action".format(self.url), data=data, + headers={"Referer": "{}/economy/donate-items/{}".format(self.url, citizen)}) + + def post_economy_donate_money_action(self, token: str, citizen: int, amount: float = 0.0, + currency: int = 62) -> Response: + data = dict(citizen_id=citizen, _token=token, currency_id=currency, amount=amount) + return self.post("{}/economy/donate-money-action".format(self.url), data=data, + headers={"Referer": "{}/economy/donate-money/{}".format(self.url, citizen)}) + + def post_economy_exchange_purchase(self, token: str, amount: float, currency: int, offer: int) -> Response: + data = dict(_token=token, amount=amount, currencyId=currency, offerId=offer) + return self.post("{}/economy/exchange/purchase/".format(self.url), data=data) + + def post_economy_exchange_retrieve(self, token: str, personal: bool, page: int, currency: int) -> Response: + data = dict(_token=token, personalOffers=int(personal), page=page, currencyId=currency) + return self.post("{}/economy/exchange/retrieve/".format(self.url), data=data) + + def post_economy_job_market_apply(self, token: str, citizen: int, salary: int) -> Response: + data = dict(_token=token, citizenId=citizen, salary=salary) + return self.post("{}/economy/job-market-apply".format(self.url), data=data) + + def post_economy_marketplace(self, token: str, 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=token) + return self.post("{}/economy/marketplaceAjax".format(self.url), data=data) + + def post_economy_marketplace_actions(self, token: str, amount: int, buy: bool = False, **kwargs) -> Response: + if buy: + data = dict(_token=token, offerId=kwargs['offer'], amount=amount, orderBy="price_asc", currentPage=1, + buyAction=1) + else: + data = dict(_token=token, countryId=kwargs["country"], price=kwargs["price"], industryId=kwargs["industry"], + quality=kwargs["quality"], amount=amount, sellAction='postOffer') + return self.post("{}/economy/marketplaceActions".format(self.url), data=data) + + def post_economy_resign(self, token: str) -> Response: + return self.post("{}/economy/resign".format(self.url), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={"_token": token, "action_type": "resign"}) + + def post_economy_sell_company(self, token: str, factory: int, pin: int = None, sell: bool = True) -> Response: + url = "{}/economy/sell-company/{}".format(self.url, factory) + data = dict(_token=token, pin="" if pin is None else pin) + if sell: + data.update({"sell": "sell"}) + else: + data.update({"dissolve": factory}) + return self.post(url, data=data, headers={"Referer": url}) + + def post_economy_train(self, token: str, tg_ids: List[int]) -> Response: + data: Dict[str, Union[int, str]] = {} + if not tg_ids: + return self.get_training_grounds_json() + else: + for idx, tg_id in enumerate(tg_ids): + data["grounds[%i][id]" % idx] = tg_id + data["grounds[%i][train]" % idx] = 1 + if data: + data['_token'] = token + return self.post("{}/economy/train".format(self.url), data=data) + + def post_economy_upgrade_company(self, token: str, factory: int, level: int, pin: str = None) -> Response: + data = dict(_token=token, type="upgrade", companyId=factory, level=level, pin="" if pin is None else pin) + return self.post("{}/economy/upgrade-company".format(self.url), data=data) + + def post_economy_work(self, token: str, action_type: str, wam: List[int] = None, employ: Dict[int, int] = None): + """ + :return: requests.Response or None + """ + if employ is None: + employ = dict() + if wam is None: + wam = [] + data: Dict[str, Union[int, str]] = dict(action_type=action_type, _token=token) + if action_type == "work": + return self.post("{}/economy/work".format(self.url), data=data) + elif action_type == "production": + max_idx = 0 + for idx, company_id in enumerate(sorted(wam or [])): + data.update({ + "companies[%i][id]" % idx: company_id, + "companies[%i][employee_works]" % idx: employ.pop(company_id, 0), + "companies[%i][own_work]" % idx: 1 + }) + max_idx = idx + 1 + for idx, company_id in enumerate(sorted(employ or [])): + idx_ = idx + max_idx + data.update({ + "companies[%i][id]" % idx_: company_id, + "companies[%i][employee_works]" % idx_: employ.pop(company_id), + "companies[%i][own_work]" % idx_: 0 + }) + return self.post("{}/economy/work".format(self.url), data=data) + else: + return + + def post_economy_work_overtime(self, token: str) -> Response: + data = dict(action_type="workOvertime", _token=token) + return self.post("{}/economy/workOvertime".format(self.url), data=data) + + def post_forgot_password(self, token: str, email: str) -> Response: + data = dict(_token=token, email=email, commit="Reset password") + return self.post("{}/forgot-password".format(self.url), data=data) + + def post_fight_activate_booster(self, token: str, battle: int, quality: int, duration: int, kind: str) -> Response: + data = dict(type=kind, quality=quality, duration=duration, battleId=battle, _token=token) + return self.post("{}/military/fight-activateBooster".format(self.url), data=data) + + def post_login(self, token: str, email: str, password: str) -> Response: + data = dict(csrf_token=token, citizen_email=email, citizen_password=password, remember='on') + return self.post("{}/login".format(self.url), data=data) + + def post_messages_alert(self, token: str, notification_ids: list) -> Response: + data = {"_token": token, "delete_alerts[]": notification_ids, "deleteAllAlerts": "1", "delete": "Delete"} + return self.post("{}/main/messages-alerts/1".format(self.url), data=data) + + def post_messages_compose(self, token: str, 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=token, citizen_message=body) + return self.post("{}/main/messages-compose/{}}".format(self.url, url_pk), data=data) + + def post_military_battle_console(self, token: str, battle_id: int, round_id: int, division: int) -> Response: + data = dict(battleId=battle_id, zoneId=round_id, action="battleStatistics", round=round_id, division=division, + type="damage", leftPage=1, rightPage=1, _token=token) + return self.post("{}/military/battle-console".format(self.url, battle_id), data=data) + + def post_military_fight_air(self, token: str, battle_id: int, side_id: int) -> Response: + data = dict(sideId=side_id, battleId=battle_id, _token=token) + return self.post("{}/military/fight-shoooot/{}".format(self.url, battle_id), data=data) + + def post_military_fight_ground(self, token: str, battle_id: int, side_id: int) -> Response: + data = dict(sideId=side_id, battleId=battle_id, _token=token) + return self.post("{}/military/fight-shooot/{}".format(self.url, battle_id), data=data) + + def post_military_group_missions(self, token: str) -> Response: + data = dict(action="check", _token=token) + return self.post("{}/military/group-missions".format(self.url), data=data) + + def post_travel(self, token: str, check: str, **kwargs) -> Response: + data = dict(_token=token, check=check, **kwargs) + return self.post("{}/main/travel".format(self.url), data=data) + + def post_travel_data(self, token: str, **kwargs) -> Response: + return self.post("{}/main/travelData".format(self.url), data=dict(_token=token, **kwargs)) + + def post_wars_attack_region(self, token: str, war: int, region: int) -> Response: + data = dict(_token=token) + return self.post("{}/wars/attack-region/{}/{}".format(self.url, war, region), data=data) + + def post_weekly_challenge_reward(self, token: str, reward_id: int) -> Response: + data = dict(_token=token, rewardId=reward_id) + return self.post("{}/main/weekly-challenge-collect-reward".format(self.url), data=data) + + def post_write_article(self, token: str, title: str, content: str, location: int, kind: int) -> Response: + data = dict(_token=token, article_name=title, article_body=content, article_location=location, + article_category=kind) + return self.post("{}/main/write-article".format(self.url), data=data) + + # Wall Posts + # ## Country + + def post_country_comment_retrieve(self, token: str, post_id: int): + data = {"_token": token, "postId": post_id} + return self.post("{}/main/country-comment/retrieve/".format(self.url), data=data) + + def post_country_post_create(self, token: str, body: str, post_as: int): + data = {"_token": token, "post_message": body, "post_as": post_as} + return self.post("{}/main/country-post/create/".format(self.url), data=data) + + def post_country_post_retrieve(self, token: str): + data = {"_token": token, "page": 1, "switchedFrom": False} + return self.post("{}/main/country-post/retrieve/".format(self.url), data=data) + + # ## Military Unit + + def post_military_unit_comment_retrieve(self, token: str, post_id: int): + data = {"_token": token, "postId": post_id} + return self.post("{}/main/military-unit-comment/retrieve/".format(self.url), data=data) + + def post_military_unit_post_create(self, token: str, body: str, post_as: int): + data = {"_token": token, "post_message": body, "post_as": post_as} + return self.post("{}/main/military-unit-post/create/".format(self.url), data=data) + + def post_military_unit_post_retrieve(self, token: str): + data = {"_token": token, "page": 1, "switchedFrom": False} + return self.post("{}/main/military-unit-post/retrieve/".format(self.url), data=data) + + # ## Party + + def post_party_comment_retrieve(self, token: str, post_id: int): + data = {"_token": token, "postId": post_id} + return self.post("{}/main/party-comment/retrieve/".format(self.url), data=data) + + def post_party_post_create(self, token: str, body: str): + data = {"_token": token, "post_message": body} + return self.post("{}/main/party-post/create/".format(self.url), data=data) + + def post_party_post_retrieve(self, token: str): + data = {"_token": token, "page": 1, "switchedFrom": False} + return self.post("{}/main/party-post/retrieve/".format(self.url), data=data) + + # ## Friend's Wall + + def post_wall_comment_retrieve(self, token: str, post_id: int): + data = {"_token": token, "postId": post_id} + return self.post("{}/main/wall-comment/retrieve/".format(self.url), data=data) + + def post_wall_post_create(self, token: str, body: str): + data = {"_token": token, "post_message": body} + return self.post("{}/main/wall-post/create/".format(self.url), data=data) + + def post_wall_post_retrieve(self, token: str): + data = {"_token": token, "page": 1, "switchedFrom": False} + return self.post("{}/main/wall-post/retrieve/".format(self.url), data=data) + + +class Reporter: + __to_update: List[Dict[Any, Any]] = [] + name: str = "" + email: str = "" + citizen_id: int = 0 + key: str = "" + allowed: bool = False + + def __init__(self): + self._req = Session() + self.url = "https://api.erep.lv" + self._req.headers.update({"user-agent": "Bot reporter v2"}) + 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 + self.citizen_id: int = citizen_id + self.key: str = "" + self.__update_key() + self.allowed = True + + def __update_key(self): + self.key = hashlib.md5(bytes(f"{self.name}:{self.email}", encoding="UTF-8")).hexdigest() + self.allowed = True + self.register_account() + + def __bot_update(self, data: dict) -> Response: + if self.__to_update: + for unreported_data in self.__to_update: + unreported_data.update(player_id=self.citizen_id, key=self.key) + self._req.post("{}/bot/update".format(self.url), json=unreported_data) + self.__to_update.clear() + r = self._req.post("{}/bot/update".format(self.url), json=data) + return r + + def register_account(self): + if not self.__registered: + try: + r = self.__bot_update(dict(key=self.key, check=True, player_id=self.citizen_id)) + if not r.json().get("status"): + self._req.post("{}/bot/register".format(self.url), json=dict(name=self.name, email=self.email, + player_id=self.citizen_id)) + finally: + self.__registered = True + self.report_action("STARTED", value=utils.now().strftime("%F %T")) + + def send_state_update(self, xp: int, cc: float, gold: float, inv_total: int, inv: int, + hp_limit: int, hp_interval: int, hp_available: int, food: int, pp: int): + + data = dict(key=self.key, player_id=self.citizen_id, state=dict( + xp=xp, cc=cc, gold=gold, inv_total=inv_total, inv_free=inv_total - inv, inv=inv, food=food, + pp=pp, hp_limit=hp_limit, hp_interval=hp_interval, hp_available=hp_available, + )) + + if self.allowed: + self.__bot_update(data) + + def report_action(self, action: str, json_val: Dict[Any, Any] = None, value: str = None): + if not self.key: + if not all([self.email, self.name, self.citizen_id]): + pass + json_data = {'player_id': self.citizen_id, 'key': self.key, 'log': dict(action=action)} + if json_val: + json_data['log'].update(dict(json=json_val)) + if value: + json_data['log'].update(dict(value=value)) + if self.allowed: + self.__bot_update(json_data) + else: + self.__to_update.append(json_data) + + +class MyJSONEncoder(JSONEncoder): + def default(self, o): + 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) + elif isinstance(o, datetime.date): + return dict(__type__='date', year=o.year, month=o.month, day=o.day) + 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(_content=o._content.decode("UTF-8"), headers=o.headers.__dict__, url=o.url, text=o.text) + elif hasattr(o, '__dict__'): + return o.__dict__ + elif isinstance(o, deque): + return list(o) + return super().default(o) + + +class BattleSide: + id: int + points: int + deployed: List[int] = list() + allies: List[int] = list() + + def __init__(self, country_id: int, points: int, allies: List[int], deployed: List[int]): + self.id = country_id + self.points = points + self.allies = [int(ally) for ally in allies] + self.deployed = [int(ally) for ally in deployed] + + +class BattleDivision: + end: datetime.datetime + epic: bool + dom_pts: Dict[str, int] = dict() + wall: Dict[str, Union[int, float]] = dict() + + @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.update({"inv": inv_pts, "def": def_pts}) + self.wall.update({"for": wall_for, "dom": wall_dom}) + + +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] = dict() + + @property + def is_air(self) -> bool: + return not bool(self.zone_id % 4) + + def __init__(self, battle: dict): + 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)) + 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.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']]) + + for div, data in battle.get('div', {}).items(): + div = int(div) + if data.get('end'): + end = datetime.datetime.fromtimestamp(data.get('end'), tz=utils.erep_tz) + else: + end = datetime.datetime.max + + self.div.update({div: BattleDivision(end, data.get('epic_type') in [1, 5], + data.get('dom_pts').get("inv"), data.get('dom_pts').get("def"), + data.get('wall').get("for"), data.get('wall').get("dom"))}) + + def __repr__(self): + now = utils.now() + is_started = self.start < utils.now() + if is_started: + timepart = "{}".format(now - self.start) + else: + timepart = "- {}".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, timepart) + + +class EnergyToFight: + energy: int = 0 + + def __init__(self, energy: int = 0): + self.energy = energy + + def __int__(self): + return self.energy + + def __str__(self): + return str(self.energy) + + def __repr__(self): + return str(self.energy) + + @property + def i(self): + return self.__int__() + + @property + def s(self): + return self.__str__() + + def check(self, new_energy: int): + if not isinstance(new_energy, (tuple, int)): + return self.energy + if 0 < new_energy < self.energy: + self.energy = new_energy + return self.energy diff --git a/erepublik_script/cli.py b/erepublik_script/cli.py new file mode 100644 index 0000000..fbd007c --- /dev/null +++ b/erepublik_script/cli.py @@ -0,0 +1,423 @@ +# -*- coding: utf-8 -*- + +"""Console script for erepublik_script.""" + +__author__ = """Eriks Karls""" +__email__ = 'eriks@72.lv' +__version__ = '0.1.0' + +import json +import os +import random +import sys +import threading +from collections import defaultdict +from datetime import timedelta +from typing import List, Tuple + +import click + +from erepublik_script import classes, utils +from erepublik_script.citizen import Citizen + + +__all__ = ["Citizen"] + +INTERACTIVE = True +CONFIG = defaultdict(bool) + + +@click.command() +def main(args=None): + player = None + try: # If errors before player is initialized + while True: + player = Citizen(email=CONFIG['email'], password=CONFIG['password']) + if player.logged_in: + break + utils.silent_sleep(2) + player.config.work = CONFIG['work'] + player.config.train = CONFIG['train'] + player.config.ot = CONFIG['ot'] + player.config.wam = bool(CONFIG['wam']) + player.config.employees = bool(CONFIG['employ']) + player.config.auto_sell = CONFIG.get('auto_sell', []) + player.config.auto_sell_all = CONFIG.get('auto_sell_all', False) + player.config.auto_buy_raw = CONFIG.get('auto_buy_raw', False) + player.config.force_wam = CONFIG.get('force_wam', False) + player.config.fight = CONFIG['fight'] + player.config.air = CONFIG['air'] + player.config.ground = CONFIG['ground'] + player.config.all_in = CONFIG['all_in'] + player.config.next_energy = CONFIG['next_energy'] + player.config.boosters = CONFIG['boosters'] + player.config.travel_to_fight = CONFIG['travel_to_fight'] + player.config.always_travel = CONFIG.get('always_travel', False) + player.config.epic_hunt = CONFIG['epic_hunt'] + player.config.epic_hunt_ebs = CONFIG['epic_hunt_ebs'] + player.config.rw_def_side = CONFIG['rw_def_side'] + player.config.random_sleep = CONFIG['random_sleep'] + player.config.continuous_fighting = CONFIG['continuous_fighting'] + player.config.interactive = CONFIG['interactive'] + player.reporter.allowed = not CONFIG.get('reporting_is_not_allowed') + + player.set_debug(CONFIG.get('debug', False)) + while True: + try: + player.update_all() + break + except: + utils.silent_sleep(2) + + now = utils.now() + dt_max = now.replace(year=9999) + tasks = { + 'eat': now, + } + wam_hour = employ_hour = 14 + 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.fight: + tasks.update({'fight': now}) + if player.config.wam: + wam_hour = 14 + if not isinstance(CONFIG['wam'], bool): + try: + wam_hour = abs(int(CONFIG['wam'])) % 24 + except ValueError: + pass + tasks.update({'wam': now.replace(hour=wam_hour, minute=0, second=0, microsecond=0)}) + if player.config.employees: + employ_hour = 8 + if not isinstance(CONFIG['employ'], bool): + try: + employ_hour = abs(int(CONFIG['employ'])) % 24 + except ValueError: + pass + tasks.update({'employ': now.replace(hour=employ_hour, minute=0, second=0, microsecond=0)}) + + if player.config.epic_hunt: + tasks['epic_hunt'] = now + + if CONFIG.get("renew_houses", True): + tasks['renew_houses'] = now + + if CONFIG.get('start_battles'): + """ {'start_battle': {war_id: {'regions': [region_id, ], + 'timing': ['at', 'hh:mm' | 'before', 'hh:mm' (before autoattack) | + 'auto' (after round for citizenship country's oldest battle or at 00:00) + 'rw', (after first round of RW if you are occupying)]}} """ + player.allowed_battles = CONFIG.get('start_battles', dict()) + raise classes.ErepublikException("Battle starting is not implemented") + + if player.reporter.allowed: + report = dict(CONFIG) + report.pop("email", None) + report.pop("password", None) + report.update( + VERSION=utils.VERSION, + COMMIT_ID=utils.COMMIT_ID + ) + player.reporter.report_action("ACTIVE_CONFIG", json_val=report) + # -1 because main thread is counted in + name = "{}-state_updater-{}".format(player.name, threading.active_count() - 1) + state_thread = threading.Thread(target=player.state_update_repeater, name=name) + state_thread.start() + + if CONFIG.get("congress", True): + tasks['congress'] = now.replace(hour=1, minute=30, second=0) + + if CONFIG.get("party_president", False): + tasks['party_president'] = now.replace(hour=1, minute=30, second=0) + + contribute_cc = int(CONFIG.get("contribute_cc", 0)) + if contribute_cc: + tasks['contribute_cc'] = now.replace(hour=2, minute=0, second=0) + + if CONFIG.get("gold_buy"): + tasks['gold_buy'] = now.replace(hour=23, minute=57, second=0, microsecond=0) + + error_count = 0 + while error_count < 3: + try: + now = utils.now() + 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 = 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 = 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 = now.replace(hour=wam_hour, minute=0, second=0, microsecond=0) + timedelta(days=1) + else: + next_time = 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('fight', dt_max) <= now or player.energy.is_energy_full: + fight_energy_debug_log: List[Tuple[int, str]] = [] + player.write_log("Doing task: fight") + player.write_log(player.health_info) + + if player.should_fight(): + player.find_battle_and_fight() + else: + player.collect_weekly_reward() + energy = classes.EnergyToFight(player.details.xp_till_level_up * 10 - player.energy.limit + 50) + fight_energy_debug_log.append(( + energy.i, + f"Levelup reachable {player.details.xp_till_level_up} * 10 - {player.energy.limit} + 50" + )) + + # Do levelup + energy.check(player.details.xp_till_level_up * 10 + 50) + fight_energy_debug_log.append(( + energy.i, f"Levelup {player.details.xp_till_level_up} * 10 + 50" + )) + + # if levelup is close stop queueing other fighting + if not player.is_levelup_close: + + # Obligatory need 75pp + if player.details.pp < 75: + energy.check(75 - player.details.pp) + fight_energy_debug_log.append((energy.i, f"Obligatory need 75pp: 75 - {player.details.pp}")) + + if player.config.continuous_fighting and player.has_battle_contribution: + energy.check(player.energy.interval) + fight_energy_debug_log.append((energy.i, f"continuous_fighting: {player.energy.interval}")) + + # All-in + if player.config.all_in: + energy.check(player.energy.limit * 2 - 3 * player.energy.interval) + fight_energy_debug_log.append(( + energy.i, f"All-in: {player.energy.limit} * 2 - 3 * {player.energy.interval}" + )) + elif player.energy.limit * 2 - 3 * player.energy.interval >= player.energy.recovered: + # 1h worth of energy + energy.check(player.energy.limit * 2 - 3 * player.energy.interval) + fight_energy_debug_log.append( + (energy.i, f"1h worth of energy: {player.energy.interval} * 10" + )) + + # All-in for AIR battles + if all([player.config.air, player.config.all_in, + player.energy.available >= player.energy.limit]): + energy.check(player.energy.limit) + fight_energy_debug_log.append(( + energy.i, f"All-in for AIR battles: {player.energy.limit}" + )) + + # Get to next Energy +1 + if player.next_reachable_energy and player.config.next_energy: + energy.check(player.next_reachable_energy * 10) + fight_energy_debug_log.append(( + energy.i, f"Get to next Energy +1: {player.next_reachable_energy} * 10" + )) + + energy = energy.i - player.energy.available + next_minutes = max([6, abs(energy) // player.energy.interval * 6]) + # utils.write_silent_log("\n".join([f"{energy} {info}" for energy, info in fight_energy_debug_log])) + next_time = player.energy.reference_time + timedelta(minutes=next_minutes) + tasks.update({'fight': 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}) + + if tasks.get('employ', dt_max) <= now: + player.write_log("Doing task: Employee work") + next_time = utils.now().replace(hour=employ_hour, minute=0, second=0) + timedelta(days=1) + next_time = next_time if player.work_employees() else tasks.get('employ') + timedelta(minutes=30) + tasks.update({'employ': next_time}) + + if tasks.get('epic_hunt', dt_max) <= now: + player.write_log("Doing task: EPIC check") + player.check_epic_battles() + if player.active_fs: + next_time = now + timedelta(minutes=1) + else: + next_time = tasks.get('eat') + tasks.update({'epic_hunt': next_time}) + + if tasks.get('gold_buy', dt_max) <= now: + player.write_log("Doing task: auto buy 10g") + for offer in player.get_monetary_offers(): + if offer['amount'] >= 10 and player.details.cc >= 20 * offer["price"]: + # TODO: check allowed amount to buy + if player.buy_monetary_market_offer(offer=offer['offer_id'], amount=10, currency=62): + break + + next_time = tasks.get('gold_buy') + timedelta(days=1) + tasks.update({'gold_buy': next_time}) + + if tasks.get('congress', dt_max) <= now: + if 1 <= now.day < 16: + next_time = now.replace(day=16) + elif 16 <= now.day < 24: + player.write_log("Doing task: candidate for congress") + player.candidate_for_congress() + if not now.month == 12: + next_time = now.replace(month=now.month + 1, day=16) + else: + next_time = now.replace(year=now.year + 1, month=1, day=16) + else: + if not now.month == 12: + next_time = now.replace(month=now.month + 1, day=16) + else: + next_time = now.replace(year=now.year + 1, month=1, day=16) + tasks.update({'congress': next_time.replace(hour=1, minute=30, second=0, microsecond=0)}) + + if tasks.get('party_president', dt_max) <= now: + if not now.day == 15: + player.write_log("Doing task: candidate for party president") + player.candidate_for_party_presidency() + if not now.month == 12: + next_time = now.replace(month=now.month + 1) + else: + next_time = now.replace(year=now.year + 1, month=1) + else: + if not now.month == 12: + next_time = now.replace(month=now.month + 1) + else: + next_time = now.replace(year=now.year + 1, month=1) + tasks.update(party_president=next_time.replace(day=16, hour=0, minute=0, second=0, microsecond=0)) + + if tasks.get('contribute_cc', dt_max) <= now: + if not now.weekday(): + player.update_money() + cc = (player.details.cc // contribute_cc) * contribute_cc + player.write_log("Doing task: Contribute {}cc to Latvia".format(cc)) + player.contribute_cc_to_country(cc) + next_time = now + timedelta(days=7 - now.weekday()) + next_time = next_time.replace(hour=2, minute=0, second=0) + tasks.update({'contribute_cc': next_time}) + + if tasks.get('renew_houses', dt_max) <= now: + player.write_log("Doing task: Renew houses") + end_times = player.renew_houses() + if end_times: + tasks.update(renew_houses=min(end_times.values()) - timedelta(hours=24)) + else: + player.write_log("No houses found! Forcing q1 usage...") + end_times = player.buy_and_activate_house(1) + if not end_times: + tasks.update(renew_houses=now + timedelta(hours=6)) + else: + tasks.update(renew_houses=min(end_times.values()) - timedelta(hours=24)) + + 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 + random_seconds = random.randint(0, 121) if player.config.random_sleep else 0 + sleep_seconds = int(utils.get_sleep_seconds(closest_next_time)) + if sleep_seconds <= 0: + raise classes.ErepublikException(f"Loop detected! Offending task: '{next_tasks[0]}'") + closest_next_time += timedelta(seconds=random_seconds) + player.write_log("My next Tasks and there time:\n" + "\n".join(sorted(next_tasks))) + player.write_log("Sleeping until (eRep): {} (sleeping for {}s + random {}s)".format( + closest_next_time.strftime("%F %T"), sleep_seconds, random_seconds)) + seconds_to_sleep = sleep_seconds + random_seconds if sleep_seconds > 0 else 0 + player.sleep(seconds_to_sleep) + + except classes.ErepublikNetworkException: + player.write_log('Network ERROR detected. Sleeping for 1min...') + player.sleep(60) + except (KeyboardInterrupt, SystemExit): + sys.exit(1) + except classes.ErepublikException as e: + utils.process_error(f"Known error detected! {e}", player.name, sys.exc_info(), player, utils.COMMIT_ID) + except: + utils.process_error("Unknown error!", player.name, sys.exc_info(), player, utils.COMMIT_ID) + error_count += 1 + if error_count < 3: + player.sleep(60) + finally: + if error_count >= 3: + player.stop_threads.set() + player.stop_threads.set() + player.write_log('Too many errors.') + except (KeyboardInterrupt, SystemExit): + sys.exit(1) + except classes.ErepublikException: + utils.process_error("[{}] To many errors.".format(utils.COMMIT_ID), player.name, sys.exc_info(), player, + utils.COMMIT_ID) + except: + if isinstance(player, Citizen): + name = player.name + elif CONFIG.get('email', None): + name = CONFIG['email'] + else: + name = "Uninitialized" + utils.process_error("[{}] Fatal error.".format(utils.COMMIT_ID), name, sys.exc_info(), player, utils.COMMIT_ID) + sys.exit(1) + + +if __name__ == "__main__": + assert sys.version_info >= (3, 7, 1) + + write_log = utils.write_silent_log + + try: + with open('config.json', 'r') as f: + CONFIG = json.load(f) + + write_log('Config file found. Checking...') + CONFIG = utils.parse_config(CONFIG) + except: + CONFIG = utils.parse_config() + + with open('config.json', 'w') as f: + json.dump(CONFIG, f, indent=True, sort_keys=True) + if CONFIG['interactive']: + write_log = utils.write_interactive_log + else: + write_log = utils.write_silent_log + write_log('\nTo quit press [ctrl] + [c]', False) + os.chdir(os.path.dirname(os.path.realpath(__file__))) + write_log('Version: ' + utils.VERSION) + while True: + main() + write_log("Restarting after 1h") + utils.interactive_sleep(60 * 60) diff --git a/erepublik_script/utils.py b/erepublik_script/utils.py new file mode 100644 index 0000000..2c956e5 --- /dev/null +++ b/erepublik_script/utils.py @@ -0,0 +1,730 @@ +import datetime +import inspect +import json +import os +import re +import sys +import time +import traceback +from collections import deque +from decimal import Decimal +from json import JSONEncoder +from pathlib import Path +from typing import Union + +import pytz +import requests +from requests import Response +from slugify import slugify + + +__all__ = ["FOOD_ENERGY", "VERSION", "COMMIT_ID", "COUNTRIES", "erep_tz", + "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", ] + + +FOOD_ENERGY = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20) +VERSION = "v0.14.1" +COMMIT_ID = "7b92e19" + +erep_tz = pytz.timezone('US/Pacific') +AIR_RANKS = {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*****", } + +GROUND_RANKS = {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", } + +COUNTRIES = {1: 'Romania', 9: 'Brazil', 10: 'Italy', 11: 'France', 12: 'Germany', 13: 'Hungary', 14: 'China', + 15: 'Spain', 23: 'Canada', 24: 'USA', 26: 'Mexico', 27: 'Argentina', 28: 'Venezuela', 29: 'United Kingdom', + 30: 'Switzerland', 31: 'Netherlands', 32: 'Belgium', 33: 'Austria', 34: 'Czech Republic', 35: 'Poland', + 36: 'Slovakia', 37: 'Norway', 38: 'Sweden', 39: 'Finland', 40: 'Ukraine', 41: 'Russia', 42: 'Bulgaria', + 43: 'Turkey', 44: 'Greece', 45: 'Japan', 47: 'South Korea', 48: 'India', 49: 'Indonesia', 50: 'Australia', + 51: 'South Africa', 52: 'Republic of Moldova', 53: 'Portugal', 54: 'Ireland', 55: 'Denmark', 56: 'Iran', + 57: 'Pakistan', 58: 'Israel', 59: 'Thailand', 61: 'Slovenia', 63: 'Croatia', 64: 'Chile', 65: 'Serbia', + 66: 'Malaysia', 67: 'Philippines', 68: 'Singapore', 69: 'Bosnia and Herzegovina', 70: 'Estonia', + 71: 'Latvia', 72: 'Lithuania', 73: 'North Korea', 74: 'Uruguay', 75: 'Paraguay', 76: 'Bolivia', 77: 'Peru', + 78: 'Colombia', 79: 'Republic of Macedonia (FYROM)', 80: 'Montenegro', 81: 'Republic of China (Taiwan)', + 82: 'Cyprus', 83: 'Belarus', 84: 'New Zealand', 164: 'Saudi Arabia', 165: 'Egypt', + 166: 'United Arab Emirates', 167: 'Albania', 168: 'Georgia', 169: 'Armenia', 170: 'Nigeria', 171: 'Cuba'} + + +class MyJSONEncoder(JSONEncoder): + def default(self, o): + if isinstance(o, 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) + elif isinstance(o, datetime.date): + return dict(__type__='date', year=o.year, month=o.month, day=o.day) + 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=o.headers.__dict__, url=o.url, text=o.text) + elif hasattr(o, '__dict__'): + return o.__dict__ + elif isinstance(o, deque): + return list(o) + return super().default(o) + + +def now(): + return datetime.datetime.now(erep_tz).replace(microsecond=0) + + +def localize_timestamp(timestamp: int): + return datetime.datetime.fromtimestamp(timestamp, erep_tz) + + +def localize_dt(dt: Union[datetime.date, datetime.datetime]): + if isinstance(dt, datetime.date): + dt = datetime.datetime.combine(dt, datetime.time(0, 0, 0)) + return erep_tz.localize(dt) + + +def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime: + return erep_tz.normalize(dt + td) + + +def eday_from_date(date: Union[datetime.date, datetime.datetime] = now()) -> int: + if isinstance(date, datetime.date): + date = datetime.datetime.combine(date, datetime.time(0, 0, 0)) + return (date - datetime.datetime(2007, 11, 20, 0, 0, 0)).days + + +def date_from_eday(eday: int) -> datetime.date: + return localize_dt(datetime.date(2007, 11, 20)) + datetime.timedelta(days=eday) + + +def get_sleep_seconds(time_untill: datetime.datetime) -> int: + """ time_until aware datetime object Wrapper for sleeping until """ + sleep_seconds = int((time_untill - now()).total_seconds()) + return sleep_seconds if sleep_seconds > 0 else 0 + + +def interactive_sleep(sleep_seconds: int): + while sleep_seconds > 0: + seconds = sleep_seconds + if (seconds - 1) // 1800: + seconds = seconds % 1800 if seconds % 1800 else 1800 + elif (seconds - 1) // 300: + seconds = seconds % 300 if seconds % 300 else 300 + elif (seconds - 1) // 60: + seconds = seconds % 60 if seconds % 60 else 60 + # elif (seconds - 1) // 30: + # seconds = seconds % 30 if seconds % 30 else 30 + else: + seconds = 1 + sys.stdout.write("\rSleeping for {:4} more seconds".format(sleep_seconds)) + sys.stdout.flush() + time.sleep(seconds) + sleep_seconds -= seconds + sys.stdout.write("\r") + + +silent_sleep = time.sleep + + +def _write_log(msg, timestamp: bool = True, should_print: bool = False): + erep_time_now = now() + txt = "[{}] {}".format(erep_time_now.strftime('%F %T'), msg) if timestamp else msg + if not os.path.isdir('log'): + os.mkdir('log') + with open("log/%s.log" % erep_time_now.strftime('%F'), 'a', encoding="utf-8") as f: + f.write("%s\n" % txt) + if should_print: + print(txt) + + +def write_interactive_log(*args, **kwargs): + _write_log(should_print=True, *args, **kwargs) + + +def write_silent_log(*args, **kwargs): + _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") + else: + version = 1 + try: + version = int(file.suffix[1:]) + 1 + basename = file.stem + except ValueError: + basename = file.name + version += 1 + + full_name = file.parent / f"{basename}.{version}" + while full_name.exists(): + version += 1 + full_name = file.parent / f"{basename}.{version}" + return str(full_name) + else: + os.makedirs(file.parent, exist_ok=True) + return str(file) + + +def write_file(filename: str, content: str) -> int: + filename = get_file(filename) + with open(filename, 'ab') as f: + return f.write(content.encode("utf-8")) + + +def write_request(response: requests.Response, is_error: bool = False): + from erepublik_script 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 = "debug/requests/{}_{}.{}".format(now().strftime('%F_%H-%M-%S'), name, ext) + write_file(filename, html) + else: + return {"name": "{}_{}.{}".format(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, content: list, player=None, local_vars=dict, promo: bool = False, captcha: bool = False): + from erepublik_script import Citizen + + file_content_template = "
+-
+
+
+
+
+
+
+ {{_getTranslation('wallTexts','report')}}
+
+
+
+
+
+ {{comment.numVotes}}
+
+
+
+
+
+ {{comment.reactionId ? settings.reactions[comment.reactionId]:_getTranslation('feed','like')}}
+
+ {{comment.createdAtTimeAgo}}
+
+
+
+
++ + {{post.commentsData.citizenData[comment.authorId].name}} + +
+ + +