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

View file

@ -10,6 +10,7 @@ export default class User {
public PasswordSalt:string;
public PasswordHash:string;
public APIKey:string;
public HasUsedClient:boolean;
public CreatedByUserId:number;
public CreatedDatetime:Date;
public LastModifiedByUserId?:number;
@ -18,31 +19,16 @@ export default class User {
public DeletedDatetime?:Date;
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) {
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.UserLevel = UserLevel.Unknown;
this.Username = "";
this.PasswordHash = "";
this.PasswordSalt = "";
this.APIKey = "";
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
public constructor() {
this.Id = Number.MIN_VALUE;
this.UserLevel = UserLevel.Unknown;
this.Username = "";
this.PasswordHash = "";
this.PasswordSalt = "";
this.APIKey = "";
this.HasUsedClient = false;
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
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 { join } from "path";
import WsData from "./objects/WsData";
import BadgeCache from "./objects/BadgeCache";
import Badge from "./entities/Badge";
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);
BadgeCache.RefreshCache();
// Web stuff
const fastify = Fastify({
@ -237,9 +241,10 @@ websocketServer.on("connection", (socket) => {
}
let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", "");
if (page === "index") {
page = "";
if (page.endsWith("/index")) {
page = page.replace("/index", "/");
}
let lengthOfUsernames = 0;
const usersOnPage = new Array<RemoteUser>();
await users.forEach(otherUser => {
@ -261,6 +266,40 @@ websocketServer.on("connection", (socket) => {
user.send(usersToSend.toBuffer());
sendGroupUpdate(user);
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;
}
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 { WebSocket } from "ws";
import { MessageType } from "../enums/MessageType";
export default class RemoteUser {
private static USER_IDS = 0;
@ -47,4 +49,9 @@ export default class RemoteUser {
this.messagesOut.add(1);
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) {
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)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, 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
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, 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.PasswordSalt = dbUser.PasswordSalt;
user.APIKey = dbUser.APIKey;
user.HasUsedClient = dbUser.HasUsedClient === 1;
user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.LastModifiedByUserId = dbUser.LastModifiedByUserId;

View file

@ -1,6 +1,7 @@
import { Console } from "hsconsole";
import Badge from "../entities/Badge";
import BadgeRepo from "../repos/BadgeRepo";
import BadgeCache from "../objects/BadgeCache";
export default abstract class BadgeService {
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.ForUrl = forUrl;
return await BadgeRepo.insertUpdate(badge);
badge = await BadgeRepo.insertUpdate(badge);
await BadgeCache.RefreshCache();
return badge;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;

View file

@ -5,6 +5,8 @@ import PasswordUtility from "../utilities/PasswordUtility";
import UserParty from "../entities/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo";
import { UserLevel } from "../enums/UserLevel";
import UserBadgeRepo from "../repos/UserBadgeRepo";
import UserBadge from "../entities/UserBadge";
export default class UserService {
public static async AuthenticateUser(username:string, password:string) {
@ -227,4 +229,44 @@ export default class UserService {
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;
}
}
}