Newer
Older
ServerToClientEvents,
} from "@sc07-canvas/lib/src/net";
import { Ban, User as UserDB } from "@prisma/client";
import { Instance } from "./Instance";
import { getClientConfig } from "../lib/SocketServer";
/**
* Represents a user ban
*
* Has implementation in here for making instance bans retroactive,
* but at time of writing, instance bans will only block new users
*/
export type IUserBan = {
id: number;
expires: Date;
publicNote: string | null;
} & (
| {
type: "user";
}
| {
type: "instance";
hostname: string;
}
);
export class User {
static instances: Map<string, User> = new Map();
sub: string;
sockets: Set<Socket<ClientToServerEvents, ServerToClientEvents>> = new Set();
private constructor(data: UserDB & { Ban: Ban | null }) {
Logger.debug("User class instansiated for " + data.sub);
this.sub = data.sub;
this.lastTimeGainStarted = data.lastTimeGainStarted;
this.undoExpires = data.undoExpires || undefined;
this.isAdmin = data.isAdmin;
this.isModerator = data.isModerator;
this.updateBanFromUserData(data).then(() => {});
this._updatedAt = Date.now();
}
async update(force: boolean = false) {
if (this.isStale() && !force) return;
const userData = await prisma.user.findFirst({
where: {
sub: this.sub,
},
include: {
Ban: true,
},
this.lastTimeGainStarted = userData.lastTimeGainStarted;
this.undoExpires = userData.undoExpires || undefined;
this.isAdmin = userData.isAdmin;
this.isModerator = userData.isModerator;
await this.updateBanFromUserData(userData);
}
private async updateBanFromUserData(userData: UserDB & { Ban: Ban | null }) {
if (userData.Ban) {
id: userData.Ban.id,
expires: userData.Ban.expiresAt,
publicNote: userData.Ban.publicNote,
type: "user",
};
} else {
// the code below is for making instance bans retroactive
//
// const instance = await this.getInstance();
// const instanceBan = await instance.getEffectiveBan();
// if (instanceBan) {
// this.ban = {
// id: instanceBan.id,
// expires: instanceBan.expiresAt,
// publicNote: instanceBan.publicNote,
// type: "instance",
// hostname: instanceBan.hostname,
// };
// }
}
}
async getInstance(): Promise<Instance> {
const [local, hostname] = this.sub.split("@");
return await Instance.fromDomain(hostname);
}
async modifyStack(modifyBy: number): Promise<any> {
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
let new_date = new Date();
if (modifyBy > 0) {
let cooldown_to_add = 0.0;
for (let i = 0; i < modifyBy; i++) {
cooldown_to_add += CanvasLib.getPixelCooldown(
this.pixelStack + i + 1,
getClientConfig()
);
}
new_date = new Date(
this.lastTimeGainStarted.valueOf() + cooldown_to_add * 1000
);
} else if (modifyBy < 0) {
const cooldown_before_change_s = CanvasLib.getPixelCooldown(
this.pixelStack + 1,
getClientConfig()
);
const cooldown_after_change_s = CanvasLib.getPixelCooldown(
this.pixelStack + 1 + modifyBy,
getClientConfig()
);
const would_gain_next_at_timestamp_ms =
this.lastTimeGainStarted.valueOf() + cooldown_before_change_s * 1000;
const time_before_next =
would_gain_next_at_timestamp_ms - Date.now().valueOf();
// To avoid issue if a negative value is present for some reason
if (time_before_next > 0) {
if (time_before_next < cooldown_after_change_s * 1000) {
new_date = new Date(
Date.now() - cooldown_after_change_s * 1000 + time_before_next
);
}
}
}
const updatedUser = await prisma.user.update({
where: { sub: this.sub },
data: {
pixelStack: { increment: modifyBy },
},
});
for (const socket of this.sockets) {
socket.emit("availablePixels", updatedUser.pixelStack);
socket.emit("pixelLastPlaced", updatedUser.lastTimeGainStarted.getTime());
}
// we just modified the user data, so we should force an update
await this.update(true);
}
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
* Set undoExpires in database and notify all user's sockets of undo ttl
*/
async setUndo(expires?: Date) {
if (expires) {
// expiration being set
await prisma.user.update({
where: { sub: this.sub },
data: {
undoExpires: expires,
},
});
for (const socket of this.sockets) {
socket.emit("undo", { available: true, expireAt: expires.getTime() });
}
} else {
// clear undo capability
await prisma.user.update({
where: { sub: this.sub },
data: {
undoExpires: undefined,
},
});
for (const socket of this.sockets) {
socket.emit("undo", { available: false });
}
}
await this.update(true);
}
/**
* Sends packet to all user's sockets with current standing information
*/
updateStanding() {
for (const socket of this.sockets) {
socket.emit("standing", {
banned: true,
until: ban.expires.toISOString(),
reason: ban.publicNote || undefined,
});
}
} else {
for (const socket of this.sockets) {
socket.emit("standing", { banned: false });
}
}
}
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
getBan<DoUpdate extends boolean = false>(
update: DoUpdate = false as DoUpdate
): ConditionalPromise<typeof this._ban, DoUpdate> {
if (update) {
return new Promise(async (res) => {
const user = await prisma.user.findFirst({
where: {
sub: this.sub,
},
include: {
Ban: true,
},
});
if (!user?.Ban) {
return res(undefined);
}
this._ban = {
type: "user",
id: user.Ban.id,
expires: user.Ban.expiresAt,
publicNote: user.Ban.publicNote,
};
res(this._ban);
}) as any;
} else {
return this._ban as any;
}
}
async ban(
expires: Date,
publicNote: string | null | undefined,
privateNote: string | null | undefined
) {
const ban = await prisma.ban.upsert({
where: {
userId: this.sub,
},
create: {
userId: this.sub,
expiresAt: expires,
publicNote,
privateNote,
},
update: {
userId: this.sub,
expiresAt: expires,
publicNote,
privateNote,
},
});
this._ban = {
id: ban.id,
type: "user",
expires,
publicNote: publicNote || null,
};
return ban;
}
async unban() {
const existing = await this.getBan(true);
if (!existing) throw new UserNotBanned();
const ban = await prisma.ban.delete({
where: { id: existing.id },
});
return ban;
}
/**
* Notifies all sockets for this user of a message
* @param alert
*/
notify(alert: IAlert) {
for (const socket of this.sockets) {
socket.emit("alert", alert);
}
}
async trackIP(ip: string) {
await prisma.iPAddress.upsert({
where: {
ip_userSub: {
ip,
userSub: this.sub,
},
},
create: {
ip,
userSub: this.sub,
lastUsedAt: new Date(),
},
update: {
ip,
userSub: this.sub,
lastUsedAt: new Date(),
},
});
}
/**
* Determine if this user data is stale and should be updated
* @see User#update
* @returns if this user data is stale
*/
private isStale() {
return Date.now() - this._updatedAt >= 1000 * 60;
}
static async fromAuthSession(auth: AuthSession): Promise<User | undefined> {
try {
const user = await this.fromSub(
auth.user.username + "@" + auth.service.instance.hostname
);
user.authSession = auth;
return user;
} catch (e) {
if (e instanceof UserNotFound) {
return undefined;
} else {
throw e;
}
}
}
static async fromSub(sub: string): Promise<User> {
if (this.instances.has(sub)) return this.instances.get(sub)!;
const userData = await prisma.user.findFirst({
where: {
sub,
},
include: {
Ban: true,
},
});
if (!userData) throw new UserNotFound();
const newUser = new User(userData);
this.instances.set(sub, newUser);
return newUser;
}
}
export class UserNotFound extends Error {
constructor() {
super();
this.name = "UserNotFound";
}
}