Skip to content

Commit 43051c8

Browse files
Some further work
1 parent 385a333 commit 43051c8

File tree

10 files changed

+225
-59
lines changed

10 files changed

+225
-59
lines changed

.idea/codeStyles/Project.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/dataSources.xml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/sqldialects.xml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
165 KB
Loading
339 KB
Binary file not shown.

src/api/routes/collectibles-shop.ts

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

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

2323
const router = Router();
2424

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

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/util/entities/Channel.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,17 @@ import {
2727
} from "typeorm";
2828
import { DmChannelDTO } from "../dtos";
2929
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
30-
import { InvisibleCharacters, Snowflake, containsAll, emitEvent, getPermission, trimSpecial, Permissions, BitField } from "../util";
30+
import {
31+
InvisibleCharacters,
32+
Snowflake,
33+
containsAll,
34+
emitEvent,
35+
getPermission,
36+
trimSpecial,
37+
DiscordApiErrors,
38+
Permissions,
39+
BitField
40+
} from "../util";
3141
import { BaseClass } from "./BaseClass";
3242
import { Guild } from "./Guild";
3343
import { Invite } from "./Invite";
@@ -338,7 +348,7 @@ export class Channel extends BaseClass {
338348
if (otherRecipientsUsers.length !== recipients.length) {
339349
throw new HTTPError("Recipient/s not found");
340350
}
341-
**/
351+
**/
342352

343353
const type =
344354
recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
@@ -458,13 +468,41 @@ export class Channel extends BaseClass {
458468

459469
static async deleteChannel(channel: Channel) {
460470
// TODO Delete attachments from the CDN for messages in the channel
461-
await Channel.delete({ id: channel.id });
462471

463472
const guild = await Guild.findOneOrFail({
464473
where: { id: channel.guild_id },
465474
select: { channel_ordering: true },
466475
});
467476

477+
if (guild.features.includes("COMMUNITY")) {
478+
if (
479+
[
480+
guild.afk_channel_id,
481+
guild.system_channel_id,
482+
guild.rules_channel_id,
483+
guild.public_updates_channel_id,
484+
].includes(channel.id)
485+
) {
486+
throw DiscordApiErrors.CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL;
487+
}
488+
}
489+
else {
490+
if (guild.afk_channel_id === channel.id) {
491+
guild.afk_channel_id = null;
492+
}
493+
if (guild.system_channel_id === channel.id) {
494+
guild.system_channel_id = null;
495+
}
496+
if (guild.rules_channel_id === channel.id) {
497+
guild.rules_channel_id = null;
498+
}
499+
if (guild.public_updates_channel_id === channel.id) {
500+
guild.public_updates_channel_id = null;
501+
}
502+
}
503+
504+
await Channel.delete({ id: channel.id });
505+
468506
const updatedOrdering = guild.channel_ordering.filter(
469507
(id) => id != channel.id,
470508
);

src/util/entities/UserSettingsProtos.ts

Lines changed: 78 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ import { User } from "./User";
2323
import {
2424
FrecencyUserSettings,
2525
PreloadedUserSettings,
26+
PreloadedUserSettings_AppearanceSettings, PreloadedUserSettings_CustomStatus,
2627
PreloadedUserSettings_LaunchPadMode,
28+
PreloadedUserSettings_PrivacySettings, PreloadedUserSettings_StatusSettings,
2729
PreloadedUserSettings_SwipeRightToLeftMode,
30+
PreloadedUserSettings_TextAndImagesSettings,
2831
PreloadedUserSettings_Theme,
2932
PreloadedUserSettings_TimestampHourCycle,
3033
PreloadedUserSettings_UIDensity,
34+
PreloadedUserSettings_VoiceAndVideoSettings,
3135
} from "discord-protos";
36+
import { BoolValue, UInt32Value } from "discord-protos/dist/discord_protos/google/protobuf/wrappers";
3237

3338
@Entity({
3439
name: "user_settings_protos",
@@ -55,50 +60,13 @@ export class UserSettingsProtos extends BaseClassWithoutId {
5560
// @Column({nullable: true, type: "simple-json"})
5661
// testSettings: {};
5762

58-
bigintReplacer(_key: string, value: any): any {
59-
if (typeof value === "bigint") {
60-
return (value as bigint).toString();
61-
} else if (value instanceof Uint8Array) {
62-
return {
63-
__type: "Uint8Array",
64-
data: Array.from(value as Uint8Array)
65-
.map((b) => b.toString(16).padStart(2, "0"))
66-
.join(""),
67-
};
68-
} else {
69-
return value;
70-
}
71-
}
72-
73-
bigintReviver(_key: string, value: any): any {
74-
if (typeof value === "string" && /^\d+n$/.test(value)) {
75-
return BigInt((value as string).slice(0, -1));
76-
} else if (
77-
typeof value === "object" &&
78-
value !== null &&
79-
"__type" in value
80-
) {
81-
if (value.__type === "Uint8Array" && "data" in value) {
82-
return new Uint8Array(
83-
value.data
84-
.match(/.{1,2}/g)!
85-
.map((byte: string) => parseInt(byte, 16)),
86-
);
87-
}
88-
}
89-
return value;
90-
}
91-
9263
get userSettings(): PreloadedUserSettings | undefined {
9364
if (!this._userSettings) return undefined;
94-
return PreloadedUserSettings.fromJson(
95-
JSON.parse(this._userSettings, this.bigintReviver),
96-
);
65+
return PreloadedUserSettings.fromJsonString(this._userSettings);
9766
}
9867

9968
set userSettings(value: PreloadedUserSettings | undefined) {
10069
if (value) {
101-
// this._userSettings = JSON.stringify(value, this.bigintReplacer);
10270
this._userSettings = PreloadedUserSettings.toJsonString(value);
10371
} else {
10472
this._userSettings = undefined;
@@ -107,35 +75,32 @@ export class UserSettingsProtos extends BaseClassWithoutId {
10775

10876
get frecencySettings(): FrecencyUserSettings | undefined {
10977
if (!this._frecencySettings) return undefined;
110-
return FrecencyUserSettings.fromJson(
111-
JSON.parse(this._frecencySettings, this.bigintReviver),
112-
);
78+
return FrecencyUserSettings.fromJsonString(this._frecencySettings);
11379
}
11480

11581
set frecencySettings(value: FrecencyUserSettings | undefined) {
11682
if (value) {
117-
this._frecencySettings = JSON.stringify(value, this.bigintReplacer);
83+
this._frecencySettings = FrecencyUserSettings.toJsonString(value);
11884
} else {
11985
this._frecencySettings = undefined;
12086
}
12187
}
12288

12389
static async getOrCreate(user_id: string): Promise<UserSettingsProtos> {
124-
const user = await User.findOneOrFail({
125-
where: { id: user_id },
126-
select: { settings: true },
127-
});
90+
if (!(await User.existsBy({ id: user_id }))) throw new Error(`User with ID ${user_id} does not exist.`);
12891

12992
let userSettings = await UserSettingsProtos.findOne({
13093
where: { user_id },
13194
});
13295

13396
let modified = false;
97+
let isNewSettings = false;
13498
if (!userSettings) {
13599
userSettings = UserSettingsProtos.create({
136100
user_id,
137101
});
138102
modified = true;
103+
isNewSettings = true;
139104
}
140105

141106
if (!userSettings.userSettings) {
@@ -176,8 +141,74 @@ export class UserSettingsProtos extends BaseClassWithoutId {
176141
modified = true;
177142
}
178143

144+
if (isNewSettings) userSettings = await this.importLegacySettings(user_id, userSettings);
145+
179146
if (modified) userSettings = await userSettings.save();
180147

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

0 commit comments

Comments
 (0)