erepublik-ebot/ebot/aviator_support.py
2022-07-04 10:44:45 +03:00

1059 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

""" eBot
Copyright (C) 2022 Eriks K
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
import os
import pathlib
import re
import sys
from io import BytesIO
from itertools import product
from operator import attrgetter
from typing import Any, Dict, List, Set, Union
import httpx
from cairosvg import svg2png
from erepublik import classes, constants, utils
from erepublik.citizen import CitizenEconomy, CitizenLeaderBoard, CitizenMedia, CitizenSocial
from erepublik.constants import AIR_RANKS, GROUND_RANKS, Rank
forbidden_ids = []
class MyOfferItem:
def __init__(self, **kwargs):
for k, v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
price: float = 999_999_999.0
country: constants.Country = constants.Country(0, "", "", "")
amount: int = 0
offer_id: int = 0
citizen_id: int = 0
quality: int = 0
price_per_use: float = 0.0
true_amount: int = 0
def __repr__(self):
return (
f"MyOfferItem(price={self.price}, country={repr(self.country)}, amount={self.amount}, "
f"offer_id={self.offer_id}, citizen_id={self.citizen_id}, quality={self.quality}, "
f"price_per_use={self.price_per_use}, true_amount={self.true_amount})"
)
class _OfferHolder:
_content: List[MyOfferItem]
_limit: int
def __init__(self, limit: int):
self._content = []
self._limit = limit
def append(self, obj: MyOfferItem):
self._content.append(obj)
self.check()
def sort(self):
# for equal price per use, largest amount offers will be first
self._content.sort(key=attrgetter("true_amount"), reverse=True)
self._content.sort(key=attrgetter("price_per_use"))
@property
def total_amount(self) -> int:
return sum(o.true_amount for o in self._content)
def check(self):
self.sort()
while (self.total_amount - self._content[-1].true_amount) >= self._limit:
self._content.pop(-1)
if self._content[-1].true_amount > (self.total_amount - self._limit) > 0:
o = self._content[-1]
ta = o.true_amount
a = o.amount
coef = ta / a
o.true_amount -= self.total_amount - self._limit
o.amount = int(o.true_amount / coef)
@property
def list(self) -> List[MyOfferItem]:
return self._content
def __iter__(self):
for i in self._content:
yield i
class Contestant:
id: int
name: str
air_kill_count: int
ground_kill_count: int
air_rank: Rank
ground_rank: Rank
residence: int
military_unit: int
has_house: bool
done_7_dos: bool
factory_count_increased: bool
factory_count: int
extra: List[Any]
def __init__(self, _id: int):
self.id = _id
self.name = ""
self.air_kill_count = 0
self.ground_kill_count = 0
self.air_rank = AIR_RANKS[1]
self.ground_rank = GROUND_RANKS[1]
self.residence = 0
self.factory_count = 0
self.factory_count_increased = False
self.done_7_dos = False
self.has_house = False
self.extra = []
self.military_unit = 0
@property
def as_dict(self) -> Dict[str, Any]:
ret = self.__dict__
ret.update(
_props=dict(
link=self.link,
air_rank_ok=self._air_rank_ok,
ground_rank_penalty=self.ground_rank_penalty,
air_health=self.air_health,
air_health_total=self.air_health_total,
ground_health=self.ground_health,
ground_weapons=self.ground_weapons,
air_rank_s=self.air_rank_s,
ground_rank_s=self.ground_rank_s,
total_health=self.total_health,
total_kills=self.total_kills,
)
)
return ret
@property
def link(self):
return f"https://www.erepublik.com/en/citizen/profile/{self.id}"
@property
def _air_rank_ok(self) -> bool:
return self.air_rank.id < 56
@property
def ground_rank_penalty(self) -> int:
if self.ground_rank.id < 70:
return 0
else:
return 100 - (89 - self.ground_rank.id) * 5
@property
def air_health(self):
if self.has_house and self._air_rank_ok:
return self.air_kill_count * (30 if self.air_rank.id < 50 else 20 if self.air_rank.id < 56 else 0)
else:
return 0
@property
def air_health_total(self):
if self.has_house and self._air_rank_ok:
return self.air_health * (1 + self.factory_count_increased) + 3500 * self.done_7_dos
else:
return 0
@property
def ground_health(self):
amount = self.ground_kill_count * 20 if self.has_house else 0
return int((amount * (100 - self.ground_rank_penalty)) / 100) // 2 * 2
@property
def ground_weapons(self):
amount = self.ground_kill_count // 4 if self.has_house else 0
return int((amount * (100 - self.ground_rank_penalty)) / 100)
@property
def air_rank_s(self) -> str:
return self.air_rank.name
@property
def ground_rank_s(self) -> str:
return self.ground_rank.name
@property
def total_health(self) -> int:
return (self.air_health_total + self.ground_health) if self.has_house else 0
@property
def total_kills(self) -> int:
return self.air_kill_count + self.ground_kill_count
@classmethod
def from_dict(cls, **data) -> "Contestant":
obj: Contestant = cls(data["id"])
obj.name = data["name"]
obj.air_kill_count = data["air_kill_count"]
obj.ground_kill_count = data["ground_kill_count"]
obj.air_rank = AIR_RANKS[data["air_rank"]["id"]]
obj.ground_rank = GROUND_RANKS[data["ground_rank"]["id"]]
obj.residence = data["residence"]
obj.factory_count = data["factory_count"]
obj.factory_count_increased = data["factory_count_increased"]
obj.done_7_dos = data["done_7_dos"]
obj.has_house = data["has_house"]
obj.extra = data["extra"]
obj.military_unit = data["military_unit"]
return obj
class SupplierCitizen(CitizenLeaderBoard, CitizenMedia, CitizenEconomy, CitizenSocial):
def __init__(self, email: str, password: str):
"""Instantiate Stripped down version of erepublik.Citizen adjusted for aviator support.
:param email: Citizen's email address
:param password: Citizen's password
:type email: str
:type password: str
"""
super().__init__(email, password)
self.stop_threads.set()
self._req.debug = True
self.config.interactive = True
self.set_debug(True)
self.init_logger()
def update(self):
self.get_csrf_token()
self.update_citizen_info()
self.update_inventory()
self.update_money()
if not os.environ.get("PYTHON_TESTS"):
self.reporter.do_init()
self.telegram.do_init(417412798, "363081107:AAE4MmIz_TJh_mFkNDA5MDkwZTY4YjI1ZWJ", self.name) # noqa
self.telegram.send_message(f"*Started aviator supply* {utils.now():%F %T}")
def get_weekly_daily_orders_done(self, name: str, mu_id: int, provisional: bool) -> bool:
weeks_ago = int(not bool(provisional))
params = dict(
currentPage=1,
panel="members",
sortBy="dailyOrdersCompleted",
weekFilter=f"week{weeks_ago}",
search=name,
)
member = self._get_military_unit_data(mu_id, **params).json()
try:
return bool(member.get("panelContents", {}).get("members", [{}])[0].get("allDailyOrdersCompleted"))
except: # noqa
return False
def get_citizen_profile(self, player_id: int = None):
return self._get_main_citizen_profile_json(player_id).json()
def direct_get_citizen_residency_data(self, name: str, city_id: int):
return self._get_main_city_data_residents(city_id, params={"search": name}).json()
def get_multiple_market_offers(
self, prod: str, amount: int, q: int = None, glob: bool = False
) -> List[MyOfferItem]:
raw_short_names = dict(frm="foodRaw", wrm="weaponRaw", hrm="houseRaw", arm="airplaneRaw")
q1_industries = list(raw_short_names.values())
q5_industries = ["house", "aircraft", "ticket"]
industry_id = constants.INDUSTRIES[prod]
if prod in raw_short_names:
q = 1
prod = raw_short_names[prod]
elif not industry_id:
self.report_error(f"Industry '{prod}' not implemented")
raise classes.ErepublikException(f"Industry '{prod}' not implemented")
max_quality = 1 if prod in q1_industries else 5 if prod.lower() in q5_industries else 7
q = max_quality if q and q > max_quality else q
if glob:
countries: Set[constants.Country] = self.get_countries_with_regions()
else:
countries: Set[constants.Country] = {self.details.citizenship}
ret_list: _OfferHolder = _OfferHolder(amount)
start_dt = self.now
iterable = [countries, [q] if q else range(1, max_quality + 1)]
for country, qlt in product(*iterable):
r = self._post_economy_marketplace(country.id, industry_id, qlt).json()
if r.get("error"):
self.write_warning(f"{country}: {r.get('message', '')}", extra={"resp": r})
continue
for offer in r["offers"]:
price = float(offer["priceWithTaxes"])
amount = int(offer["amount"])
true_amount = amount * constants.FOOD_ENERGY[f"q{qlt}"] if industry_id == 1 else amount
ppu = price / (constants.FOOD_ENERGY[f"q{qlt}"] if industry_id == 1 else 1)
ret_list.append(
MyOfferItem(
price=price,
country=country,
amount=amount,
offer_id=int(offer["id"]),
citizen_id=int(offer["citizen_id"]),
quality=qlt,
price_per_use=ppu,
true_amount=true_amount,
)
)
self.logger.debug(f"Scraped market in {self.now - start_dt}!")
return ret_list.list
def buy_multiple_market_offers(self, offers: Union[List[MyOfferItem], _OfferHolder]):
cur_country = self.details.current_country
for offer in sorted(offers, key=attrgetter("country.id")):
if offer.country != cur_country:
for x in range(3):
if self.travel_to_country(offer.country):
break
else:
raise classes.ErepublikException(f"Unable to travel to {offer.country}!")
cur_country = offer.country
self.buy_market_offer(offer, offer.amount) # noqa
self.travel_to_residence()
class LatvianSupply:
citizen: SupplierCitizen = None
free_energy: int = 0
organisation: SupplierCitizen = None
CONSOLE_TABLE_FORMAT: str = "{:>9} | {:<28} | {:26} | {:6} | {:26} | {:6} | {:^4} | {:^10} | {}"
contestants: Dict[int, Contestant]
air_top: Dict[int, int]
ground_top: Dict[int, int]
already_sent: Dict[int, Dict[str, int]]
context: Dict[str, Union[str, int, float]]
debug: bool
provisional: bool
checkpoint_id: int = 0
def __init__(self, citizen: SupplierCitizen, debug: bool = False, provisional: bool = False):
self.citizen = citizen
self.contestants = {}
self.air_top = {}
self.ground_top = {}
self.already_sent = {}
self.context = dict(
PLAYER_COUNT=0,
TABLE_AIR="",
TABLE_GROUND="",
TABLE_TOTAL="",
STARTING_ENERGY=0,
TOTAL_CC=0,
TOTAL_ENERGY=0,
END_ENERGY=0,
)
self.citizen.config.interactive = True
self.citizen._req.debug = True # noqa
self.citizen.set_debug(True)
self.debug = debug
self.provisional = provisional
if debug:
citizen.write_log("Running in DEBUG mode!")
if provisional:
citizen.write_log("Running in PROVISIONAL mode!")
def __call__(self, *args, **kwargs):
starting_checkpoint_id = kwargs.get("checkpoint_id") or 0
# Step 1: Setup primary data
if starting_checkpoint_id < 1:
latest_article = self.get_latest_article_data()
self.context["STARTING_ENERGY"] = sum(
amount * constants.FOOD_ENERGY[q] for q, amount in latest_article.get("free_food", {}).items()
)
self.free_energy = self.context["STARTING_ENERGY"]
self.context.update(TOTAL_WEEK=latest_article.get("week", 0) + 1)
self.context.update(WEEK=self.context["TOTAL_WEEK"] - 184)
article_id = latest_article.get("article_id")
self.dump_checkpoint(1, **{"article_id": article_id, "context": self.context})
else:
data = self.load_checkpoint(1)
article_id = data["article_id"]
self.context.update(**data["context"])
# Step 2: Setup ranking data
if starting_checkpoint_id < 2:
for top_data in self.citizen.get_aircraft_kill_rankings(71, 0 if self.provisional else 1, 0).get("top"):
self.air_top.update({int(top_data.get("id")): int(top_data["values"])})
for top_data in self.citizen.get_ground_kill_rankings(71, 0 if self.provisional else 1, 0, 0).get("top"):
self.ground_top.update({int(top_data.get("id")): int(top_data["values"])})
self.dump_checkpoint(2, **{"air_top": self.air_top, "ground_top": self.ground_top})
else:
data = self.load_checkpoint(2)
self.air_top = data["air_top"]
self.ground_top = data["ground_top"]
# Step 3: Get contestants
if starting_checkpoint_id < 3:
comments: Dict[str, Any] = self.citizen.get_article_comments(article_id, 1)
if not comments.get("comments", {}):
raise classes.ErepublikException("No comments found")
self.citizen.write_log(
self.CONSOLE_TABLE_FORMAT.format(
"ID", "Vārds", "G rangs", "G kili", "A rangs", "A kili", "7do", "Fabrikas", "Papildu"
)
)
self.set_contestants_from_comments(comments.get("comments", {}))
self.context["PLAYER_COUNT"] = len(self.contestants)
self.dump_checkpoint(3, **dict(contestants=self.contestants, context=self.context))
else:
data = self.load_checkpoint(3)
self.context.update(**data["context"])
for contestant in data["contestants"].values():
cont = Contestant.from_dict(**contestant)
self.contestants.update({cont.id: cont})
# Step 4: calculate expenses and buy necessary items
if starting_checkpoint_id < 4:
cc_spent_on_food = cc_spent_on_tanks = 0
previously_sent = dict(food=0, tanks=0)
for a in self.already_sent.values():
previously_sent["food"] += a["food"]
previously_sent["tanks"] += a["tanks"]
total_food_requirement = sum(c.total_health for c in self.contestants.values())
self.context["TOTAL_ENERGY"] = total_food_requirement
total_food_requirement -= self.free_energy
if total_food_requirement - previously_sent["food"] > 0:
food_offers = self.citizen.get_multiple_market_offers(
"food", total_food_requirement - previously_sent["food"], glob=True
)
cc_spent_on_food = round(sum(o.amount * o.price for o in food_offers), 2)
if self.debug or self.provisional:
for offer in food_offers:
self.citizen.logger.debug(repr(offer))
else:
self.citizen.buy_multiple_market_offers(food_offers)
total_tank_requirement = sum(c.ground_weapons for c in self.contestants.values())
self.context["TOTAL_TANKS"] = total_tank_requirement
if total_tank_requirement - previously_sent["tanks"] > 0:
tank_offers = self.citizen.get_multiple_market_offers(
"weapon",
total_tank_requirement - previously_sent["tanks"],
7,
glob=(not self.debug and not self.provisional),
)
cc_spent_on_tanks = round(sum(o.amount * o.price for o in tank_offers), 2)
if self.debug or self.provisional:
for offer in tank_offers:
self.citizen.logger.debug(repr(offer))
else:
self.citizen.buy_multiple_market_offers(tank_offers)
self.context["TOTAL_CC"] = round(cc_spent_on_food + cc_spent_on_tanks * 1.1, 2)
self.citizen.update_inventory()
self.dump_checkpoint(4, **{"context": self.context})
else:
data = self.load_checkpoint(4)
self.context.update(**data["context"])
# Step 5: send supplies
if starting_checkpoint_id < 5:
sent_data = []
for contestant in sorted(
self.contestants.values(),
key=attrgetter("total_health", "total_kills"),
reverse=True,
):
self.send_food_supplies(contestant.id, sent_data)
self.send_tank_supplies(contestant.id, sent_data)
with open(utils.get_file(f"{self.citizen.eday}.json"), "w") as f:
utils.json_dump(sent_data, f)
self.citizen.write_log(f"Not used energy: {self.free_energy:,d}hp")
self.context["END_ENERGY"] = self.free_energy
self.dump_checkpoint(5, **{"context": self.context})
else:
data = self.load_checkpoint(5)
self.context.update(**data["context"])
# Step 6: Generate images for article
if starting_checkpoint_id < 6:
image_title = f"Day {self.start_eday}-{self.end_eday}"
air_url = self.get_air_image_url(image_title)
ground_url = self.get_ground_image_url(image_title)
total_url = self.get_total_image_url(image_title)
self.context.update(AIR_URL=air_url, GROUND_URL=ground_url, TOTAL_URL=total_url)
self.dump_checkpoint(6, **{"context": self.context})
else:
data = self.load_checkpoint(6)
self.context.update(**data["context"])
# Step 7: Generate article data
if starting_checkpoint_id < 7:
for k, v in self.prepare_article_image().items():
self.context[f"TABLE_{k.upper()}"] = v
article_body = self.generate_article_body()
article_data = dict(
content=article_body,
from_eday=self.start_eday,
till_eday=self.end_eday,
title=f'[KM] eLatviešu apgāde [d{self.article_eday} {self.citizen.now.strftime("%H:%M")}]',
comment=(
f"★★★★ APGĀDE PAR NEDĒĻU [DAY {self.start_eday}-{self.end_eday}] IZDALĪTA ★★★★\n"
"★ Apgādei piesakies šī komentāra atbildes komentāros ar saucienu - piesakos! ★"
),
)
self.citizen.write_log(
f"Publishing info:\n\n### Article ###\n{article_data['title']}\n\n{article_data['comment']}\n\n"
)
self.dump_checkpoint(7, **{"context": self.context, "article_data": article_data})
else:
data = self.load_checkpoint(7)
self.context.update(**data["context"])
article_data = data["article_data"]
self.organisation = SupplierCitizen("organisation.email@example.com", "<censored>")
self.organisation.set_debug(True)
self.organisation.update()
while not self.organisation.token:
self.organisation.update_citizen_info()
# Step 8: refund
if starting_checkpoint_id < 8:
if not self.debug and not self.provisional:
self.organisation.donate_money(self.citizen.details.citizen_id, int(self.context["TOTAL_CC"] + 0.5), 1)
self.organisation.update_money()
refund_cc = int(250_000.5 - self.organisation.details.cc)
if refund_cc > 0:
wall_body = (
"★★★ [ PAZIŅOJUMS KONGRESAM ] ★★★\n\n"
f"Veikta apgāde par d{self.start_eday}-{self.end_eday} {refund_cc}cc apmērā!\n\n"
f"Ierosiniet naudas pārskaitījumu uz {self.organisation.name}\n"
)
self.citizen.write_log("### Wall ###\n" f"{wall_body}")
self.citizen.telegram.send_message(wall_body)
# Step 9: Publish article
if starting_checkpoint_id < 9 and not self.debug and not self.provisional:
comment_data = dict(message=article_data.pop("comment"))
new_article_id = self.organisation.publish_article(article_data["title"], article_data["content"], 3)
comment_data.update(article_id=new_article_id)
self.organisation.write_article_comment(**comment_data)
self.organisation.vote_article(new_article_id)
self.citizen.vote_article(new_article_id)
self.citizen.endorse_article(new_article_id, 100)
httpx.post(
"https://erep.lv/aviator/latest_article/",
json=dict(
week=self.context["TOTAL_WEEK"], article_id=new_article_id, free_food={"q1": self.free_energy // 2}
),
)
httpx.post(
"https://erep.lv/aviator/set/",
json=[
dict(
id=aviator.id, name=aviator.name, factory_count=aviator.factory_count, rank=aviator.air_rank.id
)
for aviator in self.contestants.values()
],
)
with open(f'aviator_support_{self.citizen.eday}_{self.citizen.now.strftime("%F %T")}.json', "w") as f:
utils.json_dump(self, f)
@property
def offset(self):
return int(self.debug and not self.provisional)
@property
def as_dict(self):
return self.__dict__
def get_latest_article_data(self):
data = httpx.get(f"https://erep.lv/aviator/latest_article/{self.offset}/").json()
if not data.get("status"):
raise classes.ErepublikException("Article ID and week problem")
return data
def set_contestants_from_comments(self, comments):
time_string = "%Y-%m-%d %H:%M:%S"
for comment_data in comments.values():
if comment_data.get("authorId") == 1954361:
start_dt = utils.localize_dt(datetime.datetime.strptime(comment_data.get("createdAt"), time_string))
days_ahead = 1 - start_dt.weekday()
if days_ahead <= 0:
days_ahead += 7
end_dt = utils.good_timedelta(start_dt, datetime.timedelta(days_ahead)).replace(
hour=0, minute=0, second=0
)
if not comment_data.get("replies", {}):
raise classes.ErepublikException("No replies found")
for reply_data in comment_data.get("replies").values():
if utils.localize_dt(datetime.datetime.strptime(reply_data.get("createdAt"), time_string)) > end_dt:
continue
if re.search(r"piesakos", reply_data.get("message"), re.I):
self._setup_contestant(reply_data)
@property
def start_eday(self):
now = utils.now() - datetime.timedelta(days=utils.now().weekday())
return utils.eday_from_date(constants.erep_tz.normalize(now - datetime.timedelta(days=6)))
@property
def end_eday(self):
return self.start_eday + 6
@property
def article_eday(self):
return self.end_eday + 1
def send_food_supplies(self, contestant_id: int, actions: List[Any]):
contestant = self.contestants[contestant_id]
food = {q: self.citizen.food[q] for q in reversed(list(constants.FOOD_ENERGY.keys())) if self.citizen.food[q]}
health = (contestant.air_health_total + contestant.ground_health) // 2 * 2
if contestant.id in self.already_sent:
sent = self.already_sent[contestant.id]["food"]
health -= sent
if health < 0:
health = 0
actions.append(dict(player_id=contestant.id, name=contestant.name, amount=sent, industry=1))
self.free_energy -= sent
if self.free_energy < 0:
self.free_energy = 0
if not health:
# actions.append(dict(player_id=contestant.id, name=contestant.name, amount=0, industry=1))
return
while health > 0:
food = {
q: food.get(q) for q in reversed(list(constants.FOOD_ENERGY.keys())) if food.get(q) and food.get(q) > 0
}
for quality, amount in food.items():
if constants.FOOD_ENERGY[quality] <= health:
break
else:
self.citizen.write_warning(f"{contestant.name} ({contestant.id}) needs to receive extra {health}hp")
break
if amount * constants.FOOD_ENERGY[quality] > health:
amount = health // constants.FOOD_ENERGY[quality]
q = int(quality[1])
if self.debug or self.provisional:
self.citizen.logger.debug(
f"citizen.donate_items(citizen_id={contestant.id}, " f"amount={amount}, industry_id=1, quality={q})"
)
else:
donated = self.citizen.donate_items(citizen_id=contestant.id, amount=amount, industry_id=1, quality=q)
if donated != amount:
health -= donated * constants.FOOD_ENERGY[quality]
self.citizen.write_warning(f"{contestant.name} ({contestant.id}) needs to receive extra {health}hp")
actions.append(
dict(
player_id=contestant.id,
name=contestant.name,
amount=donated * constants.FOOD_ENERGY[quality],
industry=1,
)
)
break
food[quality] -= amount
self.citizen.food[quality] -= amount
self.citizen.inventory.final["Food"][q]["amount"] -= amount
health -= amount * constants.FOOD_ENERGY[quality]
actions.append(
dict(
player_id=contestant.id,
name=contestant.name,
amount=amount * constants.FOOD_ENERGY[quality],
industry=1,
)
)
def send_tank_supplies(self, contestant_id: int, actions: List[Any]):
contestant = self.contestants[contestant_id]
tank_amount = contestant.ground_weapons
if contestant.id in self.already_sent:
sent = self.already_sent[contestant_id]["tanks"]
tank_amount -= sent
if tank_amount < 0:
tank_amount = 0
actions.append(dict(player_id=contestant.id, name=contestant.name, amount=sent, industry=2))
if not tank_amount:
# actions.append(dict(player_id=contestant.id, name=contestant.name, amount=0, industry=2))
return
if self.debug or self.provisional:
self.citizen.logger.debug(
f"citizen.donate_items(citizen_id={contestant.id}, " f"amount={tank_amount}, industry_id=2, quality=7)"
)
donated = tank_amount
else:
donated = self.citizen.donate_items(citizen_id=contestant.id, amount=tank_amount, industry_id=2, quality=7)
if donated != tank_amount:
tank_amount -= donated
self.citizen.write_log(
f"{contestant.name} ({contestant.id}) needs to receive extra {tank_amount} q7 tanks"
)
actions.append(dict(player_id=contestant.id, name=contestant.name, amount=donated, industry=2))
def get_air_image_url(self, title: str) -> str:
return self.upload_image_and_get_url(self._create_aviator_png(title))
def get_ground_image_url(self, title: str) -> str:
return self.upload_image_and_get_url(self._create_ground_png(title))
def get_total_image_url(self, title: str) -> str:
return self.upload_image_and_get_url(self._create_total_png(title))
def prepare_article_image(self) -> Dict[str, str]:
contestant_captions = {"ground": [], "air": [], "total": []}
for contestant in sorted(
self.contestants.values(), key=attrgetter("total_kills", "total_health"), reverse=True
):
bb_link = f"[url={contestant.link}]{contestant.name}[/url]"
if contestant.total_health:
contestant_captions["total"].append(bb_link)
for k, v in contestant_captions.items():
v = ", ".join(v)
if k == "total":
v = f"Apgādi saņēma: {v}."
contestant_captions[k] = v
title_trans = {"air": "Gaisa apgāde", "ground": "Zemes apgāde", "total": ""}
return {
name: (
f"[b]{title_trans[name]}[/b]\n"
f"[center][url={self.context[url_key]}][img]{self.context[url_key]}[/img][/url][/center]\n"
f"{contestant_captions[name]}"
)
for name, url_key in (
("air", "AIR_URL"),
("ground", "GROUND_URL"),
("total", "TOTAL_URL"),
)
}
def generate_article_body(self) -> str:
filename = f"{os.path.abspath(os.path.dirname(sys.argv[0]))}/scripts/KM_apgade.txt"
if os.path.isfile(filename):
with open(filename) as article_template_file:
template = article_template_file.read()
article = template.format(**self.context)
with open(utils.get_file(f"{self.article_eday}.txt"), "w") as article_file:
article_file.write(article)
return article
return ""
def dump_checkpoint(self, checkpoint_id: int, **data_to_dump):
dump_path = pathlib.Path(f"aviator_dumps/d{self.article_eday}/")
dump_path.mkdir(parents=True, exist_ok=True)
with open(dump_path / f"{checkpoint_id:02d}_checkpoint.json", "w") as f:
utils.json_dump(data_to_dump, f)
def load_checkpoint(self, checkpoint_id: int):
load_path = pathlib.Path(f"aviator_dumps/d{self.article_eday}/")
with open(load_path / f"{checkpoint_id:02d}_checkpoint.json", "r") as f:
data = utils.json_load(f)
return data
@staticmethod
def upload_image_and_get_url(image: BytesIO, title: str = None) -> str:
if title is None:
title = "aviator_top"
title += ".png"
p = httpx.post(
"https://erep.lv/image/upload",
files=[("file", (title, image, "image/png"))],
data=dict(password="<censored>"),
)
if p.json().get("status"):
return p.json().get("url")
else:
raise ValueError("Unable to upload table image!")
def _create_aviator_png(self, title: str) -> BytesIO:
"""Convert data to png table.
:param title: String containing table name
:rtype: PNG image containing
"""
data = list(row for row in self.contestants.values() if row.air_kill_count)
formatted_data = ""
col_headers = [
("name", "Spēlētājs", 10),
("air_kill_count", "Kili", 350),
("air_health", "Enerģija", 440),
("done_7_dos", "7 DO", 470),
("factory_count_increased", "Fabrikas", 540),
("air_health_total", "Kopā", 650),
]
for col, col_value, x in col_headers:
anchor = (
"start" if col == "name" else "middle" if col in ["done_7_dos", "factory_count_increased"] else "end"
)
formatted_data += f"<text y='20' font-size='16px' text-anchor='{anchor}' font-family='Hack'>\n"
formatted_data += f"<tspan x='{x}' font-weight='bold'>{col_value}</tspan>\n"
bold = " font-weight='bold'" if col == "name" else ""
for row in sorted(data, key=attrgetter("air_kill_count", "air_health_total"), reverse=True):
value = getattr(row, col, None)
if col == "air_health_total":
if row.extra:
value = str(row.extra[0])
else:
value = row.air_health_total
if isinstance(value, bool):
value = "+" if value else ""
elif isinstance(value, int):
value = f"{value:,.0f}".replace(",", " ")
elif isinstance(value, str):
if len(value) > 29:
value = value[:26] + ".."
formatted_data += f"<tspan x='{x}' dy='1.5em'{bold}>{value}</tspan>\n"
formatted_data += "</text>\n"
height = 33 + len(data) * 24
row_highlight_count = (len(data) + 1) // 2
row_highlights = ""
for i in range(row_highlight_count):
row_highlights += f"<rect x='2' y='{48 * (i + 1) - 22}' width='656' height='24' style='fill:lightgray' />"
svg_template = (
f"<svg width='660' height='{height}' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>" # noqa
f"<title>{title}</title><g id='anchors'><rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='white' />" # noqa
f"{row_highlights}"
f"<line x1='290' y1='26' x2='290' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='360' y1='26' x2='360' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='445' y1='26' x2='445' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='495' y1='26' x2='495' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='585' y1='26' x2='585' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='none' style='stroke:grey;stroke-width:1' />" # noqa
f"</g><g id='columnGroup'>{formatted_data}</g></svg>"
)
small_svg = "".join([row.strip() for row in svg_template.split("\n")])
icon_file = BytesIO()
svg2png(small_svg, write_to=icon_file, scale=2)
icon_file.seek(0)
return icon_file
def _setup_contestant(self, reply_data):
contestant = Contestant(int(reply_data.get("authorId")))
contestant.air_kill_count = self.air_top.get(contestant.id, 0)
contestant.ground_kill_count = self.ground_top.get(contestant.id, 0)
self.contestants.update({contestant.id: contestant})
profile = self.citizen.get_citizen_profile(contestant.id)
contestant.name = profile["citizen"]["name"]
contestant.air_rank = AIR_RANKS[profile["military"]["militaryData"]["aircraft"]["rankNumber"]]
contestant.ground_rank = GROUND_RANKS[profile["military"]["militaryData"]["ground"]["rankNumber"]]
contestant.military_unit = (
profile["military"]["militaryUnit"]["id"] if profile["military"]["militaryUnit"] else 0
)
if profile.get("isBanned"):
contestant.extra.append("BANNED")
elif not profile.get("location", {}).get("citizenshipCountry", {}).get("id") == 71:
contestant.extra.append("Nav pilsonis")
elif not profile["city"]["residenceCityId"]:
contestant.extra.append("Nav rezidences")
else:
contestant.residence = profile["city"]["residenceCityId"]
if contestant.id in forbidden_ids:
contestant.extra.append("Aizliegta pieteikšanās")
if contestant.extra:
return
residence_data = self.citizen.direct_get_citizen_residency_data(contestant.name, contestant.residence)
__residents = residence_data.get("widgets", {}).get("residents", {}).get("residents")
contestant.factory_count = 0
contestant.has_house = False
for resident in __residents:
if int(resident.get("citizenId")) == contestant.id:
contestant.factory_count = resident.get("numFactories", 0)
contestant.has_house = bool(resident.get("activeHouses"))
break
resp = httpx.post(
f"https://erep.lv/aviator/check/{contestant.id}",
data={"current_count": contestant.factory_count},
)
contestant.factory_count_increased = resp.json()["status"]
if not contestant.has_house:
contestant.extra.append("Nav māju")
if contestant.military_unit:
contestant.done_7_dos = self.citizen.get_weekly_daily_orders_done(
contestant.name, contestant.military_unit, self.provisional
)
self.citizen.write_log(
self.CONSOLE_TABLE_FORMAT.format(
contestant.id,
contestant.name,
contestant.ground_rank_s,
contestant.ground_kill_count,
contestant.air_rank_s,
contestant.air_kill_count,
contestant.done_7_dos,
contestant.factory_count_increased,
", ".join(contestant.extra),
)
)
def _create_ground_png(self, title: str) -> BytesIO:
"""Convert data to png table.
:param title: String containing table name
:rtype: PNG image containing
"""
data = list(row for row in self.contestants.values() if row.ground_kill_count)
formatted_data = ""
col_headers = [
("name", "Spēlētājs", 10),
("ground_kill_count", "Kili", 350),
("penalty", "", 450),
("ground_health", "Enerģija", 550),
("ground_weapons", "q7", 650),
]
for col, col_value, x in col_headers:
anchor = "start" if col == "name" else "end"
formatted_data += f" <text y='20' font-size='16px' text-anchor='{anchor}' font-family='Hack'>\n"
formatted_data += f" <tspan x='{x}' font-weight='bold'>{col_value}</tspan>\n"
bold = " font-weight='bold'" if col == "name" else ""
for row in sorted(data, key=attrgetter("ground_kill_count", "ground_health"), reverse=True):
value = getattr(row, col, None)
if col == "penalty":
if not row.ground_health and not row.ground_rank_penalty == 100 and row.extra:
value = row.extra[0]
elif row.ground_rank_penalty:
value = f"-{row.ground_rank_penalty}%"
else:
value = ""
if isinstance(value, bool):
value = "+" if value else ""
elif isinstance(value, int):
value = f"{value:,.0f}".replace(",", " ")
elif isinstance(value, str):
if len(value) > 29:
value = value[:26] + ".."
formatted_data += f" <tspan x='{x}' dy='1.5em'{bold}>{value}</tspan>\n"
formatted_data += " </text>\n"
height = 33 + len(data) * 24
row_highlight_count = (len(data) + 1) // 2
row_highlights = ""
for i in range(row_highlight_count):
row_highlights += f"<rect x='2' y='{48 * (i + 1) - 22}' width='656' height='24' style='fill:lightgray' />"
svg_template = (
f"<svg width='660' height='{height}' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>" # noqa
f"<title>{title}</title><g id='anchors'><rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='white' />" # noqa
f"{row_highlights}"
f"<line x1='290' y1='26' x2='290' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='360' y1='26' x2='360' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='460' y1='26' x2='460' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='560' y1='26' x2='560' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='none' style='stroke:grey;stroke-width:1' />" # noqa
f"</g><g id='columnGroup'>{formatted_data}</g></svg>"
)
small_svg = "".join([row.strip() for row in svg_template.split("\n")])
icon_file = BytesIO()
svg2png(small_svg, write_to=icon_file, scale=2)
icon_file.seek(0)
return icon_file
def _create_total_png(self, title: str) -> BytesIO:
"""Convert data to png table.
:param title: String containing table name
:rtype: PNG image containing
"""
data = list(self.contestants.values())
formatted_data = ""
col_headers = [
("name", "Spēlētājs", 10),
("total_kills", "Kili kopā", 400),
("total_health", "Enerģija kopā", 550),
("ground_weapons", "Tanki", 650),
]
for col, col_value, x in col_headers:
anchor = "start" if col == "name" else "end"
formatted_data += f"<text y='20' font-size='16px' text-anchor='{anchor}' font-family='Hack'>"
formatted_data += f"<tspan x='{x}' font-weight='bold'>{col_value}</tspan>"
bold = " font-weight='bold'" if col == "name" else ""
for row in sorted(data, key=attrgetter("total_kills", "total_health"), reverse=True):
value = getattr(row, col, None)
if col == "total_health" and not value:
if row.extra:
value = row.extra[0]
if isinstance(value, bool):
value = "+" if value else ""
elif isinstance(value, int):
value = f"{value:,.0f}".replace(",", " ")
elif isinstance(value, str):
if len(value) > 29:
value = value[:26] + ".."
formatted_data += f"<tspan x='{x}' dy='1.5em'{bold}>{value}</tspan>"
formatted_data += "</text>"
height = 33 + len(data) * 24
row_highlight_count = (len(data) + 1) // 2
row_highlights = ""
for i in range(row_highlight_count):
row_highlights += f"<rect x='2' y='{48 * (i + 1) - 22}' width='656' height='24' style='fill:lightgray' />"
svg_template = (
f"<svg width='660' height='{height}' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>" # noqa
f"<title>{title}</title><g id='anchors'><rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='white' />" # noqa
f"{row_highlights}"
f"<line x1='290' y1='26' x2='290' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='410' y1='26' x2='410' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<line x1='560' y1='26' x2='560' y2='{48 * row_highlight_count + 2}' style='stroke:rgb(230,230,230);stroke-width:0.5' />" # noqa
f"<rect x='1' y='1' width='658' height='{height - 2}' rx='10' fill='none' style='stroke:grey;stroke-width:1' />" # noqa
f"</g><g id='columnGroup'>{formatted_data}</g></svg>"
)
small_svg = "".join([row.strip() for row in svg_template.split("\n")])
icon_file = BytesIO()
svg2png(small_svg, write_to=icon_file, scale=2)
icon_file.seek(0)
return icon_file