implement url badges and some mp badges

This commit is contained in:
Holly Stubbs 2024-10-03 00:32:38 +01:00
parent f72bdd3f27
commit 9dfc7e4100
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
10 changed files with 223 additions and 38 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name MultiProbe // @name MultiProbe
// @namespace https://*.angusnicneven.com/* // @namespace https://*.angusnicneven.com/*
// @version 20241002.4 // @version 20241003.0
// @description Probe with friends! // @description Probe with friends!
// @author tgpholly // @author tgpholly
// @match https://*.angusnicneven.com/* // @match https://*.angusnicneven.com/*
@ -55,7 +55,7 @@ console.log("[MP] MultiProbe init");
'use strict'; 'use strict';
// Make sure to change the userscript version too!!!!!!!!!! // Make sure to change the userscript version too!!!!!!!!!!
const USERSCRIPT_VERSION_RAW = "20241002.4"; const USERSCRIPT_VERSION_RAW = "20241003.0";
const USERSCRIPT_VERSION = parseInt(USERSCRIPT_VERSION_RAW.replace(".", "")); const USERSCRIPT_VERSION = parseInt(USERSCRIPT_VERSION_RAW.replace(".", ""));
if (!continueRunningScript) { if (!continueRunningScript) {
@ -889,9 +889,9 @@ mp_button {
} }
case MessageType.BadgeUnlock: case MessageType.BadgeUnlock:
{ {
const badgeTitle = reader.readShortString(); const badgeTitle = reader.readString16();
const badgeDescription = reader.readString(); const badgeDescription = reader.readString16();
const badgeIconUrl = reader.readShortString(); const badgeIconUrl = reader.readString16();
createBadgePopup(badgeTitle, badgeDescription, badgeIconUrl); createBadgePopup(badgeTitle, badgeDescription, badgeIconUrl);
} }
@ -1095,7 +1095,7 @@ mp_button {
popup.appendChild(badgeTitle); popup.appendChild(badgeTitle);
const badgeDescription = document.createElement("mp_text"); const badgeDescription = document.createElement("mp_text");
badgeDescription.innerText = description; badgeDescription.innerHTML = description;
badgeDescription.style = "position:absolute;top:1.1rem;left:calc(1rem + 32px)"; badgeDescription.style = "position:absolute;top:1.1rem;left:calc(1rem + 32px)";
popup.appendChild(badgeDescription); popup.appendChild(badgeDescription);

View file

@ -10,6 +10,7 @@ export default class User {
public PasswordSalt:string; public PasswordSalt:string;
public PasswordHash:string; public PasswordHash:string;
public APIKey:string; public APIKey:string;
public HasUsedClient:boolean;
public CreatedByUserId:number; public CreatedByUserId:number;
public CreatedDatetime:Date; public CreatedDatetime:Date;
public LastModifiedByUserId?:number; public LastModifiedByUserId?:number;
@ -18,31 +19,16 @@ export default class User {
public DeletedDatetime?:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
public constructor(id?:number, userLevel?:UserLevel, username?:string, passwordSalt?:string, passwordHash?:string, apiKey?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { public constructor() {
if (typeof(id) == "number" && typeof(userLevel) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(apiKey) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
this.Id = id;
this.UserLevel = userLevel;
this.Username = username;
this.PasswordHash = passwordHash;
this.PasswordSalt = passwordSalt;
this.APIKey = apiKey;
this.CreatedByUserId = createdByUserId;
this.CreatedDatetime = createdDateTime;
this.LastModifiedByUserId = lastModifiedByUserId;
this.LastModifiedDatetime = lastModifiedDatetime;
this.DeletedByUserId = deletedByUserId;
this.DeletedDatetime = deletedDatetime;
this.IsDeleted = isDeleted;
} else {
this.Id = Number.MIN_VALUE; this.Id = Number.MIN_VALUE;
this.UserLevel = UserLevel.Unknown; this.UserLevel = UserLevel.Unknown;
this.Username = ""; this.Username = "";
this.PasswordHash = ""; this.PasswordHash = "";
this.PasswordSalt = ""; this.PasswordSalt = "";
this.APIKey = ""; this.APIKey = "";
this.HasUsedClient = false;
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;
} }
}
} }

View file

@ -0,0 +1,21 @@
export default class UserBadge {
public Id: number;
public UserId: number;
public BadgeId: number;
public CreatedByUserId: number;
public CreatedDatetime: Date;
public LastModifiedByUserId?: number;
public LastModifiedDatetime?: Date;
public DeletedByUserId?: number;
public DeletedDatetime?: Date;
public IsDeleted: boolean;
public constructor() {
this.Id = Number.MIN_VALUE;
this.UserId = Number.MIN_VALUE;
this.BadgeId = Number.MIN_VALUE;
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
}

View file

@ -50,6 +50,8 @@ import PartyService from "./services/PartyService";
import AdminController_Auth$Admin from "./controller/AdminController"; import AdminController_Auth$Admin from "./controller/AdminController";
import { join } from "path"; import { join } from "path";
import WsData from "./objects/WsData"; import WsData from "./objects/WsData";
import BadgeCache from "./objects/BadgeCache";
import Badge from "./entities/Badge";
Console.customHeader(`MultiProbe server started at ${new Date()}`); Console.customHeader(`MultiProbe server started at ${new Date()}`);
@ -58,6 +60,8 @@ WsData.users = users;
new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);
BadgeCache.RefreshCache();
// Web stuff // Web stuff
const fastify = Fastify({ const fastify = Fastify({
@ -237,9 +241,10 @@ websocketServer.on("connection", (socket) => {
} }
let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", ""); let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", "");
if (page === "index") { if (page.endsWith("/index")) {
page = ""; page = page.replace("/index", "/");
} }
let lengthOfUsernames = 0; let lengthOfUsernames = 0;
const usersOnPage = new Array<RemoteUser>(); const usersOnPage = new Array<RemoteUser>();
await users.forEach(otherUser => { await users.forEach(otherUser => {
@ -261,6 +266,40 @@ websocketServer.on("connection", (socket) => {
user.send(usersToSend.toBuffer()); user.send(usersToSend.toBuffer());
sendGroupUpdate(user); sendGroupUpdate(user);
updateConnectionMetrics(); updateConnectionMetrics();
const badgeForPage = BadgeCache.UrlBadges.get(page);
if (badgeForPage && await UserService.UnlockBadgeIfNotUnlocked(dbUser.Id, badgeForPage.Id)) {
user.sendBadge(badgeForPage.Name, badgeForPage.Description, badgeForPage.ImageUrl);
}
const goalBadgeKeys = BadgeCache.GoalBadges.keys;
let badge:Badge;
for (const key of goalBadgeKeys) {
switch (key) {
case "use_first_time":
{
if (!dbUser.HasUsedClient) {
await UserService.SaveUserClientLoginFlag(dbUser.Id);
dbUser.HasUsedClient = true;
badge = BadgeCache.GoalBadges.get(key)!;
if (await UserService.UnlockBadgeIfNotUnlocked(dbUser.Id, badge.Id)) {
user.sendBadge(badge.Name, badge.Description, badge.ImageUrl);
}
}
break;
}
case "first_time_party":
{
if (dbUserParty) {
badge = BadgeCache.GoalBadges.get(key)!;
if (await UserService.UnlockBadgeIfNotUnlocked(dbUser.Id, badge.Id)) {
user.sendBadge(badge.Name, badge.Description, badge.ImageUrl);
}
}
break;
}
}
}
break; break;
} }
case MessageType.CursorPos: case MessageType.CursorPos:

View file

@ -0,0 +1,30 @@
import Badge from "../entities/Badge";
import FunkyArray from "funky-array";
import BadgeService from "../services/BadgeService";
export default abstract class BadgeCache {
public static UrlBadges:FunkyArray<string, Badge>;
public static GoalBadges:FunkyArray<string, Badge>;
public static async RefreshCache() {
const badges = await BadgeService.LoadAll();
const urlBadges = new FunkyArray<string, Badge>();
const goalBadges = new FunkyArray<string, Badge>();
for (const badge of badges) {
const urlParts = badge.ForUrl.split("://");
let url = urlParts[1].replace("www.", "").toLowerCase().replace(".htm", "").replace(".html", "");
if (url.endsWith("/index")) {
url = url.replace("/index", "/");
}
if (urlParts[0] === "mp") {
goalBadges.set(url, badge);
} else {
urlBadges.set(url, badge);
}
}
BadgeCache.UrlBadges = urlBadges;
BadgeCache.GoalBadges = goalBadges;
}
}

View file

@ -1,5 +1,7 @@
import { createWriter, Endian } from "bufferstuff";
import IMetric from "simple-prom/lib/interfaces/IMetric"; import IMetric from "simple-prom/lib/interfaces/IMetric";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { MessageType } from "../enums/MessageType";
export default class RemoteUser { export default class RemoteUser {
private static USER_IDS = 0; private static USER_IDS = 0;
@ -47,4 +49,9 @@ export default class RemoteUser {
this.messagesOut.add(1); this.messagesOut.add(1);
this.socket.send(data); this.socket.send(data);
} }
sendBadge(name:string, description:string, imageUrl:string) {
const formattedDescription = description.replaceAll("\r", "").replaceAll("\n", "<br>");
this.send(createWriter(Endian.LE, 7 + name.length * 2 + formattedDescription.length * 2 + imageUrl.length * 2).writeByte(MessageType.BadgeUnlock).writeString16(name).writeString16(formattedDescription).writeString16(imageUrl).toBuffer());
}
} }

View file

@ -0,0 +1,54 @@
import UserBadge from "../entities/UserBadge";
import Database from "../objects/Database";
import RepoBase from "./RepoBase";
export default abstract class UserBadgeRepo {
public static async selectById(id:number) {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE Id = ? AND IsDeleted = 0 LIMIT 1", [id]);
if (dbUserBadge == null || dbUserBadge.length === 0) {
return null;
} else {
const userBadge = new UserBadge();
populateUserBadgeFromDB(userBadge, dbUserBadge[0]);
return userBadge;
}
}
public static async selectByUserIdBadgeId(userId:number, badgeId:number) {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND BadgeId = ? AND IsDeleted = 0 LIMIT 1", [ userId, badgeId ]);
if (dbUserBadge == null || dbUserBadge.length === 0) {
return null;
} else {
const userBadge = new UserBadge();
populateUserBadgeFromDB(userBadge, dbUserBadge[0]);
return userBadge;
}
}
public static async insertUpdate(userBadge:UserBadge) {
if (userBadge.Id === Number.MIN_VALUE) {
userBadge.Id = (await Database.Instance.query("INSERT UserBadge (UserId, BadgeId, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
userBadge.UserId, userBadge.BadgeId, userBadge.CreatedByUserId, userBadge.CreatedDatetime.getTime(), userBadge.LastModifiedByUserId ?? null, userBadge.LastModifiedDatetime?.getTime() ?? null, userBadge.DeletedByUserId ?? null, userBadge.DeletedDatetime?.getTime() ?? null, Number(userBadge.IsDeleted)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE UserBadge SET UserId = ?, BadgeId = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
userBadge.UserId, userBadge.BadgeId, userBadge.CreatedByUserId, userBadge.CreatedDatetime.getTime(), userBadge.LastModifiedByUserId ?? null, userBadge.LastModifiedDatetime?.getTime() ?? null, userBadge.DeletedByUserId ?? null, userBadge.DeletedDatetime?.getTime() ?? null, Number(userBadge.IsDeleted), userBadge.Id
]);
}
return userBadge;
}
}
function populateUserBadgeFromDB(userBadge:UserBadge, dbUser:any) {
userBadge.Id = dbUser.Id;
userBadge.UserId = dbUser.UserId;
userBadge.BadgeId = dbUser.BadgeId;
userBadge.CreatedByUserId = dbUser.CreatedByUserId;
userBadge.CreatedDatetime = new Date(dbUser.CreatedDatetime);
userBadge.LastModifiedByUserId = dbUser.LastModifiedByUserId;
userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
userBadge.DeletedByUserId = dbUser.DeletedByUserId;
userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
userBadge.IsDeleted = dbUser.IsDeleted === 1;
}

View file

@ -57,12 +57,12 @@ export default class UserRepo {
public static async insertUpdate(user:User) { public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) { if (user.Id === Number.MIN_VALUE) {
user.Id = (await Database.Instance.query("INSERT User (UserLevelId, Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ user.Id = (await Database.Instance.query("INSERT User (UserLevelId, Username, PasswordHash, PasswordSalt, HasUsedClient, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted)
]))[0]["Id"]; ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, HasUsedClient = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, Number(user.HasUsedClient), user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
]); ]);
} }
@ -77,6 +77,7 @@ function populateUserFromDB(user:User, dbUser:any) {
user.PasswordHash = dbUser.PasswordHash; user.PasswordHash = dbUser.PasswordHash;
user.PasswordSalt = dbUser.PasswordSalt; user.PasswordSalt = dbUser.PasswordSalt;
user.APIKey = dbUser.APIKey; user.APIKey = dbUser.APIKey;
user.HasUsedClient = dbUser.HasUsedClient === 1;
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime); user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;

View file

@ -1,6 +1,7 @@
import { Console } from "hsconsole"; import { Console } from "hsconsole";
import Badge from "../entities/Badge"; import Badge from "../entities/Badge";
import BadgeRepo from "../repos/BadgeRepo"; import BadgeRepo from "../repos/BadgeRepo";
import BadgeCache from "../objects/BadgeCache";
export default abstract class BadgeService { export default abstract class BadgeService {
public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string) { public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string) {
@ -20,7 +21,11 @@ export default abstract class BadgeService {
badge.ImageUrl = imageUrl; badge.ImageUrl = imageUrl;
badge.ForUrl = forUrl; badge.ForUrl = forUrl;
return await BadgeRepo.insertUpdate(badge); badge = await BadgeRepo.insertUpdate(badge);
await BadgeCache.RefreshCache();
return badge;
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e; throw e;

View file

@ -5,6 +5,8 @@ import PasswordUtility from "../utilities/PasswordUtility";
import UserParty from "../entities/UserParty"; import UserParty from "../entities/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo"; import UserPartyRepo from "../repos/UserPartyRepo";
import { UserLevel } from "../enums/UserLevel"; import { UserLevel } from "../enums/UserLevel";
import UserBadgeRepo from "../repos/UserBadgeRepo";
import UserBadge from "../entities/UserBadge";
export default class UserService { export default class UserService {
public static async AuthenticateUser(username:string, password:string) { public static async AuthenticateUser(username:string, password:string) {
@ -227,4 +229,44 @@ export default class UserService {
throw e; throw e;
} }
} }
public static async UnlockBadgeIfNotUnlocked(currentUserId:number, badgeId:number) {
try {
let userBadge = await UserBadgeRepo.selectByUserIdBadgeId(currentUserId, badgeId);
if (userBadge) {
return false;
}
userBadge = new UserBadge();
userBadge.UserId = currentUserId;
userBadge.BadgeId = badgeId;
userBadge.CreatedByUserId = currentUserId;
userBadge.CreatedDatetime = new Date();
await UserBadgeRepo.insertUpdate(userBadge)
return true;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async SaveUserClientLoginFlag(currentUserId:number) {
try {
const user = await UserRepo.selectById(currentUserId);
if (!user) {
return null;
}
user.HasUsedClient = true;
user.LastModifiedByUserId = currentUserId;
user.LastModifiedDatetime = new Date();
return await UserRepo.insertUpdate(user);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }