Updated to only report epics
Requirement update Requirement update
This commit is contained in:
parent
bf8899b8fb
commit
9f23253232
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ venv
|
|||||||
*.db
|
*.db
|
||||||
.env
|
.env
|
||||||
__pycache__
|
__pycache__
|
||||||
|
.idea
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
CMD python discord_bot.py
|
190
db.py
190
db.py
@ -1,4 +1,4 @@
|
|||||||
from typing import List, Union, Dict, Optional
|
from typing import Union, Dict, Optional
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.db import NotFoundError
|
from sqlite_utils.db import NotFoundError
|
||||||
@ -14,60 +14,20 @@ class DiscordDB:
|
|||||||
self._db = Database(memory=True)
|
self._db = Database(memory=True)
|
||||||
else:
|
else:
|
||||||
self._db = Database(self._name)
|
self._db = Database(self._name)
|
||||||
migrate_db = False
|
|
||||||
if "member" not in self._db.table_names():
|
if "member" not in self._db.table_names():
|
||||||
self._db.create_table("member", {"id": int, "name": str}, pk="id", not_null={"id", "name"})
|
self._db.create_table("member", {"id": int, "name": str}, pk="id", not_null={"id", "name"})
|
||||||
|
|
||||||
if "player" not in self._db.table_names():
|
if "player" not in self._db.table_names():
|
||||||
self._db.create_table("player", {"id": int, "name": str}, pk="id", not_null={"id", "name"})
|
self._db.create_table("player", {"id": int, "name": str}, pk="id", not_null={"id", "name"})
|
||||||
|
|
||||||
if "hunted" not in self._db.table_names() or migrate_db:
|
if "epic" not in self._db.table_names():
|
||||||
if migrate_db:
|
self._db.create_table("epic", {"id": int, }, pk="id", not_null={"id"})
|
||||||
self._db.table('hunted').drop()
|
|
||||||
self._db.create_table("hunted", {"id": int, "member_id": int, "player_id": int, 'channel_id': int},
|
|
||||||
pk="id", not_null={"id", "member_id", "player_id", "channel_id"})
|
|
||||||
self._db['hunted'].create_index(["member_id", "player_id"], unique=True)
|
|
||||||
|
|
||||||
if "medals" in self._db.table_names():
|
|
||||||
self._db.table('medals').drop()
|
|
||||||
self._db.create_table("medals", dict(id=int, player_id=int, battle_id=int, division_id=int, side_id=int,
|
|
||||||
damage=int), pk="id", defaults={"damage": 0},
|
|
||||||
not_null={"id", "player_id", "battle_id", "division_id", "side_id", "damage"})
|
|
||||||
self._db['medals'].create_index(["player_id", "battle_id", "division_id", "side_id"], unique=True)
|
|
||||||
|
|
||||||
if "hunted_players" not in self._db.view_names():
|
|
||||||
self._db.create_view("hunted_players", "select distinct player_id from hunted")
|
|
||||||
|
|
||||||
if "protected" not in self._db.table_names() or migrate_db:
|
|
||||||
self._db.create_table("protected", {"id": int, "member_id": int, "player_id": int, 'channel_id': int},
|
|
||||||
pk="id", not_null={"id", "member_id", "player_id", "channel_id"})
|
|
||||||
self._db['protected'].create_index(["member_id", "player_id"], unique=True)
|
|
||||||
|
|
||||||
if "protected_medals" not in self._db.table_names():
|
|
||||||
self._db.create_table("protected_medals",
|
|
||||||
dict(id=int, player_id=int, division_id=int, side_id=int), pk="id",
|
|
||||||
not_null={"id", "player_id", "division_id", "side_id"})
|
|
||||||
self._db['protected_medals'].create_index(["player_id", "division_id", "side_id"], unique=True)
|
|
||||||
|
|
||||||
if "protected_players" not in self._db.view_names():
|
|
||||||
self._db.create_view("protected_players", "select distinct player_id from protected")
|
|
||||||
|
|
||||||
self._db.add_foreign_keys([("hunted", "member_id", "member", "id"),
|
|
||||||
("hunted", "player_id", "player", "id"),
|
|
||||||
("protected", "member_id", "member", "id"),
|
|
||||||
("protected", "player_id", "player", "id"),
|
|
||||||
("medals", "player_id", "player", "id"),
|
|
||||||
("protected_medals", "player_id", "player", "id")])
|
|
||||||
self._db.vacuum()
|
self._db.vacuum()
|
||||||
|
|
||||||
self.member = self._db.table("member")
|
self.member = self._db.table("member")
|
||||||
self.player = self._db.table("player")
|
self.player = self._db.table("player")
|
||||||
self.hunted = self._db.table("hunted")
|
self.epic = self._db.table("epic")
|
||||||
self.medals = self._db.table("medals")
|
|
||||||
self.hunted_players = self._db.table("hunted_players")
|
|
||||||
self.protected = self._db.table("protected")
|
|
||||||
self.protected_medals = self._db.table("protected_medals")
|
|
||||||
self.protected_players = self._db.table("protected_players")
|
|
||||||
|
|
||||||
# Player methods
|
# Player methods
|
||||||
|
|
||||||
@ -150,142 +110,26 @@ class DiscordDB:
|
|||||||
self.member.update(member["id"], {"name": name})
|
self.member.update(member["id"], {"name": name})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_medal(self, pid: int, bid: int, div: int, side: int, dmg: int) -> bool:
|
# Epic Methods
|
||||||
"""Check if players (pid) damage (dmg) in battle (bid) for side in division (div) has been registered
|
|
||||||
|
|
||||||
:param pid: Player ID
|
def get_epic(self, division_id: int) -> Optional[Dict[str, Union[int, str]]]:
|
||||||
:type pid: int
|
"""Get Epic division
|
||||||
:param bid: Battle ID
|
|
||||||
:type bid: int
|
|
||||||
:param div: Division
|
|
||||||
:type div: int
|
|
||||||
:param side: Side ID
|
|
||||||
:type side: int
|
|
||||||
:param dmg: Damage amount
|
|
||||||
:type dmg: int
|
|
||||||
:return: If medal has been registered
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
medals = self.medals
|
|
||||||
record_pk = medals.lookup(dict(player_id=pid, battle_id=bid, division_id=div, side_id=side))
|
|
||||||
return medals.get(record_pk)["damage"] == dmg
|
|
||||||
|
|
||||||
def add_reported_medal(self, pid: int, bid: int, div: int, side: int, dmg: int):
|
:param division_id: int Division ID
|
||||||
medals = self.medals
|
:return: division id
|
||||||
pk = medals.lookup(dict(player_id=pid, battle_id=bid, division_id=div, side_id=side))
|
|
||||||
medals.update(pk, {"damage": dmg})
|
|
||||||
return True
|
|
||||||
|
|
||||||
def delete_medals(self, bid: List[int]):
|
|
||||||
self.medals.delete_where("battle_id in (%s)" % "?" * len(bid), bid)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_hunt(self, pid: int, member_id: int) -> bool:
|
|
||||||
try:
|
|
||||||
next(self.hunted.rows_where("player_id=? and member_id=?", [pid, member_id]))
|
|
||||||
return True
|
|
||||||
except StopIteration:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_hunted_player(self, pid: int, member_id: int, channel_id: int) -> bool:
|
|
||||||
if self.check_hunt(pid, member_id):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.hunted.insert(dict(player_id=pid, member_id=member_id, channel_id=channel_id))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def remove_hunted_player(self, pid: int, member_id: int) -> bool:
|
|
||||||
if self.check_hunt(pid, member_id):
|
|
||||||
self.hunted.delete_where("player_id=? and member_id=?", (pid, member_id))
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_member_hunted_players(self, member_id: int) -> List[Dict[str, Union[int, str]]]:
|
|
||||||
return [self.get_player(r['player_id']) for r in self.hunted.rows_where("member_id=?",
|
|
||||||
(self.get_member(member_id)['id'], ))]
|
|
||||||
|
|
||||||
def get_hunted_player_ids(self) -> List[int]:
|
|
||||||
return [r["player_id"] for r in self.hunted_players.rows]
|
|
||||||
|
|
||||||
def get_members_to_notify(self, pid: int) -> List[Dict[str, Union[int, str]]]:
|
|
||||||
return [r for r in self.hunted.rows_where("player_id = ?", [pid])]
|
|
||||||
|
|
||||||
'''' MEDAL PROTECTION '''
|
|
||||||
|
|
||||||
def check_protected_medal(self, pid: int, div: int, side: int) -> Optional[bool]:
|
|
||||||
"""Check if player (pid) in battle (bid) for side in division (div) hasn't taken protected medal
|
|
||||||
|
|
||||||
:param pid: Player ID
|
|
||||||
:type pid: int
|
|
||||||
:param bid: Battle ID
|
|
||||||
:type bid: int
|
|
||||||
:param div: Division
|
|
||||||
:type div: int
|
|
||||||
:param side: Side ID
|
|
||||||
:type side: int
|
|
||||||
:return: If medal has been registered
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
medal = next(self.protected_medals.rows_where("division_id=? and side_id=?", (div, side)))
|
return self.epic.get(division_id)
|
||||||
except StopIteration:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
return medal['player_id'] == pid
|
|
||||||
|
|
||||||
def add_protected_medal(self, pid: int, div: int, side: int):
|
def add_epic(self, division_id: int) -> bool:
|
||||||
"""Check if players (pid) medal in division (div) for side (sid) has been registered
|
"""Add Epic division.
|
||||||
|
|
||||||
:param pid: Player ID
|
:param division_id: int Epic division ID
|
||||||
:type pid: int
|
:return: bool Epic division added
|
||||||
:param div: Division
|
|
||||||
:type div: int
|
|
||||||
:param side: Side ID
|
|
||||||
:type side: int
|
|
||||||
"""
|
"""
|
||||||
self.protected_medals.lookup(dict(player_id=pid, division_id=div, side_id=side))
|
if not self.get_epic(division_id):
|
||||||
|
self.epic.insert({"id": division_id})
|
||||||
def get_protected_medal(self, div: int, side: int):
|
|
||||||
""" Get player_id (pid) in division (div) for side (sid)
|
|
||||||
|
|
||||||
:param div: Division
|
|
||||||
:type div: int
|
|
||||||
:param side: Side ID
|
|
||||||
:type side: int
|
|
||||||
"""
|
|
||||||
pk = self.protected_medals.lookup(dict(division_id=div, side_id=side))
|
|
||||||
return self.protected_medals.get(pk)
|
|
||||||
|
|
||||||
def delete_protected_medals(self, div_id: List[int]):
|
|
||||||
self.protected_medals.delete_where("division_id in (%s)" % "?" * len(div_id), div_id)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_protected(self, pid: int, member_id: int) -> bool:
|
|
||||||
try:
|
|
||||||
next(self.protected.rows_where("player_id=? and member_id=?", [pid, member_id]))
|
|
||||||
return True
|
|
||||||
except StopIteration:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_protected_player(self, pid: int, member_id: int, channel_id: int) -> bool:
|
|
||||||
if self.check_protected(pid, member_id):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.protected.insert(dict(player_id=pid, member_id=member_id, channel_id=channel_id))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def remove_protected_player(self, pid: int, member_id: int) -> bool:
|
|
||||||
if self.check_protected(pid, member_id):
|
|
||||||
self.protected.delete_where("player_id=? and member_id=?", (pid, member_id))
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_member_protected_players(self, member_id: int) -> List[Dict[str, Union[int, str]]]:
|
|
||||||
return [self.get_player(r['player_id']) for r in self.protected.rows_where("member_id=?", (member_id, ))]
|
|
||||||
|
|
||||||
def get_protected_player_ids(self) -> List[int]:
|
|
||||||
return [r["player_id"] for r in self.protected_players.rows]
|
|
||||||
|
|
||||||
def get_protected_members_to_notify(self, pid: int) -> List[Dict[str, Union[int, str]]]:
|
|
||||||
return [r for r in self.protected.rows_where("player_id = ?", [pid])]
|
|
||||||
|
311
discord_bot.py
311
discord_bot.py
@ -4,13 +4,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import pytz
|
|
||||||
import requests
|
import requests
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from sqlite_utils.db import NotFoundError
|
|
||||||
|
|
||||||
from db import DiscordDB
|
from db import DiscordDB
|
||||||
|
|
||||||
@ -28,28 +27,16 @@ fh.setLevel(logging.DEBUG)
|
|||||||
logger.addHandler(fh)
|
logger.addHandler(fh)
|
||||||
keep_fds = [fh.stream.fileno()]
|
keep_fds = [fh.stream.fileno()]
|
||||||
|
|
||||||
pidfile = f"pid"
|
pidfile = "pid"
|
||||||
with open(pidfile, 'w') as f:
|
with open(pidfile, 'w') as f:
|
||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
|
|
||||||
|
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||||
|
DEFAULT_CHANNEL_ID = os.getenv("DEFAULT_CHANNEL_ID", 603527159109124096)
|
||||||
|
ADMIN_ID = os.getenv("DEFAULT_CHANNEL_ID", 220849530730577920)
|
||||||
DB_NAME = os.getenv('DB_NAME', 'discord.db')
|
DB_NAME = os.getenv('DB_NAME', 'discord.db')
|
||||||
DB = DiscordDB(DB_NAME)
|
DB = DiscordDB(DB_NAME)
|
||||||
|
|
||||||
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'}
|
|
||||||
|
|
||||||
FLAGS = {1: 'flag_ro', 9: 'flag_br', 10: 'flag_it', 11: 'flag_fr', 12: 'flag_de', 13: 'flag_hu', 14: 'flag_cn',
|
FLAGS = {1: 'flag_ro', 9: 'flag_br', 10: 'flag_it', 11: 'flag_fr', 12: 'flag_de', 13: 'flag_hu', 14: 'flag_cn',
|
||||||
15: 'flag_es', 23: 'flag_ca', 24: 'flag_us', 26: 'flag_mx', 27: 'flag_ar', 28: 'flag_ve', 29: 'flag_gb',
|
15: 'flag_es', 23: 'flag_ca', 24: 'flag_us', 26: 'flag_mx', 27: 'flag_ar', 28: 'flag_ve', 29: 'flag_gb',
|
||||||
30: 'flag_ch', 31: 'flag_nl', 32: 'flag_be', 33: 'flag_at', 34: 'flag_cz', 35: 'flag_pl', 36: 'flag_sk',
|
30: 'flag_ch', 31: 'flag_nl', 32: 'flag_be', 33: 'flag_at', 34: 'flag_cz', 35: 'flag_pl', 36: 'flag_sk',
|
||||||
@ -62,6 +49,7 @@ FLAGS = {1: 'flag_ro', 9: 'flag_br', 10: 'flag_it', 11: 'flag_fr', 12: 'flag_de'
|
|||||||
82: 'flag_cy', 83: 'flag_by', 84: 'flag_nz', 164: 'flag_sa', 165: 'flag_eg', 166: 'flag_ae', 167: 'flag_al',
|
82: 'flag_cy', 83: 'flag_by', 84: 'flag_nz', 164: 'flag_sa', 165: 'flag_eg', 166: 'flag_ae', 167: 'flag_al',
|
||||||
168: 'flag_ge', 169: 'flag_am', 170: 'flag_ng', 171: 'flag_cu'}
|
168: 'flag_ge', 169: 'flag_am', 170: 'flag_ng', 171: 'flag_cu'}
|
||||||
|
|
||||||
|
MENTION_MAPPING = {1: "@D1", 2: "@D2", 3: "@D3", 4: "@D4", 11: "@AIR"}
|
||||||
|
|
||||||
__last_battle_response = None
|
__last_battle_response = None
|
||||||
__last_battle_update_timestamp = 0
|
__last_battle_update_timestamp = 0
|
||||||
@ -71,67 +59,33 @@ def timestamp_to_datetime(timestamp: int) -> datetime.datetime:
|
|||||||
return datetime.datetime.fromtimestamp(timestamp)
|
return datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
def s_to_human(seconds: Union[int, float]) -> str:
|
||||||
|
seconds = int(seconds)
|
||||||
|
h = seconds // 3600
|
||||||
|
m = (seconds - (h * 3600)) // 60
|
||||||
|
s = seconds % 60
|
||||||
|
return f'{h:01d}:{m:02d}:{s:02d}'
|
||||||
|
|
||||||
|
|
||||||
def get_battle_page():
|
def get_battle_page():
|
||||||
global __last_battle_update_timestamp, __last_battle_response
|
global __last_battle_update_timestamp, __last_battle_response
|
||||||
if int(datetime.datetime.now().timestamp()) >= __last_battle_update_timestamp + 60:
|
if int(datetime.datetime.now().timestamp()) >= __last_battle_update_timestamp + 60:
|
||||||
dt = datetime.datetime.now()
|
dt = datetime.datetime.now()
|
||||||
r = requests.get('https://erep.lv/battles.json')
|
r = requests.get('https://erep.lv/battles.json')
|
||||||
os.makedirs(f"{dt:%F/%H}/", exist_ok=True)
|
|
||||||
with open(f"{dt:%F/%H}/{int(dt.timestamp())}.json", 'w') as f:
|
|
||||||
f.write(r.text)
|
|
||||||
try:
|
try:
|
||||||
__last_battle_response = r.json()
|
__last_battle_response = r.json()
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
logger.warning(f"Received non json response from erep.lv/battles.json! "
|
logger.warning("Received non json response from erep.lv/battles.json!")
|
||||||
f"Located at '{dt:%F/%H}/{int(dt.timestamp())}.json'")
|
|
||||||
return get_battle_page()
|
return get_battle_page()
|
||||||
__last_battle_update_timestamp = __last_battle_response.get('last_updated', int(dt.timestamp()))
|
__last_battle_update_timestamp = __last_battle_response.get('last_updated', int(dt.timestamp()))
|
||||||
return __last_battle_response
|
return __last_battle_response
|
||||||
|
|
||||||
|
|
||||||
def check_player(player_id: int) -> bool:
|
|
||||||
try:
|
|
||||||
player_id = int(player_id)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
if not DB.get_player(player_id):
|
|
||||||
try:
|
|
||||||
r = requests.get(f'https://www.erepublik.com/en/main/citizen-profile-json/{player_id}').json()
|
|
||||||
except JSONDecodeError:
|
|
||||||
return False
|
|
||||||
if r.get('error'):
|
|
||||||
return False
|
|
||||||
DB.add_player(player_id, r.get('citizen').get('name'))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_medals(division: int):
|
|
||||||
r = get_battle_page()
|
|
||||||
if r.get('battles'):
|
|
||||||
request_time = timestamp_to_datetime(r.get('last_updated'))
|
|
||||||
for battle_id, battle in r.get('battles').items():
|
|
||||||
start_time = timestamp_to_datetime(battle.get('start'))
|
|
||||||
if start_time - datetime.timedelta(seconds=30) < request_time:
|
|
||||||
for division_data in battle.get('div', {}).values():
|
|
||||||
if not division_data.get('end') and division_data.get('div') == division:
|
|
||||||
for side, stat in division_data['stats'].items():
|
|
||||||
data = dict(id=battle.get('id'), country_id=battle.get(side).get('id'),
|
|
||||||
time=request_time - start_time, dmg=0)
|
|
||||||
if stat:
|
|
||||||
data.update(dmg=division_data['stats'][side]['damage'])
|
|
||||||
yield data
|
|
||||||
else:
|
|
||||||
yield data
|
|
||||||
|
|
||||||
|
|
||||||
class MyClient(discord.Client):
|
class MyClient(discord.Client):
|
||||||
erep_tz = pytz.timezone('US/Pacific')
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# create the background task and run it in the background
|
# create the background task and run it in the background
|
||||||
self.bg_task = self.loop.create_task(self.report_medals())
|
self.bg_task = self.loop.create_task(self.report_epics())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
@ -142,77 +96,38 @@ class MyClient(discord.Client):
|
|||||||
print('------')
|
print('------')
|
||||||
|
|
||||||
async def on_error(self, event_method, *args, **kwargs):
|
async def on_error(self, event_method, *args, **kwargs):
|
||||||
logger.warning('Ignoring exception in {}'.format(event_method))
|
logger.warning(f'Ignoring exception in {event_method}')
|
||||||
|
|
||||||
|
async def report_epics(self):
|
||||||
async def report_medals(self):
|
|
||||||
await self.wait_until_ready()
|
await self.wait_until_ready()
|
||||||
while not self.is_closed():
|
while not self.is_closed():
|
||||||
try:
|
try:
|
||||||
r = get_battle_page()
|
r = get_battle_page()
|
||||||
hunted_ids = DB.get_hunted_player_ids()
|
|
||||||
protected_ids = DB.get_protected_player_ids()
|
|
||||||
if not isinstance(r.get('battles'), dict):
|
if not isinstance(r.get('battles'), dict):
|
||||||
sleep_seconds = r.get('last_updated') + 60 - self.timestamp
|
sleep_seconds = r.get('last_updated') + 60 - self.timestamp
|
||||||
await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0)
|
await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0)
|
||||||
continue
|
continue
|
||||||
for bid, battle in r.get('battles', {}).items():
|
for bid, battle in r.get('battles', {}).items():
|
||||||
for div in battle.get('div', {}).values():
|
for div in battle.get('div', {}).values():
|
||||||
if div['stats'] and not div['end']:
|
if div.get('epic') and not DB.get_epic(div.get('div')):
|
||||||
for side, side_data in div['stats'].items():
|
await self.get_channel(DEFAULT_CHANNEL_ID).send(
|
||||||
if side_data:
|
f"<@{MENTION_MAPPING[div['div']]}> Epic battle! Round time {s_to_human(self.timestamp - battle['start'])}\n"
|
||||||
pid = side_data['citizenId']
|
f"https://www.erepublik.com/en/military/battlefield/{battle['id']}")
|
||||||
if pid in hunted_ids:
|
DB.add_epic(div.get('div'))
|
||||||
hunted_medal_key = (pid, battle['id'], div['id'],
|
|
||||||
battle[side]['id'], side_data['damage'])
|
|
||||||
if not DB.check_medal(*hunted_medal_key):
|
|
||||||
for hunt_row in DB.get_members_to_notify(pid):
|
|
||||||
format_data = dict(author=hunt_row['member_id'],
|
|
||||||
player=DB.get_player(pid)['name'],
|
|
||||||
battle=bid,
|
|
||||||
region=battle.get('region').get('name'),
|
|
||||||
division=div['div'], dmg=side_data['damage'],
|
|
||||||
side=COUNTRIES[battle[side]['id']])
|
|
||||||
|
|
||||||
await self.get_channel(hunt_row['channel_id']).send(
|
|
||||||
"<@{author}> **{player}** detected in battle for {region} on {side} "
|
|
||||||
"side in d{division} with {dmg:,d}dmg\n"
|
|
||||||
"https://www.erepublik.com/en/military/battlefield/{battle}".format(
|
|
||||||
**format_data))
|
|
||||||
DB.add_reported_medal(*hunted_medal_key)
|
|
||||||
|
|
||||||
protected_medal_key = (pid, div['id'], battle[side]['id'])
|
|
||||||
protected_medal_status = DB.check_protected_medal(*protected_medal_key)
|
|
||||||
if protected_medal_status == False:
|
|
||||||
medal = DB.get_protected_medal(div['id'], battle[side]['id'])
|
|
||||||
for protected in DB.get_protected_members_to_notify(medal['player_id']):
|
|
||||||
await self.get_channel(protected['channel_id']).send(
|
|
||||||
"<@{author}> Medal for **{player}** in battle for {region} on"
|
|
||||||
" {side} side in d{division} has been taken!\n"
|
|
||||||
"https://www.erepublik.com/en/military/battlefield/{battle}".format(
|
|
||||||
author=protected['member_id'],
|
|
||||||
player=DB.get_player(medal['player_id'])['name'],
|
|
||||||
battle=bid, region=battle.get('region').get('name'),
|
|
||||||
division=div['div'], side=COUNTRIES[battle[side]['id']]
|
|
||||||
))
|
|
||||||
DB.delete_protected_medals([medal['division_id']])
|
|
||||||
else:
|
|
||||||
if protected_medal_status is None and pid in protected_ids:
|
|
||||||
DB.add_protected_medal(*protected_medal_key)
|
|
||||||
logger.info(f"Added medal for protection {protected_medal_key}")
|
|
||||||
sleep_seconds = r.get('last_updated') + 60 - self.timestamp
|
sleep_seconds = r.get('last_updated') + 60 - self.timestamp
|
||||||
await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0)
|
await asyncio.sleep(sleep_seconds if sleep_seconds > 0 else 0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.get_channel(603527159109124096).send("<@220849530730577920> Something bad has happened with"
|
await self.get_channel(DEFAULT_CHANNEL_ID).send(
|
||||||
" medal hunter!")
|
f"<@{ADMIN_ID}> Something bad has happened with epic notifier!")
|
||||||
logger.error("Discord bot's eRepublik medal watcher died!", exc_info=e)
|
logger.error("Discord bot's eRepublik epic watcher died!", exc_info=e)
|
||||||
try:
|
try:
|
||||||
with open(f"{self.timestamp}.json", 'w') as f:
|
with open(f"{self.timestamp}.json", 'w') as f:
|
||||||
f.write(r.text)
|
f.write(r.text)
|
||||||
except NameError:
|
except NameError:
|
||||||
logger.error("There was no Response object!", exc_info=e)
|
logger.error("There was no Response object!", exc_info=e)
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
await self.get_channel(603527159109124096).send("<@220849530730577920> I've stopped, please restart")
|
await self.get_channel(DEFAULT_CHANNEL_ID).send(f"<@{ADMIN_ID}> I've stopped, please restart")
|
||||||
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@ -228,172 +143,13 @@ async def on_ready():
|
|||||||
print('------')
|
print('------')
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Parādīt lētos d1 BH, kuru dmg ir zem 5m vai Tevis ievadīta vērtībā", help="Lētie d1 BH",
|
@bot.command()
|
||||||
category="Cheap medals")
|
async def kill(ctx):
|
||||||
async def bh1(ctx, max_damage: int = 5_000_000):
|
if ctx.author.id == ADMIN_ID:
|
||||||
await _send_medal_info(ctx, 1, max_damage)
|
await ctx.send(f"{ctx.author.mention} Bye!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
@bot.command(description="Parādīt lētos d2 BH, kuru dmg ir zem 10m vai Tevis ievadīta vērtībā", help="Lētie d2 BH",
|
|
||||||
category="Cheap medals")
|
|
||||||
async def bh2(ctx, max_damage: int = 10_000_000):
|
|
||||||
await _send_medal_info(ctx, 2, max_damage)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Parādīt lētos d3 BH, kuru dmg ir zem 15m vai Tevis ievadīta vērtībā", help="Lētie d3 BH",
|
|
||||||
category="Cheap medals")
|
|
||||||
async def bh3(ctx, max_damage: int = 15_000_000):
|
|
||||||
await _send_medal_info(ctx, 3, max_damage)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Parādīt lētos d4 BH, kuru dmg ir zem 50m vai Tevis ievadīta vērtībā", help="Lētie d4 BH",
|
|
||||||
category="Cheap medals")
|
|
||||||
async def bh4(ctx, max_damage: int = 50_000_000):
|
|
||||||
await _send_medal_info(ctx, 4, max_damage)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Parādīt lētos SH, kuru dmg ir zem 50k vai Tevis ievadīta vērtībā", help="Lētie SH",
|
|
||||||
category="Cheap medals")
|
|
||||||
async def sh(ctx, min_damage: int = 50_000):
|
|
||||||
await _send_medal_info(ctx, 11, min_damage)
|
|
||||||
|
|
||||||
|
|
||||||
@bh1.error
|
|
||||||
@bh2.error
|
|
||||||
@bh3.error
|
|
||||||
@bh4.error
|
|
||||||
@sh.error
|
|
||||||
async def damage_error(ctx, error):
|
|
||||||
if isinstance(error, commands.BadArgument):
|
|
||||||
await ctx.send('Damage vērtībai ir jābūt veselam skaitlim')
|
|
||||||
|
|
||||||
|
|
||||||
async def _send_medal_info(ctx, division: int, damage: int):
|
|
||||||
cheap_bhs = [] # Battle id, Side, damage, round time
|
|
||||||
for division_data in get_medals(division):
|
|
||||||
if division_data['dmg'] < damage:
|
|
||||||
division_data['flag'] = FLAGS[division_data['country_id']]
|
|
||||||
division_data['country'] = COUNTRIES[division_data['country_id']]
|
|
||||||
cheap_bhs.append(division_data)
|
|
||||||
|
|
||||||
if cheap_bhs:
|
|
||||||
cheap_bhs = sorted(cheap_bhs, key=lambda _: _['time'])
|
|
||||||
cheap_bhs.reverse()
|
|
||||||
msg = "\n".join(["{dmg:,d}dmg for :{flag}: {country}, {time} round time "
|
|
||||||
"https://www.erepublik.com/en/military/battlefield/{id}".format(**bh) for bh in cheap_bhs])
|
|
||||||
if len(msg) > 2000:
|
|
||||||
msg = "\n".join(msg[:2000].split('\n')[:-1])
|
|
||||||
await ctx.send(msg)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send("No medals under {:,d} damage found!".format(damage))
|
await ctx.send(f"Labs mēģinājums! Mani nogalināt var tikai <@{ADMIN_ID}>")
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Informēt par spēlētāja mēģinājumiem ņemt medaļas",
|
|
||||||
help="Piereģistrēties uz spēlētāja medaļu paziņošanu", category="Hunting")
|
|
||||||
async def hunt(ctx, player_id: int):
|
|
||||||
if not check_player(player_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} didn't find any player with `id: {player_id}`!")
|
|
||||||
else:
|
|
||||||
player_name = DB.get_player(player_id).get('name')
|
|
||||||
try:
|
|
||||||
local_member_id = DB.get_member(ctx.author.id).get('id')
|
|
||||||
except NotFoundError:
|
|
||||||
local_member_id = DB.add_member(ctx.author.id, ctx.author.name).get('id')
|
|
||||||
if ctx.channel.type.value == 1:
|
|
||||||
await ctx.send(f"{ctx.author.mention}, sorry, but currently I'm unable to notify You in DM channel!")
|
|
||||||
elif DB.add_hunted_player(player_id, local_member_id, ctx.channel.id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} You'll be notified for **{player_name}** medals in this channel")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You are already being notified for **{player_name}** medals")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Show list of hunted players",
|
|
||||||
help="Parādīt visus spēlētajus, kurus es medīju", category="Hunting")
|
|
||||||
async def my_hunt(ctx):
|
|
||||||
msgs = []
|
|
||||||
for hunted_player in DB.get_member_hunted_players(ctx.author.id):
|
|
||||||
msgs.append(f"`{hunted_player['id']}` - **{hunted_player['name']}**")
|
|
||||||
if msgs:
|
|
||||||
msg = "\n".join(msgs)
|
|
||||||
await ctx.send(f"{ctx.author.mention} You are hunting:\n{msg}")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You're not hunting anyone!")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Beigt informēt par spēlētāja mēģinājumiem ņemt medaļas",
|
|
||||||
help="Atreģistrēties no spēlētāja medaļu paziņošanas", category="Hunting")
|
|
||||||
async def remove_hunt(ctx, player_id: int):
|
|
||||||
if not check_player(player_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} didn't find any player with `id: {player_id}`!")
|
|
||||||
else:
|
|
||||||
player_name = DB.get_player(player_id).get('name')
|
|
||||||
try:
|
|
||||||
local_member_id = DB.get_member(ctx.author.id).get('id')
|
|
||||||
except NotFoundError:
|
|
||||||
local_member_id = DB.add_member(ctx.author.id, ctx.author.name).get('id')
|
|
||||||
if DB.remove_hunted_player(player_id, local_member_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} You won't be notified for **{player_name}** medals")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You were not hunting **{player_name}** medals")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Informēt par mēģinājiem nozagt medaļu",
|
|
||||||
help="Piereģistrēties uz medaļu sargāšanas paziņošanu", category="Protection")
|
|
||||||
async def protect(ctx, player_id: int):
|
|
||||||
if not check_player(player_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} didn't find any player with `id: {player_id}`!")
|
|
||||||
else:
|
|
||||||
player_name = DB.get_player(player_id).get('name')
|
|
||||||
try:
|
|
||||||
local_member_id = DB.get_member(ctx.author.id).get('id')
|
|
||||||
except NotFoundError:
|
|
||||||
local_member_id = DB.add_member(ctx.author.id, ctx.author.name).get('id')
|
|
||||||
if ctx.channel.type.value == 1:
|
|
||||||
await ctx.send(f"{ctx.author.mention}, sorry, but currently I'm unable to notify You in DM channel!")
|
|
||||||
elif DB.add_protected_player(player_id, local_member_id, ctx.channel.id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} You'll be notified in this channel when anyone passes "
|
|
||||||
f"**{player_name}**'s medals")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You are already being notified for **{player_name}** medals")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Show players whose medals I'm protecting",
|
|
||||||
help="Parādīt sargājamo spēlētāju sarakstu ", category="Protection")
|
|
||||||
async def my_protected(ctx):
|
|
||||||
msgs = []
|
|
||||||
for protected_player in DB.get_member_protected_players(ctx.author.id):
|
|
||||||
msgs.append(f"`{protected_player['id']}` - **{protected_player['name']}**")
|
|
||||||
if msgs:
|
|
||||||
msg = "\n".join(msgs)
|
|
||||||
await ctx.send(f"{ctx.author.mention} You are protecting:\n{msg}")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You're not protecting anyone!")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.command(description="Beigt informēt par spēlētāju mēģinājumiem noņemt medaļas",
|
|
||||||
help="Atreģistrēties no spēlētāja medaļu sargāšanas paziņošanas", category="Protection")
|
|
||||||
async def remove_protection(ctx, player_id: int):
|
|
||||||
if not check_player(player_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} didn't find any player with `id: {player_id}`!")
|
|
||||||
else:
|
|
||||||
player_name = DB.get_player(player_id).get('name')
|
|
||||||
try:
|
|
||||||
local_member_id = DB.get_member(ctx.author.id).get('id')
|
|
||||||
except NotFoundError:
|
|
||||||
local_member_id = DB.add_member(ctx.author.id, ctx.author.name).get('id')
|
|
||||||
if DB.remove_protected_player(player_id, local_member_id):
|
|
||||||
await ctx.send(f"{ctx.author.mention} You won't be notified for **{player_name}** medals")
|
|
||||||
else:
|
|
||||||
await ctx.send(f"{ctx.author.mention} You were not protecting **{player_name}** medals")
|
|
||||||
|
|
||||||
|
|
||||||
@hunt.error
|
|
||||||
@remove_hunt.error
|
|
||||||
@protect.error
|
|
||||||
@remove_protection.error
|
|
||||||
async def hunt_error(ctx, error):
|
|
||||||
if isinstance(error, commands.BadArgument):
|
|
||||||
await ctx.send('spēlētāja identifikators jāpadod kā skaitliska vērtība, piemēram, 1620414')
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -405,6 +161,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
# daemon = daemonize.Daemonize(APP_NAME, pidfile, main)
|
|
||||||
# daemon.start()
|
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
discord.py
|
discord.py==1.7.3
|
||||||
requests
|
requests==2.26.0
|
||||||
pytz
|
python-dotenv==0.18.0
|
||||||
python-dotenv
|
sqlite_utils==3.12
|
||||||
sqlite_utils
|
|
||||||
daemonize
|
|
||||||
|
3
run.sh
3
run.sh
@ -1,10 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
python -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
echo "Checking queries..."
|
echo "Checking queries..."
|
||||||
python -m unittest
|
python -m unittest
|
||||||
echo "Starting Discord bot..."
|
echo "Starting Discord bot..."
|
||||||
python discord_bot.py &
|
python discord_bot.py &
|
||||||
sleep 10
|
|
||||||
disown -h %1
|
disown -h %1
|
||||||
|
sleep 10
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
|
|
||||||
|
69
tests.py
69
tests.py
@ -33,67 +33,8 @@ class TestDatabase(unittest.TestCase):
|
|||||||
self.assertTrue(self.db.update_player(player["id"], player["name"]))
|
self.assertTrue(self.db.update_player(player["id"], player["name"]))
|
||||||
self.assertEqual(self.db.get_player(player['id']), player)
|
self.assertEqual(self.db.get_player(player['id']), player)
|
||||||
|
|
||||||
def test_medal(self):
|
def test_epic(self):
|
||||||
kwargs = {"pid": 1, "bid": 235837, "div": 4, "side": 71, "dmg": 1}
|
self.assertFalse(self.db.get_epic(123456))
|
||||||
self.assertFalse(self.db.check_medal(**kwargs))
|
self.assertTrue(self.db.add_epic(123456))
|
||||||
self.assertTrue(self.db.add_reported_medal(**kwargs))
|
self.assertFalse(self.db.add_epic(123456))
|
||||||
self.assertTrue(self.db.check_medal(**kwargs))
|
self.assertTrue(self.db.get_epic(123456))
|
||||||
self.assertTrue(self.db.delete_medals([kwargs['bid']]))
|
|
||||||
|
|
||||||
def test_hunt(self):
|
|
||||||
member_1 = self.db.add_member(2, name="one")
|
|
||||||
member_2 = self.db.add_member(3, name="two")
|
|
||||||
member_3 = self.db.add_member(4, name="three")
|
|
||||||
self.db.add_player(1, 'plato')
|
|
||||||
self.db.add_player(2, 'draco')
|
|
||||||
self.assertFalse(self.db.check_hunt(1, member_1['id']))
|
|
||||||
self.assertFalse(self.db.remove_hunted_player(1, member_1['id']))
|
|
||||||
player_1_hunt = [{'id': 1, "member_id": member_1['id'], 'player_id': 1, 'channel_id': 123},
|
|
||||||
{'id': 2, "member_id": member_2['id'], 'player_id': 1, 'channel_id': 234},
|
|
||||||
{'id': 3, "member_id": member_3['id'], 'player_id': 1, 'channel_id': 345}]
|
|
||||||
for hunt in player_1_hunt:
|
|
||||||
self.assertTrue(self.db.add_hunted_player(hunt['player_id'], hunt['member_id'], hunt['channel_id']))
|
|
||||||
|
|
||||||
player_2_hunt = [{'id': 4, "member_id": member_1['id'], 'player_id': 2, 'channel_id': 456}]
|
|
||||||
for hunt in player_2_hunt:
|
|
||||||
self.assertTrue(self.db.add_hunted_player(hunt['player_id'], hunt['member_id'], hunt['channel_id']))
|
|
||||||
|
|
||||||
self.assertListEqual(self.db.get_hunted_player_ids(), [1, 2])
|
|
||||||
self.assertListEqual(self.db.get_members_to_notify(1), player_1_hunt)
|
|
||||||
self.assertListEqual(self.db.get_members_to_notify(2), player_2_hunt)
|
|
||||||
|
|
||||||
self.assertFalse(self.db.add_hunted_player(1, member_1['id'], 567))
|
|
||||||
self.assertTrue(self.db.check_hunt(1, member_1['id']))
|
|
||||||
self.assertTrue(self.db.remove_hunted_player(1, member_1['id']))
|
|
||||||
self.assertFalse(self.db.check_hunt(1, member_1['id']))
|
|
||||||
|
|
||||||
'''' MEDAL PROTECTION '''
|
|
||||||
def test_protected_medal(self):
|
|
||||||
medal_data = {"pid": 4229720, "div": 7799071, "side": 71}
|
|
||||||
self.assertFalse(self.db.check_protected_medal(**medal_data))
|
|
||||||
self.assertIsNone(self.db.add_protected_medal(**medal_data))
|
|
||||||
self.assertTrue(self.db.check_protected_medal(**medal_data))
|
|
||||||
self.assertFalse(self.db.check_protected_medal(2, medal_data['div'], medal_data['side']))
|
|
||||||
self.assertTrue(self.db.delete_protected_medals([medal_data['div']]))
|
|
||||||
|
|
||||||
def test_protection(self):
|
|
||||||
member = self.db.add_member(2, name="one")
|
|
||||||
self.db.add_player(2, 'plato')
|
|
||||||
self.db.add_player(1620414, 'inpoc1')
|
|
||||||
|
|
||||||
self.assertFalse(self.db.check_protected(2, member['id']))
|
|
||||||
self.assertFalse(self.db.remove_protected_player(2, member['id']))
|
|
||||||
protected_player_1 = {'id': 1, "member_id": member['id'], 'player_id': 1620414, 'channel_id': 123}
|
|
||||||
self.assertTrue(self.db.add_protected_player(
|
|
||||||
protected_player_1['player_id'], protected_player_1['member_id'], protected_player_1['channel_id']
|
|
||||||
))
|
|
||||||
protected_player_2 = {'id': 2, "member_id": member['id'], 'player_id': 2, 'channel_id': 123}
|
|
||||||
self.assertTrue(self.db.add_protected_player(
|
|
||||||
protected_player_2['player_id'], protected_player_2['member_id'], protected_player_2['channel_id']
|
|
||||||
))
|
|
||||||
|
|
||||||
protected_player_ids = [2, 1620414]
|
|
||||||
self.assertListEqual(self.db.get_protected_player_ids(), protected_player_ids)
|
|
||||||
self.assertListEqual(self.db.get_protected_members_to_notify(1620414), [protected_player_1])
|
|
||||||
self.assertListEqual(self.db.get_protected_members_to_notify(2), [protected_player_2])
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user