Safe read and write opearation with file lock.

Added file locks.  -  avoiding data leaks.
file handle module  -  To handle file opearations safly.
Added filelock.py  -  licensed file.
linted the pdata.py  -  type checking and formating.
This commit is contained in:
pranav-1711 2022-02-26 16:22:18 +05:30
parent 7c67998dec
commit 9f4be5eb84
6 changed files with 694 additions and 264 deletions

View file

@ -83,7 +83,7 @@ def bootstraping():
# import features
if settings["whitelist"]:
pdata.loadWhitelist()
pdata.load_white_list()
import_discord_bot()
import_games()

89
dist/ba_root/mods/file_handle.py vendored Normal file
View file

@ -0,0 +1,89 @@
"""Module to handle operations with file."""
# ba_meta require api 6
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
__all__ = ["OpenJson", "JsonFile", "PathNotExistsError"]
from typing import TYPE_CHECKING
from dataclasses import dataclass
import json
import os
import re
from filelock import FileLock
if TYPE_CHECKING:
pass
class PathNotExistsError(Exception):
"""Error telling path does not exits."""
@dataclass
class JsonFile:
"""Object to handle simple operations with json file."""
path: str
def load(self, **kw) -> dict:
"""Loads the json file."""
if not os.path.exists(self.path):
PathNotExistsError(f"Path does not exists. {self.path}")
with FileLock(self.path):
with open(self.path, mode="r", encoding="utf-8") as json_file:
try:
data = json.load(json_file, **kw)
except json.JSONDecodeError:
print(f"Could not load json. {self.path}", end="")
print("Creating json in the file.", end="")
data = {}
self.dump(data)
return data
def dump(self, data: dict, **kw) -> None:
"""Dumps the json file."""
if not os.path.exists(self.path):
PathNotExistsError(f"Path does not exists. {self.path}")
with FileLock(self.path):
with open(self.path, mode="w", encoding="utf-8") as json_file:
json.dump(data, json_file, **kw)
def format(self, data: dict) -> None:
"""Dumps the json file."""
if not os.path.exists(self.path):
PathNotExistsError(f"Path does not exists. {self.path}")
with FileLock(self.path):
output = json.dumps(data, indent=4)
output2 = re.sub(r'": \[\s+', '": [', output)
output3 = re.sub(r'",\s+', '", ', output2)
output4 = re.sub(r'"\s+\]', '"]', output3)
with open(self.path, mode="w", encoding="utf-8") as json_file:
json_file.write(output4)
class OpenJson:
"""Context manager to open json files.
Json files opened with this will be file locked. If
json file is not readable then It will create new dict."""
def __init__(self, path: str) -> None:
self.json_obj = JsonFile(path)
def __enter__(self) -> JsonFile:
return self.json_obj
def __exit__(self, _type, value, traceback):
if traceback:
print(traceback)

103
dist/ba_root/mods/filelock.py vendored Normal file
View file

@ -0,0 +1,103 @@
# Copyright (c) 2009, Evan Fosmark
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are those
# of the authors and should not be interpreted as representing official policies,
# either expressed or implied, of the FreeBSD Project.
import os
import time
import errno
class FileLockException(Exception):
pass
class FileLock(object):
"""A file locking mechanism that has context-manager support so
you can use it in a with statement. This should be relatively cross
compatible as it doesn't rely on msvcrt or fcntl for the locking.
"""
def __init__(self, file_name, timeout=10, delay=0.05):
"""Prepare the file locker. Specify the file to lock and optionally
the maximum timeout and the delay between each attempt to lock.
"""
self.is_locked = False
self.lockfile = os.path.join(os.getcwd(), "%s.lock" % file_name)
self.file_name = file_name
self.timeout = timeout
self.delay = delay
def acquire(self):
"""Acquire the lock, if possible. If the lock is in use, it check again
every `wait` seconds. It does this until it either gets the lock or
exceeds `timeout` number of seconds, in which case it throws
an exception.
"""
start_time = time.time()
while True:
try:
self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
break
except OSError as e:
if e.errno != errno.EEXIST:
raise
if (time.time() - start_time) >= self.timeout:
raise FileLockException("Timeout occured.")
time.sleep(self.delay)
self.is_locked = True
def release(self):
"""Get rid of the lock by deleting the lockfile.
When working in a `with` statement, this gets automatically
called at the end.
"""
if self.is_locked:
os.close(self.fd)
os.unlink(self.lockfile)
self.is_locked = False
def __enter__(self):
"""Activated when used in the with statement.
Should automatically acquire a lock to be used in the with block.
"""
if not self.is_locked:
self.acquire()
print(f"{self.file_name.split('/')[-1]} locked")
return self
def __exit__(self, type, value, traceback):
"""Activated at the end of the with statement.
It automatically releases the lock if it isn't locked.
"""
if self.is_locked:
self.release()
print(f"{self.file_name.split('/')[-1]} unlocked")
def __del__(self):
"""Make sure that the FileLock instance doesn't leave a lockfile
lying around.
"""
self.release()

View file

@ -1,302 +1,540 @@
# Released under the MIT License. See LICENSE for details.
import _ba, os, json
from serverData import serverdata
"""Module to manage players data."""
# ba_meta require api 6
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import time
import os
import _thread
roles = {}
data = {}
custom = {}
whitelist=[]
data_path = os.path.join(_ba.env()['python_directory_user'],"playersData" + os.sep)
from serverData import serverdata
from file_handle import OpenJson
import _ba # pylint: disable=import-error
# ============== player data =======================
def get_info(id):
with open(data_path+'profiles.json', 'r') as f:
profiles = json.load(f)
if id in profiles:
if TYPE_CHECKING:
pass
return profiles[id]
PLAYERS_DATA_PATH = os.path.join(
_ba.env()["python_directory_user"], "playersData" + os.sep
)
class CacheData: # pylint: disable=too-few-public-methods
"""Stores the cache data."""
roles: dict = {}
data: dict = {}
custom: dict = {}
whitelist: list[str] = []
def get_info(account_id: str) -> dict | None:
"""Returns the information about player.
Parameters
----------
account_id : str
account_id of the client
Returns
-------
dict | None
information of client
"""
with OpenJson(PLAYERS_DATA_PATH + "profiles.json") as profiles_file:
profiles = profiles_file.load()
if account_id in profiles:
return profiles[account_id]
return None
def get_profiles():
with open(data_path+'profiles.json', 'r') as f:
profiles = json.load(f)
def get_profiles() -> dict:
"""Returns the profiles of all players.
Returns
-------
dict
profiles of the players
"""
with OpenJson(PLAYERS_DATA_PATH + "profiles.json") as profiles_file:
profiles = profiles_file.load()
return profiles
def commit_profiles(profiles):
with open(data_path+'profiles.json', 'w') as f:
json.dump(profiles,f,indent=4)
def add_profile(id,display_string,currentname,age):
f=open(data_path+"profiles.json","r")
profiles=json.load(f)
f.close()
profiles[id]={"display_string":display_string,
"profiles":[],
"name":currentname,
"isBan":False,
"isMuted":False,
"accountAge":age,
"registerOn":time.time(),
"canStartKickVote":True,
"spamCount":0,
"lastSpam":time.time(),
"totaltimeplayer":0,
"lastseen":0}
def commit_profiles(profiles: dict) -> None:
"""Commits the given profiles in the database.
Parameters
----------
profiles : dict
profiles of all players
"""
with OpenJson(PLAYERS_DATA_PATH + "profiles.json") as profiles_file:
profiles_file.dump(profiles, indent=4)
def add_profile(
account_id: str,
display_string: str,
current_name: str,
account_age: int,
) -> None:
"""Adds the profile in database.
f=open(data_path+"profiles.json","w")
json.dump(profiles,f,indent=4)
serverdata.clients[id]=profiles[id]
serverdata.clients[id]["warnCount"]=0
serverdata.clients[id]["lastWarned"]=time.time()
serverdata.clients[id]["verified"]=False
serverdata.clients[id]["rejoincount"]=1
serverdata.clients[id]["lastJoin"]=time.time()
f.close()
def update_displayString(id,display_string):
profiles=get_profiles()
if id in profiles:
profiles[id]["display_string"]=display_string
commit_profiles(profiles)
def update_profile(id,display_string=None,allprofiles=[],name=None):
f=open(data_path+"profiles.json","r")
profiles=json.load(f.read())
f.close()
if id in profiles:
if display_string != None and display_string not in profiles[id]['display_string']:
profiles[id]['display_string'].append(display_string)
if profiles!=[]:
for profile in allprofiles:
if profile not in profiles[id]['profiles']:
profiles[id]['profiles'].append(profile)
if name != None:
profiles[id]['name']=name
f=open(data_path+"profiles.json","w")
json.dump(profiles,f,indent=4)
f.close()
def ban_player(id):
profiles= get_profiles()
if id in profiles:
profiles[id]['isBan']=True
_thread.start_new_thread(commit_profiles,(profiles,))
# commit_profiles(profiles)
def mute(id):
profiles=get_profiles()
if id in profiles:
profiles[id]["isMuted"]=True
_thread.start_new_thread(commit_profiles,(profiles,))
# commit_profiles(profiles)
def unmute(id):
profiles=get_profiles()
if id in profiles:
profiles[id]["isMuted"]=False
_thread.start_new_thread(commit_profiles,(profiles,))
# commit_profiles(profiles)
def updateSpam(id,spamCount,lastSpam):
profiles=get_profiles()
if id in profiles:
profiles[id]["spamCount"]=spamCount
profiles[id]["lastSpam"]=lastSpam
commit_profiles(profiles)
#================ ROLES ==========================
def commit_roles(data):
global roles
if data == {}:
return
output=json.dumps(data,indent=4)
import re
output2 = re.sub(r'": \[\s+', '": [', output)
output3 = re.sub(r'",\s+', '", ', output2)
output4 = re.sub(r'"\s+\]', '"]', output3)
with open(data_path+'roles.json','w') as f:
f.write(output4)
def get_roles():
global roles
if roles == {}:
with open(data_path+'roles.json', 'r') as f:
roles = json.load(f)
return roles
def create_role(role):
global roles
_roles = get_roles()
if role not in _roles:
_roles[role] = {
"tag":role,
"tagcolor":[1,1,1],
"commands":[],
"ids":[]
Parameters
----------
account_id : str
account id of the client
display_string : str
display string of the client
current_name : str
name of the client
account_age : int
account_age of the account
"""
with OpenJson(PLAYERS_DATA_PATH + "profiles.json") as profiles_file:
profiles = profiles_file.load()
profiles[account_id] = {
"display_string": display_string,
"profiles": [],
"name": current_name,
"isBan": False,
"isMuted": False,
"accountAge": account_age,
"registerOn": time.time(),
"canStartKickVote": True,
"spamCount": 0,
"lastSpam": time.time(),
"totaltimeplayer": 0,
"lastseen": 0,
}
roles = _roles
commit_roles(_roles)
return
commit_profiles(profiles)
serverdata.clients[account_id] = profiles[account_id]
serverdata.clients[account_id]["warnCount"] = 0
serverdata.clients[account_id]["lastWarned"] = time.time()
serverdata.clients[account_id]["verified"] = False
serverdata.clients[account_id]["rejoincount"] = 1
serverdata.clients[account_id]["lastJoin"] = time.time()
def update_display_string(account_id: str, display_string: str) -> None:
"""Updates the display string of the account.
Parameters
----------
account_id : str
account id of the client
display_string : str
new display string to be updated
"""
profiles = get_profiles()
if account_id in profiles:
profiles[account_id]["display_string"] = display_string
commit_profiles(profiles)
def update_profile(
account_id: str,
display_string: str = None,
allprofiles: list[str] = None,
name: str = None,
) -> None:
"""Updates the profile of client.
Parameters
----------
account_id : str
account id of the client
display_string : str, optional
display string of the account, by default None
allprofiles : list[str], optional
all profiles of the client, by default None
name : str, optional
name to be updated, by default None
"""
with OpenJson(PLAYERS_DATA_PATH + "profiles.json") as profiles_file:
profiles = profiles_file.load()
if profiles is None:
return
if account_id in profiles and display_string is not None:
if display_string not in profiles[account_id]["display_string"]:
profiles[account_id]["display_string"].append(display_string)
def add_player_role(role, id):
global roles
_roles = get_roles()
if role in _roles:
if allprofiles is not None:
for profile in allprofiles:
if profile not in profiles[account_id]["profiles"]:
profiles[account_id]["profiles"].append(profile)
if id not in _roles[role]["ids"]:
_roles[role]["ids"].append(id)
roles =_roles
commit_roles(_roles)
if name is not None:
profiles[account_id]["name"] = name
commit_profiles(profiles)
def ban_player(account_id: str) -> None:
"""Bans the player.
Parameters
----------
account_id : str
account id of the player to be banned
"""
profiles = get_profiles()
if account_id in profiles:
profiles[account_id]["isBan"] = True
_thread.start_new_thread(commit_profiles, (profiles,))
def mute(account_id: str) -> None:
"""Mutes the player.
Parameters
----------
account_id : str
acccount id of the player to be muted
"""
profiles = get_profiles()
if account_id in profiles:
profiles[account_id]["isMuted"] = True
_thread.start_new_thread(commit_profiles, (profiles,))
def unmute(account_id: str) -> None:
"""Unmutes the player.
Parameters
----------
account_id : str
acccount id of the player to be unmuted
"""
profiles = get_profiles()
if account_id in profiles:
profiles[account_id]["isMuted"] = False
_thread.start_new_thread(commit_profiles, (profiles,))
def update_spam(account_id: str, spam_count: int, last_spam: float) -> None:
"""Updates the spam time and count.
Parameters
----------
account_id : str
account id of the client
spam_count : int
spam count to be added
last_spam : float
last spam time
"""
profiles = get_profiles()
if account_id in profiles:
profiles[account_id]["spamCount"] = spam_count
profiles[account_id]["lastSpam"] = last_spam
commit_profiles(profiles)
def commit_roles(data: dict) -> None:
"""Commits the roles in database.
Parameters
----------
data : dict
data to be commited
"""
if not data:
return
with OpenJson(PLAYERS_DATA_PATH + "roles.json") as roles_file:
roles_file.format(data)
def get_roles() -> dict:
"""Returns the roles.
Returns
-------
dict
roles
"""
if CacheData.roles == {}:
with OpenJson(PLAYERS_DATA_PATH + "roles.json") as roles_file:
roles = roles_file.load()
return roles
return CacheData.roles
def create_role(role: str) -> None:
"""Ceates the role.
Parameters
----------
role : str
role to be created
"""
roles = get_roles()
if role in roles:
return
roles[role] = {
"tag": role,
"tagcolor": [1, 1, 1],
"commands": [],
"ids": [],
}
CacheData.roles = roles
commit_roles(roles)
def add_player_role(role: str, account_id: str) -> None:
"""Adds the player to the role.
Parameters
----------
role : str
role to be added
account_id : str
account id of the client
"""
roles = get_roles()
if role in roles:
if account_id not in roles[role]["ids"]:
roles[role]["ids"].append(account_id)
CacheData.roles = roles
commit_roles(roles)
else:
print("no role such")
def remove_player_role(role: str, account_id: str) -> str:
"""Removes the role from player.
def remove_player_role(role, id):
global roles
_roles = get_roles()
if role in _roles:
_roles[role]["ids"].remove(id)
roles =_roles
commit_roles(_roles)
return "removed from "+role
Parameters
----------
role : str
role to br removed
account_id : str
account id of the client
Returns
-------
str
status of the removing role
"""
roles = get_roles()
if role in roles:
roles[role]["ids"].remove(account_id)
CacheData.roles = roles
commit_roles(roles)
return "removed from " + role
return "role not exists"
def add_command_role(role: str, command: str) -> str:
"""Adds the command to the role.
Parameters
----------
role : str
role to add the command
command : str
command to be added
def add_command_role(role, command):
global roles
_roles = get_roles()
if role in _roles:
if command not in _roles[role]["commands"]:
_roles[role]["commands"].append(command)
roles =_roles
commit_roles(_roles)
return "command added to "+role
Returns
-------
str
status of the adding command
"""
roles = get_roles()
if role in roles:
if command not in roles[role]["commands"]:
roles[role]["commands"].append(command)
CacheData.roles = roles
commit_roles(roles)
return "command added to " + role
return "command not exists"
def remove_command_role(role, command):
global roles
_roles = get_roles()
if role in _roles:
if command in _roles[role]["commands"]:
_roles[role]["commands"].remove(command)
roles =_roles
commit_roles(_roles)
return "command added to "+role
def remove_command_role(role: str, command: str) -> str:
"""Removes the command from the role.
Parameters
----------
role : str
role to remove command from
command : str
command to be removed
Returns
-------
str
status of the removing command
"""
roles = get_roles()
if role in roles:
if command in roles[role]["commands"]:
roles[role]["commands"].remove(command)
CacheData.roles = roles
commit_roles(roles)
return "command added to " + role
return "command not exists"
def change_role_tag(role, tag):
global roles
_roles = get_roles()
if role in _roles:
_roles[role]['tag'] = tag
roles = _roles
commit_roles(_roles)
def change_role_tag(role: str, tag: str) -> str:
"""Changes the tag of the role.
Parameters
----------
role : str
role to chnage the tag
tag : str
tag to be added
Returns
-------
str
status of the adding tag
"""
roles = get_roles()
if role in roles:
roles[role]["tag"] = tag
CacheData.roles = roles
commit_roles(roles)
return "tag changed"
return "role not exists"
def get_player_roles(acc_id):
def get_player_roles(account_id: str) -> list[str]:
"""Returns the avalibe roles of the account.
_roles = get_roles()
roles=[]
for role in _roles:
if acc_id in _roles[role]["ids"]:
roles.append(role)
return roles
Parameters
----------
account_id : str
account id of the client
Returns
-------
list[str]
list of the roles
"""
roles = get_roles()
have_roles = []
for role in roles:
if account_id in roles[role]["ids"]:
have_roles.append(role)
return have_roles
##### those ups done will clean it in future
def get_custom() -> dict:
"""Returns the custom effects.
#======================= CUSTOM EFFECTS/TAGS ===============
def get_custom():
global custom
if custom=={}:
with open(data_path+"custom.json","r") as f:
custom = json.loads(f.read())
return custom
Returns
-------
dict
custom effects
"""
if CacheData.custom == {}:
with OpenJson(PLAYERS_DATA_PATH + "custom.json") as custom_file:
custom = custom_file.load()
return custom
return CacheData.custom
def set_effect(effect, id):
global custom
_custom = get_custom()
_custom['customeffects'][id] = effect
custom = _custom
def set_effect(effect: str, accout_id: str) -> None:
"""Sets the costum effect for the player.
Parameters
----------
effect : str
effect to be added to the player
accout_id : str
account id of the client
"""
custom = get_custom()
custom["customeffects"][accout_id] = effect
CacheData.custom = custom
commit_c()
def set_tag(tag, id):
def set_tag(tag: str, account_id: str) -> None:
"""Sets the custom tag to the player.
global custom
_custom = get_custom()
_custom['customtag'][id] = tag
custom = _custom
Parameters
----------
tag : str
tag to be added to the player
account_id : str
account id of the client
"""
custom = get_custom()
custom["customtag"][account_id] = tag
CacheData.custom = custom
commit_c()
def remove_effect(id):
global custom
_custom = get_custom()
_custom['customeffects'].pop(id)
custom = _custom
def remove_effect(account_id: str) -> None:
"""Removes the effect from player.
Parameters
----------
account_id : str
account id of the client
"""
custom = get_custom()
custom["customeffects"].pop(account_id)
CacheData.custom = custom
commit_c()
def remove_tag(id):
global custom
_custom = get_custom()
_custom['customtag'].pop(id)
custom = _custom
def remove_tag(account_id: str) -> None:
"""Removes the tag from the player
Parameters
----------
account_id : str
account id of the client
"""
custom = get_custom()
custom["customtag"].pop(account_id)
CacheData.custom = custom
commit_c()
def commit_c():
global custom
with open(data_path+"custom.json",'w') as f:
json.dump(custom,f,indent=4)
"""Commits the custom data into the custom.json."""
with OpenJson(PLAYERS_DATA_PATH + "custom.json") as custom_file:
custom_file.dump(CacheData.custom, indent=4)
def update_toppers(topperlist):
global roles
_roles = get_roles()
if "top5" not in _roles:
def update_toppers(topper_list: list[str]) -> None:
"""Updates the topper list into top5 role.
Parameters
----------
topper_list : list[str]
list of the topper players
"""
roles = get_roles()
if "top5" not in roles:
create_role("top5")
roles["top5"]["ids"]=topperlist
CacheData.roles["top5"]["ids"] = topper_list
commit_roles(roles)
def loadWhitelist():
global whitelist
with open(data_path+"whitelist.json","r") as f:
data=json.loads(f.read())
for id in data:
whitelist.append(id)
def load_white_list() -> None:
"""Loads the whitelist."""
with OpenJson(PLAYERS_DATA_PATH + "whitelist.json") as whitelist_file:
data = whitelist_file.load()
for account_id in data:
CacheData.whitelist.append(account_id)

View file

@ -23,14 +23,6 @@
"commands": [],
"ids": []
},
"top5": {
"tag": "top5", "tagcolor": [1,
1,
1
],
"commands": [],
"ids": ["pb-IF4VAk4a"]
},
"smoothy": {
"tag": "smoothy", "tagcolor": [1,
1,
@ -46,5 +38,13 @@
],
"commands": [],
"ids": []
},
"top5": {
"tag": "top5", "tagcolor": [1,
1,
1
],
"commands": [],
"ids": ["pb-IF5XUm9eAg==", "pb-IF43VUwlAg==", "pb-IF4iVUc5Cg==", "pb-IF4vNnMJ", "pb-IF4TVWwZUQ=="]
}
}

View file

@ -90,7 +90,7 @@ class checkserver(object):
return
if settings["whitelist"] and ros["account_id"]!=None:
if ros["account_id"] not in pdata.whitelist:
if ros["account_id"] not in pdata.CacheData.whitelist:
_ba.screenmessage("Not in whitelist,contact admin",color=(1,0,0),transient=True,clients=[ros['client_id']])
logger.log(d_str+"||"+ros["account_id"]+" | kicked > not in whitelist")
_ba.disconnect_client(ros['client_id'])
@ -321,7 +321,7 @@ def save_age(age, pb_id,display_string):
def save_ids(ids,pb_id,display_string):
pdata.update_displayString(pb_id,ids)
pdata.update_display_string(pb_id,ids)
if display_string not in ids: