Merge branch 'inventory_updates'
* inventory_updates: Python 3.8, isort, requirement update Representation of Citizen class Created method for current products on sale. Updated inventory to also include products on sale # Conflicts: # erepublik/citizen.py
This commit is contained in:
commit
9c64bfac0f
@ -19,3 +19,8 @@ insert_final_newline = false
|
|||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
line_length=120
|
||||||
|
multi_line_output=0
|
||||||
|
balanced_wrapping=True
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -85,7 +85,7 @@ celerybeat-schedule
|
|||||||
|
|
||||||
# virtualenv
|
# virtualenv
|
||||||
.venv
|
.venv
|
||||||
venv/
|
venv*/
|
||||||
ENV/
|
ENV/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from itertools import product
|
from itertools import product
|
||||||
from json import dumps, loads
|
from json import dumps, loads
|
||||||
@ -508,7 +509,21 @@ class Citizen(CitizenAPI):
|
|||||||
if kind not in final_items:
|
if kind not in final_items:
|
||||||
final_items[kind] = {}
|
final_items[kind] = {}
|
||||||
|
|
||||||
icon = item['icon'] if item['icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png"
|
if item['icon']:
|
||||||
|
icon = item['icon']
|
||||||
|
else:
|
||||||
|
if item['type'] == 'damageBoosters':
|
||||||
|
icon = "/images/modules/pvp/damage_boosters/damage_booster.png"
|
||||||
|
elif item['type'] == 'aircraftDamageBoosters':
|
||||||
|
icon = "/images/modules/pvp/damage_boosters/air_damage_booster.png"
|
||||||
|
elif item['type'] == 'prestigePointsBoosters':
|
||||||
|
icon = "/images/modules/pvp/prestige_points_boosters/prestige_booster.png"
|
||||||
|
elif item['type'] == 'speedBoosters':
|
||||||
|
icon = "/images/modules/pvp/speed_boosters/speed_booster.png"
|
||||||
|
elif item['type'] == 'catchupBoosters':
|
||||||
|
icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png"
|
||||||
|
else:
|
||||||
|
icon = "//www.erepublik.net/images/modules/manager/tab_storage.png"
|
||||||
data = dict(kind=kind, quality=item.get('quality', 0), amount=item.get('amount', 0),
|
data = dict(kind=kind, quality=item.get('quality', 0), amount=item.get('amount', 0),
|
||||||
durability=item.get('duration', 0), icon=icon, name=name)
|
durability=item.get('duration', 0), icon=icon, name=name)
|
||||||
if item.get('type') in ('damageBoosters', "aircraftDamageBoosters"):
|
if item.get('type') in ('damageBoosters', "aircraftDamageBoosters"):
|
||||||
@ -536,9 +551,22 @@ class Citizen(CitizenAPI):
|
|||||||
icon=icon)
|
icon=icon)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
offers = {}
|
||||||
|
for offer in self._get_economy_my_market_offers().json():
|
||||||
|
kind = self.get_industry_name(offer['industryId'])
|
||||||
|
data = dict(quality=offer.get('quality', 0), amount=offer.get('amount', 0), icon=offer.get('icon'),
|
||||||
|
kind=kind, name=kind)
|
||||||
|
data = {data['quality']: data}
|
||||||
|
|
||||||
|
if kind not in offers:
|
||||||
|
offers[kind] = {}
|
||||||
|
|
||||||
|
offers[kind].update(data)
|
||||||
|
|
||||||
self.inventory.update({"used": j.get("inventoryStatus").get("usedStorage"),
|
self.inventory.update({"used": j.get("inventoryStatus").get("usedStorage"),
|
||||||
"total": j.get("inventoryStatus").get("totalStorage")})
|
"total": j.get("inventoryStatus").get("totalStorage")})
|
||||||
inventory = dict(items=dict(active=active_items, final=final_items, raw=raw_materials), status=self.inventory)
|
inventory = dict(items=dict(active=active_items, final=final_items,
|
||||||
|
raw=raw_materials, offers=offers), status=self.inventory)
|
||||||
self.food["total"] = sum([self.food[q] * FOOD_ENERGY[q] for q in FOOD_ENERGY])
|
self.food["total"] = sum([self.food[q] * FOOD_ENERGY[q] for q in FOOD_ENERGY])
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
@ -1673,6 +1701,14 @@ class Citizen(CitizenAPI):
|
|||||||
"\n".join(["{}: {}".format(k, v) for k, v in kinds.items()]), kind
|
"\n".join(["{}: {}".format(k, v) for k, v in kinds.items()]), kind
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def get_my_market_offers(self) -> List[Dict[str, Union[int, float, str]]]:
|
||||||
|
ret = []
|
||||||
|
for offer in self._get_economy_my_market_offers().json():
|
||||||
|
line = offer.copy()
|
||||||
|
line.pop('icon', None)
|
||||||
|
ret.append(line)
|
||||||
|
return ret
|
||||||
|
|
||||||
def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> Response:
|
def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> Response:
|
||||||
if industry not in self.available_industries.values():
|
if industry not in self.available_industries.values():
|
||||||
self.write_log(f"Trying to sell unsupported industry {industry}")
|
self.write_log(f"Trying to sell unsupported industry {industry}")
|
||||||
@ -1750,7 +1786,8 @@ class Citizen(CitizenAPI):
|
|||||||
@property
|
@property
|
||||||
def factories(self) -> Dict[int, str]:
|
def factories(self) -> Dict[int, str]:
|
||||||
"""Returns factory industries as dict(id: name)
|
"""Returns factory industries as dict(id: name)
|
||||||
:return: dict
|
:return: Factory id:name dict
|
||||||
|
":rtype: Dict[int, str]
|
||||||
"""
|
"""
|
||||||
return {1: "Food", 2: "Weapons", 4: "House", 23: "Aircraft",
|
return {1: "Food", 2: "Weapons", 4: "House", 23: "Aircraft",
|
||||||
7: "FRM q1", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
|
7: "FRM q1", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
|
||||||
@ -1759,13 +1796,25 @@ class Citizen(CitizenAPI):
|
|||||||
24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5", }
|
24: "ARM q1", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5", }
|
||||||
|
|
||||||
def get_industry_id(self, industry_name: str) -> int:
|
def get_industry_id(self, industry_name: str) -> int:
|
||||||
"""
|
"""Returns industry id
|
||||||
Returns industry id
|
|
||||||
:type industry_name: str
|
:type industry_name: str
|
||||||
:return: int
|
:return: int
|
||||||
"""
|
"""
|
||||||
return self.available_industries.get(industry_name, 0)
|
return self.available_industries.get(industry_name, 0)
|
||||||
|
|
||||||
|
def get_industry_name(self, industry_id: int) -> str:
|
||||||
|
"""Returns industry name from industry ID
|
||||||
|
|
||||||
|
:type industry_id: int
|
||||||
|
:return: industry name
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
for iname, iid in self.available_industries.items():
|
||||||
|
if iid == industry_id:
|
||||||
|
return iname
|
||||||
|
return ""
|
||||||
|
|
||||||
def buy_tg_contract(self) -> Response:
|
def buy_tg_contract(self) -> Response:
|
||||||
ret = self._post_main_buy_gold_items('gold', "TrainingContract2", 1)
|
ret = self._post_main_buy_gold_items('gold', "TrainingContract2", 1)
|
||||||
self.reporter.report_action("BUY_TG_CONTRACT", ret.json())
|
self.reporter.report_action("BUY_TG_CONTRACT", ret.json())
|
||||||
|
@ -8,7 +8,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union, Any, List, NoReturn, Mapping, Optional
|
from typing import Any, List, Mapping, NoReturn, Optional, Union
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
@ -19,6 +19,10 @@ __all__ = ["FOOD_ENERGY", "COMMIT_ID", "COUNTRIES", "erep_tz", 'COUNTRY_LINK',
|
|||||||
"write_silent_log", "write_interactive_log", "get_file", "write_file",
|
"write_silent_log", "write_interactive_log", "get_file", "write_file",
|
||||||
"send_email", "normalize_html_json", "process_error", "process_warning", 'report_promo', 'calculate_hit']
|
"send_email", "normalize_html_json", "process_error", "process_warning", 'report_promo', 'calculate_hit']
|
||||||
|
|
||||||
|
if not sys.version_info >= (3, 7):
|
||||||
|
raise AssertionError('This script requires Python version 3.7 and higher\n'
|
||||||
|
'But Your version is v{}.{}.{}'.format(*sys.version_info))
|
||||||
|
|
||||||
FOOD_ENERGY = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20)
|
FOOD_ENERGY = dict(q1=2, q2=4, q3=6, q4=8, q5=10, q6=12, q7=20)
|
||||||
COMMIT_ID = "7b92e19"
|
COMMIT_ID = "7b92e19"
|
||||||
|
|
||||||
@ -113,6 +117,15 @@ def localize_dt(dt: Union[datetime.date, datetime.datetime]) -> datetime.datetim
|
|||||||
|
|
||||||
|
|
||||||
def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime:
|
def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime:
|
||||||
|
"""Normalize timezone aware datetime object after timedelta to correct jumps over DST switches
|
||||||
|
|
||||||
|
:param dt: Timezone aware datetime object
|
||||||
|
:type dt: datetime.datetime
|
||||||
|
:param td: timedelta object
|
||||||
|
:type td: datetime.timedelta
|
||||||
|
:return: datetime object with correct timezone when jumped over DST
|
||||||
|
:rtype: datetime.datetime
|
||||||
|
"""
|
||||||
return erep_tz.normalize(dt + td)
|
return erep_tz.normalize(dt + td)
|
||||||
|
|
||||||
|
|
||||||
|
96
examples/battle_launcher.py
Normal file
96
examples/battle_launcher.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import threading
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from erepublik import Citizen, utils
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
'email': 'player@email.com',
|
||||||
|
'password': 'Pa$5w0rd!',
|
||||||
|
'interactive': True,
|
||||||
|
'fight': True,
|
||||||
|
'debug': True,
|
||||||
|
'start_battles': {
|
||||||
|
121672: {"auto_attack": False, "regions": [661]},
|
||||||
|
125530: {"auto_attack": False, "regions": [259]},
|
||||||
|
125226: {"auto_attack": True, "regions": [549]},
|
||||||
|
124559: {"auto_attack": True, "regions": [176]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _battle_launcher(player: Citizen):
|
||||||
|
"""Launch battles. Check every 5th minute (0,5,10...45,50,55) if any battle could be started on specified regions
|
||||||
|
and after launching wait for 90 minutes before starting next attack so that all battles aren't launched at the same
|
||||||
|
time. If player is allowed to fight, do 100 hits on the first round in players division.
|
||||||
|
|
||||||
|
:param player: Logged in Citizen instance
|
||||||
|
":type player: Citizen
|
||||||
|
"""
|
||||||
|
global CONFIG
|
||||||
|
finished_war_ids = {*[]}
|
||||||
|
war_data = CONFIG.get('start_battles', {})
|
||||||
|
war_ids = {int(war_id) for war_id in war_data.keys()}
|
||||||
|
next_attack_time = player.now
|
||||||
|
next_attack_time = next_attack_time.replace(minute=next_attack_time.minute // 5 * 5, second=0)
|
||||||
|
while not player.stop_threads.is_set():
|
||||||
|
try:
|
||||||
|
attacked = False
|
||||||
|
player.update_war_info()
|
||||||
|
running_wars = {b.war_id for b in player.all_battles.values()}
|
||||||
|
for war_id in war_ids - finished_war_ids - running_wars:
|
||||||
|
war = war_data[str(war_id)]
|
||||||
|
war_regions = set(war.get('regions'))
|
||||||
|
auto_attack = war.get('auto_attack')
|
||||||
|
|
||||||
|
status = player.get_war_status(war_id)
|
||||||
|
if status.get('ended', False):
|
||||||
|
CONFIG['start_battles'].pop(str(war_id), None)
|
||||||
|
finished_war_ids.add(war_id)
|
||||||
|
continue
|
||||||
|
elif not status.get('can_attack'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if auto_attack or (player.now.hour > 20 or player.now.hour < 2):
|
||||||
|
for reg in war_regions:
|
||||||
|
if attacked:
|
||||||
|
break
|
||||||
|
if reg in status.get('regions', {}).keys():
|
||||||
|
player.launch_attack(war_id, reg, status.get('regions', {}).get(reg))
|
||||||
|
attacked = True
|
||||||
|
hits = 100
|
||||||
|
if player.energy.food_fights >= hits and player.config.fight:
|
||||||
|
for _ in range(120):
|
||||||
|
player.update_war_info()
|
||||||
|
battle_id = player.get_war_status(war_id).get("battle_id")
|
||||||
|
if battle_id is not None and battle_id in player.all_battles:
|
||||||
|
player.fight(battle_id, player.details.citizenship, hits)
|
||||||
|
break
|
||||||
|
player.sleep(1)
|
||||||
|
if attacked:
|
||||||
|
break
|
||||||
|
if attacked:
|
||||||
|
break
|
||||||
|
war_ids -= finished_war_ids
|
||||||
|
if attacked:
|
||||||
|
next_attack_time = utils.good_timedelta(next_attack_time, timedelta(hours=1, minutes=30))
|
||||||
|
else:
|
||||||
|
next_attack_time = utils.good_timedelta(next_attack_time, timedelta(minutes=5))
|
||||||
|
player.stop_threads.wait(utils.get_sleep_seconds(next_attack_time))
|
||||||
|
except:
|
||||||
|
player.report_error("Task error: start_battles")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False)
|
||||||
|
player.config.interactive = CONFIG['interactive']
|
||||||
|
player.config.fight = CONFIG['fight']
|
||||||
|
player.set_debug(CONFIG.get('debug', False))
|
||||||
|
player.login()
|
||||||
|
if CONFIG.get('start_battles'):
|
||||||
|
name = "{}-start_battles-{}".format(player.name, threading.active_count() - 1)
|
||||||
|
state_thread = threading.Thread(target=_battle_launcher, args=(player,), name=name)
|
||||||
|
state_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
102
examples/eat_work_train.py
Normal file
102
examples/eat_work_train.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from erepublik import Citizen, utils
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
'email': 'player@email.com',
|
||||||
|
'password': 'Pa$5w0rd!',
|
||||||
|
'interactive': True,
|
||||||
|
'debug': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False)
|
||||||
|
player.config.interactive = CONFIG['interactive']
|
||||||
|
player.config.fight = CONFIG['fight']
|
||||||
|
player.set_debug(CONFIG.get('debug', False))
|
||||||
|
player.login()
|
||||||
|
now = player.now.replace(second=0, microsecond=0)
|
||||||
|
dt_max = now.replace(year=9999)
|
||||||
|
tasks = {
|
||||||
|
'eat': now,
|
||||||
|
}
|
||||||
|
if player.config.work:
|
||||||
|
tasks.update({'work': now})
|
||||||
|
if player.config.train:
|
||||||
|
tasks.update({'train': now})
|
||||||
|
if player.config.ot:
|
||||||
|
tasks.update({'ot': now})
|
||||||
|
if player.config.wam:
|
||||||
|
tasks.update({'wam': now.replace(hour=14, minute=0)})
|
||||||
|
while True:
|
||||||
|
player.update_all()
|
||||||
|
if tasks.get('work', dt_max) <= now:
|
||||||
|
player.write_log("Doing task: work")
|
||||||
|
player.update_citizen_info()
|
||||||
|
player.work()
|
||||||
|
if player.config.ot:
|
||||||
|
tasks['ot'] = now
|
||||||
|
player.collect_daily_task()
|
||||||
|
next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1))
|
||||||
|
tasks.update({'work': next_time})
|
||||||
|
|
||||||
|
if tasks.get('train', dt_max) <= now:
|
||||||
|
player.write_log("Doing task: train")
|
||||||
|
player.update_citizen_info()
|
||||||
|
player.train()
|
||||||
|
player.collect_daily_task()
|
||||||
|
next_time = utils.good_timedelta(now.replace(hour=0, minute=0, second=0), timedelta(days=1))
|
||||||
|
tasks.update({'train': next_time})
|
||||||
|
|
||||||
|
if tasks.get('wam', dt_max) <= now:
|
||||||
|
player.write_log("Doing task: Work as manager")
|
||||||
|
success = player.work_wam()
|
||||||
|
player.eat()
|
||||||
|
if success:
|
||||||
|
next_time = utils.good_timedelta(now.replace(hour=14, minute=0, second=0, microsecond=0),
|
||||||
|
timedelta(days=1))
|
||||||
|
else:
|
||||||
|
next_time = utils.good_timedelta(now.replace(second=0, microsecond=0), timedelta(minutes=30))
|
||||||
|
|
||||||
|
tasks.update({'wam': next_time})
|
||||||
|
|
||||||
|
if tasks.get('eat', dt_max) <= now:
|
||||||
|
player.write_log("Doing task: eat")
|
||||||
|
player.eat()
|
||||||
|
|
||||||
|
if player.energy.food_fights > player.energy.limit // 10:
|
||||||
|
next_minutes = 12
|
||||||
|
else:
|
||||||
|
next_minutes = (player.energy.limit - 5 * player.energy.interval) // player.energy.interval * 6
|
||||||
|
|
||||||
|
next_time = player.energy.reference_time + timedelta(minutes=next_minutes)
|
||||||
|
tasks.update({'eat': next_time})
|
||||||
|
|
||||||
|
if tasks.get('ot', dt_max) <= now:
|
||||||
|
player.write_log("Doing task: ot")
|
||||||
|
if now > player.my_companies.next_ot_time:
|
||||||
|
player.work_ot()
|
||||||
|
next_time = now + timedelta(minutes=60)
|
||||||
|
else:
|
||||||
|
next_time = player.my_companies.next_ot_time
|
||||||
|
tasks.update({'ot': next_time})
|
||||||
|
|
||||||
|
closest_next_time = dt_max
|
||||||
|
next_tasks = []
|
||||||
|
for task, next_time in sorted(tasks.items(), key=lambda s: s[1]):
|
||||||
|
next_tasks.append("{}: {}".format(next_time.strftime('%F %T'), task))
|
||||||
|
if next_time < closest_next_time:
|
||||||
|
closest_next_time = next_time
|
||||||
|
sleep_seconds = int(utils.get_sleep_seconds(closest_next_time))
|
||||||
|
if sleep_seconds <= 0:
|
||||||
|
player.write_log(f"Loop detected! Offending task: '{next_tasks[0]}'")
|
||||||
|
player.write_log("My next Tasks and there time:\n" + "\n".join(sorted(next_tasks)))
|
||||||
|
player.write_log("Sleeping until (eRep): {} (sleeping for {}s)".format(
|
||||||
|
closest_next_time.strftime("%F %T"), sleep_seconds))
|
||||||
|
seconds_to_sleep = sleep_seconds if sleep_seconds > 0 else 0
|
||||||
|
player.sleep(seconds_to_sleep)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
16
requirements.txt
Normal file
16
requirements.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
bumpversion==0.5.3
|
||||||
|
coverage==5.0.2
|
||||||
|
edx-sphinx-theme==1.5.0
|
||||||
|
flake8==3.7.9
|
||||||
|
ipython==7.11.1
|
||||||
|
isort==4.3.21
|
||||||
|
pip==19.3.1
|
||||||
|
PyInstaller==3.5
|
||||||
|
pytz==2019.3
|
||||||
|
requests==2.22.0
|
||||||
|
setuptools==44.0.0
|
||||||
|
Sphinx==2.3.1
|
||||||
|
tox==3.14.3
|
||||||
|
twine==3.1.1
|
||||||
|
watchdog==0.9.0
|
||||||
|
wheel==0.33.6
|
@ -1,14 +0,0 @@
|
|||||||
pip==19.1.1
|
|
||||||
bumpversion==0.5.3
|
|
||||||
wheel==0.33.4
|
|
||||||
watchdog==0.9.0
|
|
||||||
flake8==3.7.8
|
|
||||||
tox==3.13.2
|
|
||||||
coverage==4.5.3
|
|
||||||
Sphinx==2.2.0
|
|
||||||
twine==2.0.0
|
|
||||||
ipython
|
|
||||||
PyInstaller
|
|
||||||
pytz==2019.1
|
|
||||||
requests==2.22.0
|
|
||||||
edx-sphinx-theme
|
|
3
setup.py
3
setup.py
@ -11,7 +11,7 @@ with open('README.rst') as readme_file:
|
|||||||
with open('HISTORY.rst') as history_file:
|
with open('HISTORY.rst') as history_file:
|
||||||
history = history_file.read()
|
history = history_file.read()
|
||||||
|
|
||||||
requirements = ['pytz>=2019.2', 'requests>=2.22']
|
requirements = ['pytz==2019.3', 'requests==2.22.0']
|
||||||
|
|
||||||
setup_requirements = []
|
setup_requirements = []
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ setup(
|
|||||||
'Natural Language :: English',
|
'Natural Language :: English',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
],
|
],
|
||||||
description="Python package for automated eRepublik playing",
|
description="Python package for automated eRepublik playing",
|
||||||
entry_points={},
|
entry_points={},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user