Skip to content

Commit 681efaa

Browse files
Some further work
1 parent 67f955f commit 681efaa

7 files changed

Lines changed: 223 additions & 50 deletions

File tree

165 KB
Loading
339 KB
Binary file not shown.

src/api/routes/collectibles-shop.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
import { route } from "@spacebar/api";
2020
import { Request, Response, Router } from "express";
21-
import { CollectiblesShopResponse } from "@spacebar/schemas";
21+
import { Config } from "@spacebar/util";
22+
import { CollectiblesCategoryItem, CollectiblesShopResponse, ItemRowShopBlock } from "@spacebar/schemas";
2223

2324
const router = Router({ mergeParams: true });
2425

@@ -33,10 +34,34 @@ router.get(
3334
},
3435
}),
3536
(req: Request, res: Response) => {
36-
res.send({
37-
shop_blocks: [],
38-
categories: [],
39-
} as CollectiblesShopResponse);
37+
const { endpointPublic: publicCdnEndpoint } = Config.get().cdn;
38+
res.send({ shop_blocks: [], categories: [] });
39+
// res.send({
40+
// shop_blocks: [
41+
// {
42+
// type: 0,
43+
// banner_asset: {
44+
// animated: null,
45+
// static: `${publicCdnEndpoint}/content/store/banners/main-store-banner.png`,
46+
// },
47+
// summary: "Welcome! Don't go alone, take this! :)",
48+
// category_sku_id: "spacebarshop",
49+
// name: "Spacebar",
50+
// category_store_listing_id: "a",
51+
// logo_url: "",
52+
// unpublished_at: null,
53+
// ranked_sku_ids: [],
54+
// },
55+
// ],
56+
// categories: [
57+
// {
58+
// sku_id: "spacebarshop",
59+
// name: "Spacebar shop category",
60+
// summary: "Spacebar shop category items",
61+
//
62+
// }
63+
// ],
64+
// } as CollectiblesShopResponse);
4065
},
4166
);
4267

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
3+
Copyright (C) 2025 Spacebar and Spacebar Contributors
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU Affero General Public License as published
7+
by the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU Affero General Public License for more details.
14+
15+
You should have received a copy of the GNU Affero General Public License
16+
along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
import { Router, Response, Request } from "express";
20+
import { Config, Snowflake } from "@spacebar/util";
21+
import { storage } from "../util/Storage";
22+
import FileType from "file-type";
23+
import { HTTPError } from "lambert-server";
24+
import crypto from "crypto";
25+
import { multer } from "../util/multer";
26+
27+
const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"];
28+
const STATIC_MIME_TYPES = [
29+
"image/png",
30+
"image/jpeg",
31+
"image/webp",
32+
"image/svg+xml",
33+
"image/svg",
34+
];
35+
const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];
36+
37+
const router = Router();
38+
39+
router.get("/:asset_id", async (req: Request, res: Response) => {
40+
let { asset_id } = req.params;
41+
const path = `avatar-decoration-presets/${asset_id}`;
42+
43+
const file = await storage.get(path);
44+
if (!file) throw new HTTPError("not found", 404);
45+
const type = await FileType.fromBuffer(file);
46+
47+
res.set("Content-Type", type?.mime);
48+
res.set("Cache-Control", "public, max-age=31536000");
49+
50+
return res.send(file);
51+
});
52+
53+
54+
export default router;

src/schemas/responses/CollectiblesShopResponse.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ export interface CollectiblesShopResponse {
2525

2626
export type AnyShopBlock = ItemRowShopBlock | BundleTileRowShopBlock | ItemCollectionShopBlock;
2727

28-
export interface BaseShopBlock {
28+
export class BaseShopBlock {
2929
type: number;
3030
}
3131

32-
export interface ItemRowShopBlock extends BaseShopBlock {
33-
type: 0;
32+
export class ItemRowShopBlock extends BaseShopBlock {
33+
declare type: 0;
3434
category_sku_id: string;
3535
name: string;
3636
category_store_listing_id: string;

src/util/entities/Channel.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,17 @@ import { HTTPError } from "lambert-server";
2020
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
2121
import { DmChannelDTO } from "../dtos";
2222
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
23-
import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, BitField } from "../util";
23+
import {
24+
InvisibleCharacters,
25+
Snowflake,
26+
containsAll,
27+
emitEvent,
28+
getPermission,
29+
trimSpecial,
30+
DiscordApiErrors,
31+
Permissions,
32+
BitField
33+
} from "../util";
2434
import { BaseClass } from "./BaseClass";
2535
import { Guild } from "./Guild";
2636
import { Invite } from "./Invite";
@@ -263,7 +273,7 @@ export class Channel extends BaseClass {
263273
if (otherRecipientsUsers.length !== recipients.length) {
264274
throw new HTTPError("Recipient/s not found");
265275
}
266-
**/
276+
**/
267277

268278
const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
269279

@@ -377,17 +387,49 @@ export class Channel extends BaseClass {
377387

378388
static async deleteChannel(channel: Channel) {
379389
// TODO Delete attachments from the CDN for messages in the channel
380-
await Channel.delete({ id: channel.id });
381390

382391
if (channel.guild_id) {
383392
const guild = await Guild.findOneOrFail({
384393
where: { id: channel.guild_id },
385394
select: { channel_ordering: true },
386395
});
387396

388-
const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
389-
await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
390-
}
397+
if (guild.features.includes("COMMUNITY")) {
398+
if (
399+
[
400+
guild.afk_channel_id,
401+
guild.system_channel_id,
402+
guild.rules_channel_id,
403+
guild.public_updates_channel_id,
404+
].includes(channel.id)
405+
) {
406+
throw DiscordApiErrors.CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL;
407+
}
408+
}
409+
else {
410+
if (guild.afk_channel_id === channel.id) {
411+
guild.afk_channel_id = null;
412+
}
413+
if (guild.system_channel_id === channel.id) {
414+
guild.system_channel_id = null;
415+
}
416+
if (guild.rules_channel_id === channel.id) {
417+
guild.rules_channel_id = null;
418+
}
419+
if (guild.public_updates_channel_id === channel.id) {
420+
guild.public_updates_channel_id = null;
421+
}
422+
}
423+
424+
await Channel.delete({ id: channel.id });
425+
426+
const updatedOrdering = guild.channel_ordering.filter(
427+
(id) => id != channel.id,
428+
);
429+
await Guild.update(
430+
{ id: channel.guild_id },
431+
{ channel_ordering: updatedOrdering },
432+
);
391433
}
392434

393435
static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) {

src/util/entities/UserSettingsProtos.ts

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,22 @@
1919
import { Column, Entity, JoinColumn, OneToOne } from "typeorm";
2020
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
2121
import { User } from "./User";
22-
import { FrecencyUserSettings, PreloadedUserSettings } from "discord-protos";
22+
import {
23+
FrecencyUserSettings,
24+
PreloadedUserSettings,
25+
PreloadedUserSettings_AppearanceSettings,
26+
PreloadedUserSettings_CustomStatus,
27+
PreloadedUserSettings_LaunchPadMode,
28+
PreloadedUserSettings_PrivacySettings,
29+
PreloadedUserSettings_StatusSettings,
30+
PreloadedUserSettings_SwipeRightToLeftMode,
31+
PreloadedUserSettings_TextAndImagesSettings,
32+
PreloadedUserSettings_Theme,
33+
PreloadedUserSettings_TimestampHourCycle,
34+
PreloadedUserSettings_UIDensity,
35+
PreloadedUserSettings_VoiceAndVideoSettings,
36+
} from "discord-protos";
37+
import { BoolValue, UInt32Value } from "discord-protos/dist/discord_protos/google/protobuf/wrappers";
2338

2439
@Entity({
2540
name: "user_settings_protos",
@@ -45,40 +60,13 @@ export class UserSettingsProtos extends BaseClassWithoutId {
4560
// @Column({nullable: true, type: "simple-json"})
4661
// testSettings: {};
4762

48-
bigintReplacer(_key: string, value: unknown): unknown {
49-
if (typeof value === "bigint") {
50-
return (value as bigint).toString();
51-
} else if (value instanceof Uint8Array) {
52-
return {
53-
__type: "Uint8Array",
54-
data: Array.from(value as Uint8Array)
55-
.map((b) => b.toString(16).padStart(2, "0"))
56-
.join(""),
57-
};
58-
} else {
59-
return value;
60-
}
61-
}
62-
63-
bigintReviver(_key: string, value: unknown): unknown {
64-
if (typeof value === "string" && /^\d+n$/.test(value)) {
65-
return BigInt((value as string).slice(0, -1));
66-
} else if (typeof value === "object" && value !== null && "__type" in value) {
67-
if (value.__type === "Uint8Array" && "data" in value) {
68-
return new Uint8Array((value.data as string).match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16)));
69-
}
70-
}
71-
return value;
72-
}
73-
7463
get userSettings(): PreloadedUserSettings | undefined {
7564
if (!this._userSettings) return undefined;
76-
return PreloadedUserSettings.fromJson(JSON.parse(this._userSettings, this.bigintReviver));
65+
return PreloadedUserSettings.fromJsonString(this._userSettings);
7766
}
7867

7968
set userSettings(value: PreloadedUserSettings | undefined) {
8069
if (value) {
81-
// this._userSettings = JSON.stringify(value, this.bigintReplacer);
8270
this._userSettings = PreloadedUserSettings.toJsonString(value);
8371
} else {
8472
this._userSettings = undefined;
@@ -87,33 +75,32 @@ export class UserSettingsProtos extends BaseClassWithoutId {
8775

8876
get frecencySettings(): FrecencyUserSettings | undefined {
8977
if (!this._frecencySettings) return undefined;
90-
return FrecencyUserSettings.fromJson(JSON.parse(this._frecencySettings, this.bigintReviver));
78+
return FrecencyUserSettings.fromJsonString(this._frecencySettings);
9179
}
9280

9381
set frecencySettings(value: FrecencyUserSettings | undefined) {
9482
if (value) {
95-
this._frecencySettings = JSON.stringify(value, this.bigintReplacer);
83+
this._frecencySettings = FrecencyUserSettings.toJsonString(value);
9684
} else {
9785
this._frecencySettings = undefined;
9886
}
9987
}
10088

101-
static async getOrDefault(user_id: string, save: boolean = false): Promise<UserSettingsProtos> {
102-
const user = await User.findOneOrFail({
103-
where: { id: user_id },
104-
select: { settings: true },
105-
});
89+
static async getOrCreate(user_id: string, save: boolean = false): Promise<UserSettingsProtos> {
90+
if (!(await User.existsBy({ id: user_id }))) throw new Error(`User with ID ${user_id} does not exist.`);
10691

10792
let userSettings = await UserSettingsProtos.findOne({
10893
where: { user_id },
10994
});
11095

11196
let modified = false;
97+
let isNewSettings = false;
11298
if (!userSettings) {
11399
userSettings = UserSettingsProtos.create({
114100
user_id,
115101
});
116102
modified = true;
103+
isNewSettings = true;
117104
}
118105

119106
if (!userSettings.userSettings) {
@@ -150,8 +137,73 @@ export class UserSettingsProtos extends BaseClassWithoutId {
150137
modified = true;
151138
}
152139

140+
if (isNewSettings) userSettings = await this.importLegacySettings(user_id, userSettings);
141+
153142
if (modified && save) userSettings = await userSettings.save();
154143

155144
return userSettings;
156145
}
146+
147+
static async importLegacySettings(user_id: string, settings: UserSettingsProtos): Promise<UserSettingsProtos> {
148+
const user = await User.findOneOrFail({
149+
where: { id: user_id },
150+
select: { settings: true },
151+
});
152+
if (!user) throw new Error(`User with ID ${user_id} does not exist.`);
153+
154+
const legacySettings = user.settings;
155+
const { frecencySettings, userSettings } = settings;
156+
157+
if (userSettings === undefined) {
158+
throw new Error("UserSettingsProtos.userSettings is undefined, this should not happen.");
159+
}
160+
if (frecencySettings === undefined) {
161+
throw new Error("UserSettingsProtos.frecencySettings is undefined, this should not happen.");
162+
}
163+
164+
if (legacySettings) {
165+
if (legacySettings.afk_timeout !== null && legacySettings.afk_timeout !== undefined) {
166+
userSettings.voiceAndVideo ??= PreloadedUserSettings_VoiceAndVideoSettings.create();
167+
userSettings.voiceAndVideo.afkTimeout = UInt32Value.fromJson(legacySettings.afk_timeout);
168+
}
169+
170+
if (legacySettings.allow_accessibility_detection !== null && legacySettings.allow_accessibility_detection !== undefined) {
171+
userSettings.privacy ??= PreloadedUserSettings_PrivacySettings.create();
172+
userSettings.privacy.allowAccessibilityDetection = legacySettings.allow_accessibility_detection;
173+
}
174+
175+
if (legacySettings.animate_emoji !== null && legacySettings.animate_emoji !== undefined) {
176+
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
177+
userSettings.textAndImages.animateEmoji = BoolValue.fromJson(legacySettings.animate_emoji);
178+
}
179+
180+
if (legacySettings.animate_stickers !== null && legacySettings.animate_stickers !== undefined) {
181+
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
182+
userSettings.textAndImages.animateStickers = UInt32Value.fromJson(legacySettings.animate_stickers);
183+
}
184+
185+
if (legacySettings.contact_sync_enabled !== null && legacySettings.contact_sync_enabled !== undefined) {
186+
userSettings.privacy ??= PreloadedUserSettings_PrivacySettings.create();
187+
userSettings.privacy.contactSyncEnabled = BoolValue.fromJson(legacySettings.contact_sync_enabled);
188+
}
189+
190+
if (legacySettings.convert_emoticons !== null && legacySettings.convert_emoticons !== undefined) {
191+
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
192+
userSettings.textAndImages.convertEmoticons = BoolValue.fromJson(legacySettings.convert_emoticons);
193+
}
194+
195+
if (legacySettings.custom_status !== null && legacySettings.custom_status !== undefined) {
196+
userSettings.status ??= PreloadedUserSettings_StatusSettings.create();
197+
userSettings.status.customStatus = PreloadedUserSettings_CustomStatus.create({
198+
emojiId: legacySettings.custom_status.emoji_id === undefined ? undefined : (BigInt(legacySettings.custom_status.emoji_id) as bigint),
199+
emojiName: legacySettings.custom_status.emoji_name,
200+
expiresAtMs: legacySettings.custom_status.expires_at === undefined ? undefined : (BigInt(legacySettings.custom_status.expires_at) as bigint),
201+
text: legacySettings.custom_status.text,
202+
createdAtMs: BigInt(Date.now()) as bigint,
203+
});
204+
}
205+
}
206+
207+
return settings;
208+
}
157209
}

0 commit comments

Comments
 (0)