From a070124d1d31fc69227b06163a15f941a16f6dee Mon Sep 17 00:00:00 2001 From: Ayush Saini <36878972+imayushsaini@users.noreply.github.com> Date: Sat, 24 Jun 2023 18:02:12 +0530 Subject: [PATCH] added push notification subscription service --- dist/ba_root/mods/custom_hooks.py | 40 +++-- dist/ba_root/mods/playersData/blacklist.json | 37 ++++- .../mods/playersData/subscribed_players.json | 31 ++++ .../mods/playersData/subscriptions.json | 10 ++ dist/ba_root/mods/plugins/bcs_plugin.py | 18 +- .../ba_root/mods/plugins/bombsquad_service.py | 13 +- .../mods/tools/notification_manager.py | 154 ++++++++++++++++++ dist/ba_root/mods/tools/servercheck.py | 4 +- 8 files changed, 278 insertions(+), 29 deletions(-) create mode 100644 dist/ba_root/mods/playersData/subscribed_players.json create mode 100644 dist/ba_root/mods/playersData/subscriptions.json create mode 100644 dist/ba_root/mods/tools/notification_manager.py diff --git a/dist/ba_root/mods/custom_hooks.py b/dist/ba_root/mods/custom_hooks.py index 37b7263..e059844 100644 --- a/dist/ba_root/mods/custom_hooks.py +++ b/dist/ba_root/mods/custom_hooks.py @@ -35,6 +35,7 @@ from features import votingmachine from features import text_on_map, announcement from features import map_fun from spazmod import modifyspaz +from tools import notification_manager if TYPE_CHECKING: from typing import Optional, Any @@ -46,6 +47,8 @@ def filter_chat_message(msg: str, client_id: int) -> str | None: return handlechat.filter_chat_message(msg, client_id) # ba_meta export plugin + + class modSetup(ba.Plugin): def on_app_running(self): """Runs when app is launched.""" @@ -69,8 +72,11 @@ class modSetup(ba.Plugin): ba.internal.sign_in_v1('Local') ba.timer(60, playlist.flush_playlists) - def on_app_shutdown(self): - pass + def on_app_shutdown(self): # TODO not working, fix this, also dump server logs + print("Server shutting down , lets save cache") + pdata.dump_cache() + notification_manager.dump_cache() + print("Done dumping memory") def score_screen_on_begin(_stats: ba.Stats) -> None: @@ -99,6 +105,7 @@ def bootstraping(): _thread.start_new_thread(mystats.refreshStats, ()) pdata.load_cache() _thread.start_new_thread(pdata.dump_cache, ()) + _thread.start_new_thread(notification_manager.dump_cache, ()) # import plugins if settings["elPatronPowerups"]["enable"]: @@ -124,8 +131,7 @@ def bootstraping(): from plugins import colorfulmaps2 try: pass -# from tools import healthcheck -# healthcheck.main() spamming logs , will increase log interval later + # from tools import healthcheck except Exception as e: print(e) try: @@ -283,24 +289,25 @@ def on_map_init(): text_on_map.textonmap() modifyspaz.setTeamCharacter() + def shutdown(func) -> None: """Set the app to quit either now or at the next clean opportunity.""" def wrapper(*args, **kwargs): # add screen text and tell players we are going to restart soon. ba.internal.chatmessage( - "Server will restart on next opportunity. (series end)") + "Server will restart on next opportunity. (series end)") _ba.restart_scheduled = True _ba.get_foreground_host_activity().restart_msg = _ba.newnode('text', - attrs={ - 'text': "Server going to restart after this series.", - 'flatness': 1.0, - 'h_align': 'right', - 'v_attach': 'bottom', - 'h_attach': 'right', - 'scale': 0.5, - 'position': (-25, 54), - 'color': (1, 0.5, 0.7) - }) + attrs={ + 'text': "Server going to restart after this series.", + 'flatness': 1.0, + 'h_align': 'right', + 'v_attach': 'bottom', + 'h_attach': 'right', + 'scale': 0.5, + 'position': (-25, 54), + 'color': (1, 0.5, 0.7) + }) func(*args, **kwargs) return wrapper @@ -318,7 +325,8 @@ def on_player_request(func) -> bool: if current_player.get_v1_account_id() == player.get_v1_account_id(): count += 1 if count >= settings["maxPlayersPerDevice"]: - _ba.screenmessage("Reached max players limit per device", clients=[player.inputdevice.client_id], transient=True,) + _ba.screenmessage("Reached max players limit per device", clients=[ + player.inputdevice.client_id], transient=True,) return False return func(*args, **kwargs) return wrapper diff --git a/dist/ba_root/mods/playersData/blacklist.json b/dist/ba_root/mods/playersData/blacklist.json index 589a154..3ed0009 100644 --- a/dist/ba_root/mods/playersData/blacklist.json +++ b/dist/ba_root/mods/playersData/blacklist.json @@ -1,11 +1,34 @@ { "ban": { - "ids": [ - ], - "ips": [ - ], - "deviceids": [ - ] + "ids": { + "pb-234": { + "till": "2023-06-07 21:59:20", + "reason": "auto ban for spam" + } + }, + "ips": { + "19.168.0.0.1": { + "till": "2023-06-07 21:59:20", + "reason": "auto ban for spam" + } + }, + "deviceids": { + "sdfdsfwr3": { + "till": "2023-06-07 21:59:20", + "reason": "auto ban for spam" + } + } }, - "muted-ids": [] + "muted-ids": { + "pb-IF4iU0QaEw==": { + "till": "2023-06-19 19:44:47", + "reason": "manually from website" + } + }, + "kick-vote-disabled": { + "pb-JiNJARBaXEFBVF9HFkNXXF1EF0ZaRlZE": { + "till": "2023-06-12 19:37:48", + "reason": "manually from website" + } + } } \ No newline at end of file diff --git a/dist/ba_root/mods/playersData/subscribed_players.json b/dist/ba_root/mods/playersData/subscribed_players.json new file mode 100644 index 0000000..5107c6a --- /dev/null +++ b/dist/ba_root/mods/playersData/subscribed_players.json @@ -0,0 +1,31 @@ +{ + "pb-IF41U2scIg==": { + "subscribers": [ + "jrz6Rg" + ] + }, + "pb-IF4IVFkKNA==": { + "subscribers": [ + "jrz6Rg" + ], + "name": "\ue063ATTITUDEB2" + }, + "pb-IF4CKhYn": { + "subscribers": [ + "jrz6Rg" + ], + "name": "\ue032Rico Un\u2122\u00a9\u00ae" + }, + "pb-IF4jF1NY": { + "subscribers": [ + "jrz6Rg" + ], + "name": "\ue01eHoemaster" + }, + "pb-IF4wVVk8Jg==": { + "subscribers": [ + "jrz6Rg" + ], + "name": "\ue063Homulilly" + } +} \ No newline at end of file diff --git a/dist/ba_root/mods/playersData/subscriptions.json b/dist/ba_root/mods/playersData/subscriptions.json new file mode 100644 index 0000000..6087258 --- /dev/null +++ b/dist/ba_root/mods/playersData/subscriptions.json @@ -0,0 +1,10 @@ +{ + "jrz6Rg": { + "endpoint": "https://wns2-pn1p.notify.windows.com/w/?token=8nmgbauneNBqKw7H6IGl", + "expirationTime": null, + "keys": { + "p256dh": "BOG4rFeziA", + "auth": "yNIDrg" + } + } +} \ No newline at end of file diff --git a/dist/ba_root/mods/plugins/bcs_plugin.py b/dist/ba_root/mods/plugins/bcs_plugin.py index ac32750..bad389d 100644 --- a/dist/ba_root/mods/plugins/bcs_plugin.py +++ b/dist/ba_root/mods/plugins/bcs_plugin.py @@ -18,7 +18,7 @@ os.environ['FLASK_ENV'] = 'development' app = Flask(__name__) app.config["DEBUG"] = False -SECRET_KEY = "my_secret_key" +SECRET_KEY = "my_secret_key2" @app.after_request @@ -56,6 +56,19 @@ def get_top200(): return jsonify(bombsquad_service.get_top_200()), 200 +@app.route('/api/subscribe', methods=['POST']) +def subscribe_player(): + try: + data = request.get_json() + bombsquad_service.subscribe_player( + data["subscription"], data["player_id"], data["name"]) + response = { + 'message': f'Subscribed {data["name"]} successfully , will send confirmation notification to test'} + return jsonify(response), 201 + except Exception as e: + return jsonify({'message': 'Error processing request', 'error': str(e)}), 400 + + # ============ Admin only ========= @@ -250,7 +263,8 @@ def update_server_config(): def enable(): - flask_run = _thread.start_new_thread(app.run, ("0.0.0.0", _ba.get_game_port(), False)) + flask_run = _thread.start_new_thread( + app.run, ("0.0.0.0", _ba.get_game_port(), False)) # uvicorn_thread = threading.Thread(target=start_uvicorn) # uvicorn_thread.start() # options = { diff --git a/dist/ba_root/mods/plugins/bombsquad_service.py b/dist/ba_root/mods/plugins/bombsquad_service.py index c509c2b..fd0a816 100644 --- a/dist/ba_root/mods/plugins/bombsquad_service.py +++ b/dist/ba_root/mods/plugins/bombsquad_service.py @@ -1,3 +1,6 @@ +import ecdsa +import base64 +import json import ba import _ba import ba.internal @@ -6,7 +9,7 @@ from typing import Optional, Any, Dict, List, Type, Sequence from ba._gameactivity import GameActivity from playersData import pdata from serverData import serverdata -from tools import servercheck, logger +from tools import servercheck, logger, notification_manager import setting from datetime import datetime import _thread @@ -15,7 +18,7 @@ import yaml stats = {} leaderboard = {} top200 = {} - +vapidkeys = {} serverinfo = {} @@ -24,7 +27,7 @@ class BsDataThread(object): global stats stats["name"] = _ba.app.server._config.party_name stats["discord"] = "https://discord.gg/ucyaesh" - stats["vapidKey"] = "sjfbsdjfdsf" + stats["vapidKey"] = notification_manager.get_vapid_keys()["public_key"] self.refresh_stats_cache_timer = ba.Timer(8, ba.Call(self.refreshStats), timetype=ba.TimeType.REAL, repeat=True) @@ -255,3 +258,7 @@ def do_action(action, value): _ba.pushcall(ba.Call(_ba.chatmessage, value), from_other_thread=True) elif action == "quit": _ba.pushcall(ba.Call(_ba.quit), from_other_thread=True) + + +def subscribe_player(sub, account_id, name): + notification_manager.subscribe(sub, account_id, name) diff --git a/dist/ba_root/mods/tools/notification_manager.py b/dist/ba_root/mods/tools/notification_manager.py new file mode 100644 index 0000000..00de127 --- /dev/null +++ b/dist/ba_root/mods/tools/notification_manager.py @@ -0,0 +1,154 @@ +import time +import shutil +import random +import string +from pywebpush import webpush +import json +import base64 +import ecdsa +import os +import _ba +from datetime import datetime +vapidkeys = {} +subscriptions = {} +subscribed_players = {} +PLAYERS_DATA_PATH = os.path.join( + _ba.env()["python_directory_user"], "playersData" + os.sep +) + + +def get_vapid_keys(): + global vapidkeys + if vapidkeys != {}: + return vapidkeys + try: + f = open(".keys", "r") + vapidkeys = json.load(f) + f.close() + return vapidkeys + except: + pk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) + vk = pk.get_verifying_key() + vapidkeys = { + 'private_key': base64.urlsafe_b64encode(pk.to_string()).rstrip(b'=').decode('utf-8'), + 'public_key': base64.urlsafe_b64encode(b'\x04' + vk.to_string()).rstrip(b'=').decode('utf-8') + } + f = open(".keys", "w") + json.dump(vapidkeys, f) + f.close() + return vapidkeys + + +def send_push_notification(subscription, payload): + # try: + # Send the push notification using the subscription and payload + print(subscription) + print(payload) + print(get_vapid_keys()["private_key"]) + + webpush(subscription_info=subscription, data=json.dumps(payload), + vapid_private_key=get_vapid_keys()["private_key"], vapid_claims={ + 'sub': 'mailto:{}'.format("test@ballistica.net"), + }) + print("Push notification sent successfully") + # except Exception as e: + # print("Error sending push notification:", str(e)) + + +# if we already have that browser subscription saved get id or generate new one +def get_subscriber_id(sub): + subscriber_id = None + + for key, value in subscriptions.items(): + if value["endpoint"] == sub["endpoint"]: + subscriber_id = key + break + + if not subscriber_id: + subscriber_id = generate_random_string(6) + subscriptions[subscriber_id] = sub + return subscriber_id + + +def generate_random_string(length): + letters = string.ascii_letters + string.digits + return ''.join(random.choice(letters) for _ in range(length)) + + +def subscribe(sub, account_id, name): + + id = get_subscriber_id(sub) + if account_id in subscribed_players: + if id not in subscribed_players[account_id]["subscribers"]: + subscribed_players[account_id]["subscribers"].append(id) + subscribed_players[account_id]["name"] = name + else: + subscribed_players[account_id] = {"subscribers": [id], "name": name} + send_push_notification(sub, {"notification": { + "title": "Notification working !", "body": f'subscribed {name}'}}) + + +def player_joined(pb_id): + now = datetime.now() + if pb_id in subscribed_players: + if "last_notification" in subscribed_players[pb_id] and (now - subscribed_players[pb_id]["last_notification"]).seconds < 15 * 60: + pass + else: + subscribed_players[pb_id]["last_notification"] = now + subscribes = subscribed_players[pb_id]["subscribers"] + for subscriber_id in subscribes: + sub = subscriptions[subscriber_id] + send_push_notification( + sub, { + "notification": { + "title": f'{subscribed_players[pb_id]["name"] } is playing now', + "body": f'Join {_ba.app.server._config.party_name} server {subscribed_players[pb_id]["name"]} is waiting for you ', + "icon": "assets/icons/icon-96x96.png", + "vibrate": [100, 50, 100], + "requireInteraction": True, + "data": {"dateOfArrival": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, + "actions": [{"action": "nothing", "title": "Launch Bombsquad"}], + } + }) + + +def loadCache(): + global subscriptions + global subscribed_players + try: + f = open(PLAYERS_DATA_PATH+"subscriptions.json", "r") + subscriptions = json.load(f) + f.close() + except: + f = open(PLAYERS_DATA_PATH+"subscriptions.json.backup", "r") + subscriptions = json.load(f) + f.close() + try: + f = open(PLAYERS_DATA_PATH+"subscribed_players.json", "r") + subscribed_players = json.load(f) + f.close() + except: + f = open(PLAYERS_DATA_PATH+"subscribed_players.json.backup", "r") + subscribed_players = json.load(f) + f.close() + + +def dump_cache(): + if subscriptions != {}: + shutil.copyfile(PLAYERS_DATA_PATH + "subscriptions.json", + PLAYERS_DATA_PATH + "subscriptions.json.backup") + + with open(PLAYERS_DATA_PATH + "subscriptions.json", "w") as f: + json.dump(subscriptions, f, indent=4) + if subscribed_players != {}: + shutil.copyfile(PLAYERS_DATA_PATH + "subscribed_players.json", + PLAYERS_DATA_PATH + "subscribed_players.json.backup") + + with open(PLAYERS_DATA_PATH + "subscribed_players.json", "w") as f: + json.dump(subscribed_players, f, indent=4) + + time.sleep(60) + dump_cache() + + +loadCache() diff --git a/dist/ba_root/mods/tools/servercheck.py b/dist/ba_root/mods/tools/servercheck.py index 2f42f67..328dd6f 100644 --- a/dist/ba_root/mods/tools/servercheck.py +++ b/dist/ba_root/mods/tools/servercheck.py @@ -17,7 +17,7 @@ import _thread from tools import logger from features import profanity from playersData import pdata - +from . import notification_manager blacklist = pdata.get_blacklist() settings = setting.get_settings_data() @@ -191,6 +191,7 @@ def on_player_join_server(pbid, player_data, ip, device_id): _ba.screenmessage(settings["regularWelcomeMsg"] + " " + device_string, color=(0.60, 0.8, 0.6), transient=True, clients=[clid]) + notification_manager.player_joined(pbid) else: # fetch id for first time. thread = FetchThread( @@ -203,6 +204,7 @@ def on_player_join_server(pbid, player_data, ip, device_id): thread.start() _ba.screenmessage(settings["firstTimeJoinMsg"], color=(0.6, 0.8, 0.6), transient=True, clients=[clid]) + notification_manager.player_joined(pbid) # pdata.add_profile(pbid,d_string,d_string)