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
## --- [4.2.0] - 2023/TBD
### New features
TBD
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
### 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))
- 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)
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
theme = CharField(default="default")
cleared_notifs = CharField(default="default")
class Meta:
table_name = "users"
@ -171,6 +172,7 @@ class HelperUsers:
"roles": [],
"servers": [],
"support_logs": "",
"cleared_notifs": "",
}
user = model_to_dict(Users.get(Users.user_id == user_id))

View File

@ -579,20 +579,16 @@ class Helpers:
return version_data
@staticmethod
def get_announcements():
data = (
'[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]'
)
def get_announcements(self):
data = []
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)
except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data
def get_version_string(self):

View File

@ -726,12 +726,21 @@ class TasksManager:
def check_for_updates(self):
logger.info("Checking for Crafty updates...")
self.helper.update_available = self.helper.check_remote_version()
remote = self.helper.update_available
if self.helper.update_available:
logger.info(f"Found new version {self.helper.update_available}")
else:
logger.info(
"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...")
for user in HelperUsers.get_all_users():
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.pfp import ApiUsersUserPfpHandler
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 (
ApiCraftyConfigIndexHandler,
ApiCraftyCustomizeIndexHandler,
@ -77,6 +80,11 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler,
handler_args,
),
(
r"/api/v2/crafty/announcements/?",
ApiAnnounceIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/?",
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 json
import os
from apscheduler.jobstores.base import JobLookupError
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
@ -192,8 +193,8 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
logger.info("No active tasks found for server")
except JobLookupError as e:
logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True)
except Exception:
return self.finish_json(

View File

@ -20,11 +20,9 @@ function getDirView(event = false) {
console.log("Well that failed");
}
} else if ($("#root_files_button").hasClass("clicked")) {
path = $("#zip_server_path").val();
getTreeView(path, true);
getTreeView($("#zip_server_path").val(), true);
} else {
path = $("#file-uploaded").val();
getTreeView(path, true, true);
getTreeView($("#file-uploaded").val(), true, true);
}
}
@ -132,7 +130,7 @@ function process_tree_response(response) {
}
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(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");

View File

@ -1,27 +1,32 @@
<ul class="navbar-nav ml-auto">
<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
{% if data.get('update_available') %}
text-danger
{% end %}
"></i>
<!-- <span class="count bg-success">3</span>-->
</a>
"></i><span id="notif-count" class="badge badge-notify"></span> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
<ul style="padding-top: 10px;" id="announcements">
</ul>
</div>
</li>
<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>
</a>
</li>
<li class="nav-item dropdown user-dropdown">
<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-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="font-weight-light text-muted mb-0">Roles: </p>
{% 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>
</div>
{% 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">
<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>
</span>
{% 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 %}
{% 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 %}
<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>
</li>
</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>
function pfpError(image) {
image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png";
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>

View File

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

View File

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

View File

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

View File

@ -890,7 +890,6 @@
window.location.reload();
}
});
let doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
@ -1198,7 +1197,7 @@
let idx = document.getElementById('server_type').selectedIndex;
// get the value of the selected option
let cSelect = document.getElementById("server");
let which;
let which = {};
try {
which = document.getElementById('server_type').options[idx].value;
} catch {
@ -1234,10 +1233,10 @@
}
function serverJarChange(selectObj) {
let type_select = document.getElementById('server_jar')
let tidx = type_select.selectedIndex;
let val = type_select.options[tidx].value;
let jcSelect = "";
const type_select = document.getElementById('server_jar')
const tidx = type_select.selectedIndex;
const val = type_select.options[tidx].value;
let jcSelect = {};
if (val == 'None') {
jcSelect = document.getElementById("server_type");
while (jcSelect.options.length > 0) {
@ -1251,7 +1250,7 @@
// get the value of the selected option
let jwhich = selectObj.options[jidx].value;
// 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
jcSelect = document.getElementById("server_type");
// 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.
"""