Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
35 changes: 30 additions & 5 deletions src/api/routes/collectibles-shop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

import { route } from "@spacebar/api";
import { Request, Response, Router } from "express";
import { CollectiblesShopResponse } from "@spacebar/schemas";
import { Config } from "@spacebar/util";
import { CollectiblesCategoryItem, CollectiblesShopResponse, ItemRowShopBlock } from "@spacebar/schemas";

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

Expand All @@ -33,10 +34,34 @@ router.get(
},
}),
(req: Request, res: Response) => {
res.send({
shop_blocks: [],
categories: [],
} as CollectiblesShopResponse);
const { endpointPublic: publicCdnEndpoint } = Config.get().cdn;
res.send({ shop_blocks: [], categories: [] });
// res.send({
// shop_blocks: [
// {
// type: 0,
// banner_asset: {
// animated: null,
// static: `${publicCdnEndpoint}/content/store/banners/main-store-banner.png`,
// },
// summary: "Welcome! Don't go alone, take this! :)",
// category_sku_id: "spacebarshop",
// name: "Spacebar",
// category_store_listing_id: "a",
// logo_url: "",
// unpublished_at: null,
// ranked_sku_ids: [],
// },
// ],
// categories: [
// {
// sku_id: "spacebarshop",
// name: "Spacebar shop category",
// summary: "Spacebar shop category items",
//
// }
// ],
// } as CollectiblesShopResponse);
},
);

Expand Down
54 changes: 54 additions & 0 deletions src/cdn/routes/avatar-decoration-presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2025 Spacebar and Spacebar Contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import { Router, Response, Request } from "express";
import { Config, Snowflake } from "@spacebar/util";
import { storage } from "../util/Storage";
import FileType from "file-type";
import { HTTPError } from "lambert-server";
import crypto from "crypto";
import { multer } from "../util/multer";

const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"];
const STATIC_MIME_TYPES = [
"image/png",
"image/jpeg",
"image/webp",
"image/svg+xml",
"image/svg",
];
const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];

const router = Router();

router.get("/:asset_id", async (req: Request, res: Response) => {
let { asset_id } = req.params;

Check failure on line 40 in src/cdn/routes/avatar-decoration-presets.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

'asset_id' is never reassigned. Use 'const' instead
const path = `avatar-decoration-presets/${asset_id}`;

const file = await storage.get(path);
if (!file) throw new HTTPError("not found", 404);
const type = await FileType.fromBuffer(file);

res.set("Content-Type", type?.mime);
res.set("Cache-Control", "public, max-age=31536000");

return res.send(file);
});


export default router;
6 changes: 3 additions & 3 deletions src/schemas/responses/CollectiblesShopResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ export interface CollectiblesShopResponse {

export type AnyShopBlock = ItemRowShopBlock | BundleTileRowShopBlock | ItemCollectionShopBlock;

export interface BaseShopBlock {
export class BaseShopBlock {
type: number;
}

export interface ItemRowShopBlock extends BaseShopBlock {
type: 0;
export class ItemRowShopBlock extends BaseShopBlock {
declare type: 0;
category_sku_id: string;
name: string;
category_store_listing_id: string;
Expand Down
54 changes: 48 additions & 6 deletions src/util/entities/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { DmChannelDTO } from "../dtos";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, BitField } from "../util";
import {
InvisibleCharacters,
Snowflake,
containsAll,
emitEvent,
getPermission,
trimSpecial,
DiscordApiErrors,
Permissions,
BitField
} from "../util";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Invite } from "./Invite";
Expand Down Expand Up @@ -263,7 +273,7 @@
if (otherRecipientsUsers.length !== recipients.length) {
throw new HTTPError("Recipient/s not found");
}
**/
**/

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

Expand Down Expand Up @@ -377,20 +387,52 @@

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

if (channel.guild_id) {
const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id },
select: { channel_ordering: true },
});

const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
}
if (guild.features.includes("COMMUNITY")) {
if (
[
guild.afk_channel_id,
guild.system_channel_id,
guild.rules_channel_id,
guild.public_updates_channel_id,
].includes(channel.id)
) {
throw DiscordApiErrors.CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL;
}
}
else {
if (guild.afk_channel_id === channel.id) {
guild.afk_channel_id = null;
}
if (guild.system_channel_id === channel.id) {
guild.system_channel_id = null;
}
if (guild.rules_channel_id === channel.id) {
guild.rules_channel_id = null;
}
if (guild.public_updates_channel_id === channel.id) {
guild.public_updates_channel_id = null;
}
}

await Channel.delete({ id: channel.id });

const updatedOrdering = guild.channel_ordering.filter(
(id) => id != channel.id,
);
await Guild.update(
{ id: channel.guild_id },
{ channel_ordering: updatedOrdering },
);
}

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

Check failure on line 435 in src/util/entities/Channel.ts

View workflow job for this annotation

GitHub Actions / build (24.x)

Declaration or statement expected.
if (!guild)
guild = await Guild.findOneOrFail({
where: { id: guild_id },
Expand Down
136 changes: 100 additions & 36 deletions src/util/entities/UserSettingsProtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@
import { Column, Entity, JoinColumn, OneToOne } from "typeorm";
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
import { User } from "./User";
import { FrecencyUserSettings, PreloadedUserSettings } from "discord-protos";
import {
FrecencyUserSettings,
PreloadedUserSettings,
PreloadedUserSettings_AppearanceSettings,
PreloadedUserSettings_CustomStatus,
PreloadedUserSettings_LaunchPadMode,
PreloadedUserSettings_PrivacySettings,
PreloadedUserSettings_StatusSettings,
PreloadedUserSettings_SwipeRightToLeftMode,
PreloadedUserSettings_TextAndImagesSettings,
PreloadedUserSettings_Theme,
PreloadedUserSettings_TimestampHourCycle,
PreloadedUserSettings_UIDensity,
PreloadedUserSettings_VoiceAndVideoSettings,
} from "discord-protos";
import { BoolValue, UInt32Value } from "discord-protos/dist/discord_protos/google/protobuf/wrappers";

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

bigintReplacer(_key: string, value: unknown): unknown {
if (typeof value === "bigint") {
return (value as bigint).toString();
} else if (value instanceof Uint8Array) {
return {
__type: "Uint8Array",
data: Array.from(value as Uint8Array)
.map((b) => b.toString(16).padStart(2, "0"))
.join(""),
};
} else {
return value;
}
}

bigintReviver(_key: string, value: unknown): unknown {
if (typeof value === "string" && /^\d+n$/.test(value)) {
return BigInt((value as string).slice(0, -1));
} else if (typeof value === "object" && value !== null && "__type" in value) {
if (value.__type === "Uint8Array" && "data" in value) {
return new Uint8Array((value.data as string).match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16)));
}
}
return value;
}

get userSettings(): PreloadedUserSettings | undefined {
if (!this._userSettings) return undefined;
return PreloadedUserSettings.fromJson(JSON.parse(this._userSettings, this.bigintReviver));
return PreloadedUserSettings.fromJsonString(this._userSettings);
}

set userSettings(value: PreloadedUserSettings | undefined) {
if (value) {
// this._userSettings = JSON.stringify(value, this.bigintReplacer);
this._userSettings = PreloadedUserSettings.toJsonString(value);
} else {
this._userSettings = undefined;
Expand All @@ -87,37 +75,48 @@ export class UserSettingsProtos extends BaseClassWithoutId {

get frecencySettings(): FrecencyUserSettings | undefined {
if (!this._frecencySettings) return undefined;
return FrecencyUserSettings.fromJson(JSON.parse(this._frecencySettings, this.bigintReviver));
return FrecencyUserSettings.fromJsonString(this._frecencySettings);
}

set frecencySettings(value: FrecencyUserSettings | undefined) {
if (value) {
this._frecencySettings = JSON.stringify(value, this.bigintReplacer);
this._frecencySettings = FrecencyUserSettings.toJsonString(value);
} else {
this._frecencySettings = undefined;
}
}

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

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

let modified = false;
let isNewSettings = false;
if (!userSettings) {
userSettings = UserSettingsProtos.create({
user_id,
});
modified = true;
isNewSettings = true;
}

if (!userSettings.userSettings) {
userSettings.userSettings = PreloadedUserSettings.create({
ads: {
alwaysDeliver: false,
},
appearance: {
developerMode: user.settings?.developer_mode ?? true,
theme: PreloadedUserSettings_Theme.DARK,
mobileRedesignDisabled: true,
launchPadMode: PreloadedUserSettings_LaunchPadMode.LAUNCH_PAD_DISABLED,
swipeRightToLeftMode: PreloadedUserSettings_SwipeRightToLeftMode.SWIPE_RIGHT_TO_LEFT_REPLY,
timestampHourCycle: PreloadedUserSettings_TimestampHourCycle.AUTO,
uiDensity: PreloadedUserSettings_UIDensity.UI_DENSITY_COMPACT,
},
versions: {
dataVersion: 0,
clientVersion: 0,
Expand All @@ -138,8 +137,73 @@ export class UserSettingsProtos extends BaseClassWithoutId {
modified = true;
}

if (isNewSettings) userSettings = await this.importLegacySettings(user_id, userSettings);

if (modified && save) userSettings = await userSettings.save();

return userSettings;
}

static async importLegacySettings(user_id: string, settings: UserSettingsProtos): Promise<UserSettingsProtos> {
const user = await User.findOneOrFail({
where: { id: user_id },
select: { settings: true },
});
if (!user) throw new Error(`User with ID ${user_id} does not exist.`);

const legacySettings = user.settings;
const { frecencySettings, userSettings } = settings;

if (userSettings === undefined) {
throw new Error("UserSettingsProtos.userSettings is undefined, this should not happen.");
}
if (frecencySettings === undefined) {
throw new Error("UserSettingsProtos.frecencySettings is undefined, this should not happen.");
}

if (legacySettings) {
if (legacySettings.afk_timeout !== null && legacySettings.afk_timeout !== undefined) {
userSettings.voiceAndVideo ??= PreloadedUserSettings_VoiceAndVideoSettings.create();
userSettings.voiceAndVideo.afkTimeout = UInt32Value.fromJson(legacySettings.afk_timeout);
}

if (legacySettings.allow_accessibility_detection !== null && legacySettings.allow_accessibility_detection !== undefined) {
userSettings.privacy ??= PreloadedUserSettings_PrivacySettings.create();
userSettings.privacy.allowAccessibilityDetection = legacySettings.allow_accessibility_detection;
}

if (legacySettings.animate_emoji !== null && legacySettings.animate_emoji !== undefined) {
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
userSettings.textAndImages.animateEmoji = BoolValue.fromJson(legacySettings.animate_emoji);
}

if (legacySettings.animate_stickers !== null && legacySettings.animate_stickers !== undefined) {
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
userSettings.textAndImages.animateStickers = UInt32Value.fromJson(legacySettings.animate_stickers);
}

if (legacySettings.contact_sync_enabled !== null && legacySettings.contact_sync_enabled !== undefined) {
userSettings.privacy ??= PreloadedUserSettings_PrivacySettings.create();
userSettings.privacy.contactSyncEnabled = BoolValue.fromJson(legacySettings.contact_sync_enabled);
}

if (legacySettings.convert_emoticons !== null && legacySettings.convert_emoticons !== undefined) {
userSettings.textAndImages ??= PreloadedUserSettings_TextAndImagesSettings.create();
userSettings.textAndImages.convertEmoticons = BoolValue.fromJson(legacySettings.convert_emoticons);
}

if (legacySettings.custom_status !== null && legacySettings.custom_status !== undefined) {
userSettings.status ??= PreloadedUserSettings_StatusSettings.create();
userSettings.status.customStatus = PreloadedUserSettings_CustomStatus.create({
emojiId: legacySettings.custom_status.emoji_id === undefined ? undefined : (BigInt(legacySettings.custom_status.emoji_id) as bigint),
emojiName: legacySettings.custom_status.emoji_name,
expiresAtMs: legacySettings.custom_status.expires_at === undefined ? undefined : (BigInt(legacySettings.custom_status.expires_at) as bigint),
text: legacySettings.custom_status.text,
createdAtMs: BigInt(Date.now()) as bigint,
});
}
}

return settings;
}
}
Loading