Skip to content

Commit 19709c8

Browse files
committed
add user inventory
1 parent 635610c commit 19709c8

File tree

10 files changed

+408
-80
lines changed

10 files changed

+408
-80
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- CreateTable
2+
CREATE TABLE "inventory_items" (
3+
"id" TEXT NOT NULL,
4+
"userId" TEXT NOT NULL,
5+
"itemId" TEXT NOT NULL,
6+
"itemType" TEXT NOT NULL,
7+
"acquiredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
9+
CONSTRAINT "inventory_items_pkey" PRIMARY KEY ("id")
10+
);
11+
12+
-- CreateIndex
13+
CREATE UNIQUE INDEX "inventory_items_userId_itemType_itemId_key" ON "inventory_items"("userId", "itemType", "itemId");
14+
15+
-- AddForeignKey
16+
ALTER TABLE "inventory_items" ADD CONSTRAINT "inventory_items_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ model User {
236236
modSuggestActions ModeratorSuggestAction[] @relation(name: "modSuggestActions")
237237
238238
moderationActions ModeratorSuggestAction?
239+
inventory InventoryItem[]
239240
240241
241242
@@unique([username, tag])
@@ -244,7 +245,19 @@ model User {
244245
@@index([tag])
245246
}
246247

248+
model InventoryItem {
249+
@@map("inventory_items")
250+
id String @id
251+
userId String
252+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
247253
254+
itemId String
255+
itemType String
256+
257+
acquiredAt DateTime @default(now())
258+
259+
@@unique([userId, itemType, itemId])
260+
}
248261

249262

250263

src/common/Bitwise.ts

Lines changed: 145 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,168 @@ export interface Bitwise {
77
}
88

99
export const USER_BADGES = {
10+
DEER_EARS_WHITE: {
11+
bit: 8388608,
12+
color: 'linear-gradient(273deg, #fb83a7, #ffffff)',
13+
textColor: '#2a1d1d',
14+
overlay: true,
15+
icon: 'pets',
16+
},
17+
DEER_EARS_HORNS_DARK: {
18+
bit: 1048576,
19+
color: 'linear-gradient(267deg, #8f8f8f, #090a25)',
20+
textColor: '#ffffff',
21+
overlay: true,
22+
icon: 'pets',
23+
},
24+
DEER_EARS_HORNS: {
25+
bit: 262144,
26+
color: 'linear-gradient(270deg, #aa4908, #ffd894)',
27+
textColor: '#321515',
28+
overlay: true,
29+
icon: 'pets',
30+
},
31+
GOAT_HORNS: {
32+
bit: 524288,
33+
color: 'linear-gradient(268deg, #cb75d7, #390a8f)',
34+
overlay: true,
35+
icon: 'pets',
36+
},
37+
GOAT_EARS_WHITE: {
38+
bit: 131072,
39+
color: 'linear-gradient(89deg, #ffecc2, #94e4ff)',
40+
textColor: '#503030',
41+
overlay: true,
42+
icon: 'pets',
43+
},
44+
WOLF_EARS: {
45+
bit: 65536,
46+
color: 'linear-gradient(90deg, #585858ff 0%, #252525ff 100%)',
47+
textColor: '#ffffff',
48+
overlay: true,
49+
icon: 'pets',
50+
},
51+
DOG_SHIBA: {
52+
bit: 32768,
53+
color: 'linear-gradient(261deg, #ffeeb3, #9e7aff)',
54+
textColor: '#2e1919',
55+
overlay: true,
56+
icon: 'sound_detection_dog_barking',
57+
},
58+
DOG_EARS_BROWN: {
59+
bit: 16384,
60+
color: 'linear-gradient(90deg, #bb7435 0%, #ffbd67ff 100%)',
61+
overlay: true,
62+
icon: 'sound_detection_dog_barking',
63+
},
64+
BUNNY_EARS_MAID: {
65+
bit: 8192,
66+
color: 'linear-gradient(100deg, #ff94e2, #ffffff)',
67+
textColor: '#2a1d1d',
68+
overlay: true,
69+
icon: 'cruelty_free',
70+
},
71+
BUNNY_EARS_BLACK: {
72+
bit: 4096,
73+
color: 'linear-gradient(90deg, #585858ff 0%, #252525ff 100%)',
74+
textColor: '#ffffff',
75+
overlay: true,
76+
icon: 'cruelty_free',
77+
},
78+
CAT_EARS_MAID: {
79+
bit: 2097152,
80+
color: 'linear-gradient(100deg, #ff94e2, #ffffff)',
81+
textColor: '#2a1d1d',
82+
overlay: true,
83+
icon: 'pets',
84+
},
85+
CAT_EARS_PURPLE: {
86+
bit: 4194304,
87+
color: 'linear-gradient(268deg, #cb75d7, #390a8f)',
88+
textColor: '#ffffff',
89+
overlay: true,
90+
icon: 'pets',
91+
},
92+
CAT_EARS_BLUE: {
93+
bit: 512,
94+
color: 'linear-gradient(90deg, #78a5ff 0%, #ffffff 100%)',
95+
overlay: true,
96+
icon: 'pets',
97+
},
98+
CAT_EARS_WHITE: {
99+
bit: 256,
100+
color: 'linear-gradient(90deg, #ffa761 0%, #ffffff 100%)',
101+
overlay: true,
102+
icon: 'pets',
103+
},
104+
FOX_EARS_GOLD: {
105+
bit: 1024,
106+
color: 'linear-gradient(90deg, #ffb100 0%, #ffffff 100%)',
107+
overlay: true,
108+
icon: 'pets',
109+
},
110+
FOX_EARS_BROWN: {
111+
bit: 2048,
112+
color: 'linear-gradient(90deg, #bb7435 0%, #ffffff 100%)',
113+
overlay: true,
114+
icon: 'pets',
115+
},
10116
FOUNDER: {
11-
name: 'Founder',
117+
removable: false,
12118
bit: 1,
13-
description: 'Creator of Nerimity',
14-
color: '#6fd894',
119+
color: 'linear-gradient(90deg, #4fffbd 0%, #4a5efc 100%)',
120+
type: 'earned',
121+
icon: 'crown',
15122
},
16123
ADMIN: {
17-
name: 'Admin',
124+
removable: false,
18125
bit: 2,
19-
description: 'Admin of Nerimity',
20-
color: '#d8a66f',
126+
color: 'linear-gradient(90deg, rgba(224,26,185,1) 0%, rgba(64,122,255,1) 100%)',
127+
type: 'earned',
128+
icon: 'verified_user',
21129
},
22-
CONTRIBUTOR: {
23-
name: 'Contributor',
24-
description: 'Helped with this project in some way',
25-
bit: 4,
26-
color: '#ffffff',
130+
MOD: {
131+
removable: false,
132+
bit: 64,
133+
color: 'linear-gradient(90deg, #57acfa 0%, #1485ed 100%)',
134+
type: 'earned',
135+
icon: 'shield',
136+
},
137+
EMO_SUPPORTER: {
138+
bit: 128,
139+
textColor: 'rgba(255,255,255,0.8)',
140+
color: 'linear-gradient(90deg, #424242 0%, #303030 100%)',
141+
type: 'earned',
142+
icon: 'favorite',
27143
},
28144
SUPPORTER: {
29-
name: 'Supporter',
30-
description: 'Supported this project by donating money',
31145
bit: 8,
32-
color: '#d86f6f',
146+
color: 'linear-gradient(90deg, rgba(235,78,209,1) 0%, rgba(243,189,247,1) 100%)',
147+
type: 'earned',
148+
icon: 'favorite',
33149
},
34-
EMO_SUPPORTER: {
35-
name: 'Emo Supporter',
36-
description: 'Supported this project by donating money',
37-
bit: 128,
150+
CONTRIBUTOR: {
151+
bit: 4,
152+
color: '#ffffff',
153+
type: 'earned',
154+
icon: 'crowdsource',
38155
},
39156
PALESTINE: {
40-
free: true,
41-
name: 'Palestine',
42-
description: 'Palestine',
43157
bit: 16,
44-
color: 'linear-gradient(90deg, rgba(224,26,185,1) 0%, rgba(64,122,255,1) 100%);',
158+
color: 'linear-gradient(90deg, red, white, green)',
159+
icon: 'volunteer_activism',
45160
},
46-
MOD: {
47-
name: 'Moderator',
48-
description: 'Moderator of Nerimity',
49-
bit: 64,
50-
credit: 'Avatar Border by upklyak on Freepik',
161+
BOT: {
162+
removable: false,
163+
bit: 32,
164+
color: 'var(--primary-color)',
165+
type: 'earned',
166+
icon: 'robot_2',
51167
},
52168
};
53169

170+
export const UserBadgesArray = Object.values(USER_BADGES);
171+
54172
export const isUserAdmin = (badge: number) => {
55173
return hasBit(badge, USER_BADGES.ADMIN.bit) || hasBit(badge, USER_BADGES.FOUNDER.bit);
56174
};

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createQueueProcessor } from '@nerimity/mimiqueue';
1717
import { deleteServer } from './services/Server';
1818
import { Prisma } from '@src/generated/prisma/client';
1919
import { getHourStart, isString } from './common/utils';
20+
import { migrateExistingBadges } from './services/User/User';
2021

2122
(Date.prototype.toJSON as unknown as (this: Date) => number) = function () {
2223
return this.getTime();
@@ -50,6 +51,7 @@ if (cluster.isPrimary) {
5051
prismaConnected = true;
5152

5253
if (env.TYPE === 'api') {
54+
migrateExistingBadges();
5355
scheduleBumpReset();
5456
vacuumSchedule();
5557
scheduleDeleteMessages();

src/routes/moderation/getUser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ async function route(req: Request, res: Response) {
1414
const user = await prisma.user.findFirst({
1515
where: { id: userId },
1616
include: {
17+
inventory: true,
1718
suspension: {
1819
include: {
1920
suspendBy: true,

src/routes/moderation/updateUser.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { authenticate } from '../../middleware/authenticate';
44
import { isModMiddleware } from './isModMiddleware';
55
import { customExpressValidatorResult, generateError } from '../../common/errorHandler';
66
import { addToObjectIfExists } from '../../common/addToObjectIfExists';
7-
import { USER_BADGES, hasBit } from '../../common/Bitwise';
7+
import { USER_BADGES, addBit, hasBit, removeBit } from '../../common/Bitwise';
88
import bcrypt from 'bcrypt';
99
import { removeUserCacheByUserIds } from '../../cache/UserCache';
1010
import { getIO } from '../../socket/socket';
@@ -13,6 +13,7 @@ import { ModAuditLogType } from '../../common/ModAuditLog';
1313
import { generateId } from '../../common/flakeId';
1414
import { checkUserPassword } from '../../services/UserAuthentication';
1515
import { Prisma } from '@src/generated/prisma/client';
16+
import { InventoryItemType } from '@src/services/User/User';
1617

1718
export function updateUser(Router: Router) {
1819
Router.post('/moderation/users/:userId', authenticate(), isModMiddleware(), route);
@@ -22,11 +23,12 @@ interface Body {
2223
email?: string;
2324
username?: string;
2425
tag?: string;
25-
badges?: number;
2626
newPassword?: string;
2727
password?: string;
2828

2929
emailConfirmed?: boolean;
30+
addedInventoryItems?: { itemType: string; itemId: string }[];
31+
removedInventoryIds?: string[];
3032
}
3133

3234
async function route(req: Request, res: Response) {
@@ -55,19 +57,41 @@ async function route(req: Request, res: Response) {
5557
username: true,
5658
tag: true,
5759
badges: true,
60+
inventory: true,
5861
},
5962
});
6063

6164
if (!user?.account && !user?.application) return res.status(404).json(generateError('User does not exist.'));
6265

63-
if (body.badges !== undefined) {
64-
const alreadyIsFounder = hasBit(user.badges, USER_BADGES.FOUNDER.bit);
65-
const updatedIsFounder = hasBit(body.badges, USER_BADGES.FOUNDER.bit);
66+
let newBadges = user.badges;
67+
let modifiedFounder = false;
68+
69+
if (body.addedInventoryItems?.length) {
70+
body.addedInventoryItems.forEach((i) => {
71+
if (i.itemType === InventoryItemType.BADGE) {
72+
const bit = parseInt(i.itemId);
73+
const valid = Object.values(USER_BADGES).find((b) => b.bit === bit);
74+
if (!valid) return;
75+
if (bit === USER_BADGES.FOUNDER.bit) modifiedFounder = true;
76+
newBadges = addBit(newBadges, bit);
77+
}
78+
});
79+
}
80+
if (body.removedInventoryIds?.length) {
81+
body.removedInventoryIds.forEach((id) => {
82+
const inventory = user.inventory.find((i) => i.id === id);
83+
if (inventory?.itemType === InventoryItemType.BADGE) {
84+
const bit = parseInt(inventory.itemId);
85+
if (bit === USER_BADGES.FOUNDER.bit) modifiedFounder = true;
86+
newBadges = removeBit(newBadges, bit);
87+
}
88+
});
89+
}
6690

67-
if (alreadyIsFounder !== updatedIsFounder) {
68-
return res.status(403).json(generateError(`Cannot modify the ${USER_BADGES.FOUNDER.name} badge`));
69-
}
91+
if (modifiedFounder) {
92+
return res.status(403).json(generateError('You cannot modify the Founder badge.'));
7093
}
94+
7195
if (body.tag || body.username) {
7296
const exists = await prisma.user.findFirst({
7397
where: {
@@ -99,6 +123,28 @@ async function route(req: Request, res: Response) {
99123
const newUser = await prisma.user.update({
100124
where: { id: userId },
101125
data: {
126+
...(body.addedInventoryItems?.length
127+
? {
128+
inventory: {
129+
upsert: body.addedInventoryItems.map((i) => ({
130+
create: {
131+
id: generateId(),
132+
itemType: i.itemType,
133+
itemId: i.itemId,
134+
},
135+
update: {},
136+
where: { userId_itemType_itemId: { userId: userId!, itemType: i.itemType, itemId: i.itemId } },
137+
})),
138+
},
139+
}
140+
: {}),
141+
...(body.removedInventoryIds?.length
142+
? {
143+
inventory: {
144+
deleteMany: { userId: userId!, id: { in: body.removedInventoryIds } },
145+
},
146+
}
147+
: {}),
102148
...(Object.keys(updateAccount).length
103149
? {
104150
account: { update: updateAccount },
@@ -107,7 +153,7 @@ async function route(req: Request, res: Response) {
107153

108154
...addToObjectIfExists('username', body.username),
109155
...addToObjectIfExists('tag', body.tag),
110-
...addToObjectIfExists('badges', body.badges),
156+
...addToObjectIfExists('badges', newBadges),
111157
},
112158
include: {
113159
account: {

src/routes/users/Router.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@ import { userExternalEmbedCreate } from './userExternalEmbedCreate';
2929
import { userExternalEmbedDelete } from './userExternalEmbedDelete';
3030
import { userExternalEmbedGet } from './userExternalEmbedGet';
3131
import { userExternalEmbedRawGet } from './userExternalEmbedRawGet';
32+
import { userInventoryGet } from './userInventoryGet';
3233

3334
const UsersRouter = Router();
3435

36+
userInventoryGet(UsersRouter);
37+
3538
userExternalEmbedCreate(UsersRouter);
3639
userExternalEmbedDelete(UsersRouter);
3740
userExternalEmbedGet(UsersRouter);

0 commit comments

Comments
 (0)