Merge branch 'feature/arcadia-notifications' into 'dev'

Add Arcadia Notifications to Front end

See merge request crafty-controller/crafty-4!621
This commit is contained in:
Iain Powrie 2023-09-05 01:44:53 +00:00
commit 44653f1a67
No known key found for this signature in database
14 changed files with 289 additions and 45 deletions

View File

@ -1,7 +1,7 @@
# Changelog # Changelog
## --- [4.2.0] - 2023/TBD ## --- [4.2.0] - 2023/TBD
### New features ### New features
TBD - Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
### Bug fixes ### Bug fixes
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607)) - PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612)) - Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))

View File

@ -45,6 +45,7 @@ class Users(BaseModel):
manager = IntegerField(default=None, null=True) manager = IntegerField(default=None, null=True)
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png") pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
theme = CharField(default="default") theme = CharField(default="default")
cleared_notifs = CharField(default="default")
class Meta: class Meta:
table_name = "users" table_name = "users"
@ -171,6 +172,7 @@ class HelperUsers:
"roles": [], "roles": [],
"servers": [], "servers": [],
"support_logs": "", "support_logs": "",
"cleared_notifs": "",
} }
user = model_to_dict(Users.get(Users.user_id == user_id)) user = model_to_dict(Users.get(Users.user_id == user_id))

View File

@ -579,20 +579,16 @@ class Helpers:
return version_data return version_data
@staticmethod def get_announcements(self):
def get_announcements(): data = []
data = (
'[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]'
)
try: try:
response = requests.get("https://craftycontrol.com/notify.json", timeout=2) response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content) data = json.loads(response.content)
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}") logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data return data
def get_version_string(self): def get_version_string(self):

View File

@ -726,12 +726,21 @@ class TasksManager:
def check_for_updates(self): def check_for_updates(self):
logger.info("Checking for Crafty updates...") logger.info("Checking for Crafty updates...")
self.helper.update_available = self.helper.check_remote_version() self.helper.update_available = self.helper.check_remote_version()
remote = self.helper.update_available
if self.helper.update_available: if self.helper.update_available:
logger.info(f"Found new version {self.helper.update_available}") logger.info(f"Found new version {self.helper.update_available}")
else: else:
logger.info( logger.info(
"No updates found! You are on the most up to date Crafty version." "No updates found! You are on the most up to date Crafty version."
) )
if self.helper.update_available:
self.helper.update_available = {
"id": str(remote),
"title": f"{remote} Update Available",
"date": "",
"desc": "Release notes are available by clicking this notification.",
"link": "https://gitlab.com/crafty-controller/crafty-4/-/releases",
}
logger.info("Refreshing Gravatar PFPs...") logger.info("Refreshing Gravatar PFPs...")
for user in HelperUsers.get_all_users(): for user in HelperUsers.get_all_users():
if user.email: if user.email:

View File

@ -52,6 +52,9 @@ from app.classes.web.routes.api.users.user.permissions import (
from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
from app.classes.web.routes.api.crafty.announcements.index import (
ApiAnnounceIndexHandler,
)
from app.classes.web.routes.api.crafty.config.index import ( from app.classes.web.routes.api.crafty.config.index import (
ApiCraftyConfigIndexHandler, ApiCraftyConfigIndexHandler,
ApiCraftyCustomizeIndexHandler, ApiCraftyCustomizeIndexHandler,
@ -77,6 +80,11 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler, ApiAuthInvalidateTokensHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/crafty/announcements/?",
ApiAnnounceIndexHandler,
handler_args,
),
( (
r"/api/v2/crafty/config/?", r"/api/v2/crafty/config/?",
ApiCraftyConfigIndexHandler, ApiCraftyConfigIndexHandler,

View File

@ -0,0 +1,110 @@
import logging
import json
from jsonschema import ValidationError, validate
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
notif_schema = {
"type": "object",
"properties": {
"id": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiAnnounceIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
data = self.helper.get_announcements()
cleared = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
res = [d.get("id", None) for d in data]
# remove notifs that are no longer in Crafty.
for item in cleared[:]:
if item not in res:
cleared.remove(item)
updata = {"cleared_notifs": ",".join(cleared)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
if len(cleared) > 0:
for item in data[:]:
if item["id"] in cleared:
data.remove(item)
self.finish_json(
200,
{
"status": "ok",
"data": data,
},
)
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, notif_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
announcements = self.helper.get_announcements()
res = [d.get("id", None) for d in announcements]
cleared_notifs = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
# remove notifs that are no longer in Crafty.
for item in cleared_notifs[:]:
if item not in res:
cleared_notifs.remove(item)
if str(data["id"]) in str(res):
cleared_notifs.append(data["id"])
else:
self.finish_json(200, {"status": "error", "error": "INVALID_DATA"})
return
updata = {"cleared_notifs": ",".join(cleared_notifs)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
self.finish_json(
200,
{
"status": "ok",
"data": {},
},
)

View File

@ -1,6 +1,7 @@
import logging import logging
import json import json
import os import os
from apscheduler.jobstores.base import JobLookupError
from jsonschema import validate from jsonschema import validate
from jsonschema.exceptions import ValidationError from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer from app.classes.models.server_permissions import EnumPermissionsServer
@ -192,8 +193,8 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
# remove old server's tasks # remove old server's tasks
try: try:
self.tasks_manager.remove_all_server_tasks(server_id) self.tasks_manager.remove_all_server_tasks(server_id)
except: except JobLookupError as e:
logger.info("No active tasks found for server") logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True) self.controller.remove_server(server_id, True)
except Exception: except Exception:
return self.finish_json( return self.finish_json(

View File

@ -20,11 +20,9 @@ function getDirView(event = false) {
console.log("Well that failed"); console.log("Well that failed");
} }
} else if ($("#root_files_button").hasClass("clicked")) { } else if ($("#root_files_button").hasClass("clicked")) {
path = $("#zip_server_path").val(); getTreeView($("#zip_server_path").val(), true);
getTreeView(path, true);
} else { } else {
path = $("#file-uploaded").val(); getTreeView($("#file-uploaded").val(), true, true);
getTreeView(path, true, true);
} }
} }
@ -132,7 +130,7 @@ function process_tree_response(response) {
} }
function getToggleMain(event) { function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path'); const path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block"); document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down"); document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret"); document.getElementById(path + "span").classList.toggle("tree-caret");

View File

@ -1,27 +1,32 @@
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link count-indicator"> <a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
aria-expanded="false">
<i class="fas fa-broadcast-tower <i class="fas fa-broadcast-tower
{% if data.get('update_available') %} {% if data.get('update_available') %}
text-danger text-danger
{% end %} {% end %}
"></i> "></i><span id="notif-count" class="badge badge-notify"></span> </a>
<!-- <span class="count bg-success">3</span>--> <div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
</a> <ul style="padding-top: 10px;" id="announcements">
</ul>
</div>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link count-indicator" href="/panel/panel_config"> <a class="nav-link" href="/panel/panel_config">
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
</a> </a>
</li> </li>
<li class="nav-item dropdown user-dropdown"> <li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a> <img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown"> <div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center"> <div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> <img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p> <p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p> <p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %} {% for r in data['user_role'] %}
@ -33,27 +38,130 @@
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p> <p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div> </div>
{% if data['user_data']['preparing'] %} {% if data['user_data']['preparing'] %}
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}<br><br></span> <span class="dropdown-item" id="support_progress"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress"> <span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;"> <div class="support_progress" style="height: 15px; width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> <div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div> </div>
</span> </span>
{% else %} {% else %}
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a> <a class="dropdown-item" id="support_logs"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}</i></a>
{% end %} {% end %}
{% if data['superuser'] %} {% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a> <a class="dropdown-item" href="/panel/activity_logs"><i
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
'activityLog', data['lang']) }}</a>
{% end %} {% end %}
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a> <a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
translate('notify', 'logout', data['lang']) }}</a>
</div> </div>
</li> </li>
</ul> </ul>
<style>
.badge-notify {
background: var(--purple);
position: absolute;
-moz-transform: translate(-70%, -70%);
/* For Firefox */
-ms-transform: translate(-70%, -70%);
/* for IE */
-webkit-transform: translate(-70%, -70%);
/* For Safari, Chrome, iOS */
-o-transform: translate(-70%, -70%);
}
.clear-button:hover {
cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.notif-div::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.notif-div {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
<script> <script>
function pfpError(image) { function pfpError(image) {
image.onerror = ""; image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png"; image.src = "/static/assets/images/faces-clipart/pic-3.png";
return true; return true;
} }
function updateAnnouncements(data) {
console.log(data)
let text = "";
for (let value of data) {
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
}
if (data.length > 0) {
localStorage.setItem("notif-count", data.length);
$("#notif-count").html(data.length);
$("#announcements").html(text);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`);
}
$(".clear-button").on("click", function (event) {
console.log("CLEAR BUTTON")
let uuid = event.target.getAttribute("data-id");
$(`#${uuid}`).remove();
send_clear(uuid);
let notif_count = localStorage.getItem("notif-count") - 1;
if (notif_count > 0) {
localStorage.setItem("notif-count", notif_count);
$("#notif-count").html(notif_count);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`)
$("#notif-count").html("");
}
});
}
async function getAnnouncements() {
var token = getCookie("_xsrf");
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
updateAnnouncements(responseData.data)
} else {
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
}
}
async function send_clear(uuid) {
var token = getCookie("_xsrf");
let body = JSON.stringify({ "id": uuid });
console.log(body)
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
},
body: body,
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
return
} else {
bootbox.alert(responseData.error)
}
}
$(document).ready(function () {
getAnnouncements();
})
</script> </script>

View File

@ -168,10 +168,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ <button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
translate('serverBackups', 'cancel', data['lang']) }}</button> <button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -201,8 +201,8 @@
let responseData = await res.json(); let responseData = await res.json();
let html = `` let html = ``
if (responseData.status === "ok") { if (responseData.status === "ok") {
for (let i = 0; i < responseData.data.length; i++) { for (let value of responseData.data) {
html += `<span class='box'>${responseData.data[i]}<br /></span>` html += `<span class='box'>${value}<br /></span>`
} }
console.log('Got Log From Server') console.log('Got Log From Server')
$('#virt_console').html(html); $('#virt_console').html(html);

View File

@ -240,8 +240,8 @@
let responseData = await res.json(); let responseData = await res.json();
let html = `` let html = ``
if (responseData.status === "ok") { if (responseData.status === "ok") {
for (let i = 0; i < responseData.data.length; i++) { for (let value of responseData.data) {
html += `<span class='box'>${responseData.data[i]}<br /></span>` html += `<span class='box'>${value}<br /></span>`
} }
console.log('Got Log From Server') console.log('Got Log From Server')
$('#virt_console').html(html); $('#virt_console').html(html);
@ -385,7 +385,6 @@
} }
$(document).ready(() => { $(document).ready(() => {
let scrolled = false;
$('#virt_console').on('scroll', chkScroll); $('#virt_console').on('scroll', chkScroll);
$('#to-bottom').on('click', scrollToBottom) $('#to-bottom').on('click', scrollToBottom)
}); });

View File

@ -890,7 +890,6 @@
window.location.reload(); window.location.reload();
} }
}); });
let doUpload = false;
} }
}, false); }, false);
xmlHttpRequest.addEventListener('error', (e) => { xmlHttpRequest.addEventListener('error', (e) => {
@ -1198,7 +1197,7 @@
let idx = document.getElementById('server_type').selectedIndex; let idx = document.getElementById('server_type').selectedIndex;
// get the value of the selected option // get the value of the selected option
let cSelect = document.getElementById("server"); let cSelect = document.getElementById("server");
let which; let which = {};
try { try {
which = document.getElementById('server_type').options[idx].value; which = document.getElementById('server_type').options[idx].value;
} catch { } catch {
@ -1234,10 +1233,10 @@
} }
function serverJarChange(selectObj) { function serverJarChange(selectObj) {
let type_select = document.getElementById('server_jar') const type_select = document.getElementById('server_jar')
let tidx = type_select.selectedIndex; const tidx = type_select.selectedIndex;
let val = type_select.options[tidx].value; const val = type_select.options[tidx].value;
let jcSelect = ""; let jcSelect = {};
if (val == 'None') { if (val == 'None') {
jcSelect = document.getElementById("server_type"); jcSelect = document.getElementById("server_type");
while (jcSelect.options.length > 0) { while (jcSelect.options.length > 0) {
@ -1251,7 +1250,7 @@
// get the value of the selected option // get the value of the selected option
let jwhich = selectObj.options[jidx].value; let jwhich = selectObj.options[jidx].value;
// use the selected option value to retrieve the list of items from the serverTypesLists array // use the selected option value to retrieve the list of items from the serverTypesLists array
jcList = Object.keys(serverTypesLists[jwhich]); let jcList = Object.keys(serverTypesLists[jwhich]);
// get the country select element via its known id // get the country select element via its known id
jcSelect = document.getElementById("server_type"); jcSelect = document.getElementById("server_type");
// remove the current options from the country select // remove the current options from the country select

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("users", cleared_notifs=peewee.CharField(default=""))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("users", ["cleared_notifs"])
"""
Write your rollback migrations here.
"""