From c77d00e243f7cf104b8b0f8b26f339849dbd040f Mon Sep 17 00:00:00 2001 From: Rory& Date: Thu, 11 Jun 2026 22:16:07 +0200 Subject: Move entities to database --- src/api/middlewares/Authentication.ts | 3 +- .../applications/#application_id/bot/index.ts | 5 +- .../#application_id/commands/#command_id/index.ts | 7 +- .../applications/#application_id/commands/index.ts | 7 +- .../guilds/#guild_id/commands/#command_id/index.ts | 7 +- .../guilds/#guild_id/commands/index.ts | 7 +- .../routes/applications/#application_id/index.ts | 5 +- src/api/routes/applications/@me.ts | 5 +- src/api/routes/applications/index.ts | 5 +- src/api/routes/auth/forgot.ts | 6 +- .../routes/auth/generate-registration-tokens.ts | 8 +- src/api/routes/auth/login.ts | 10 +- src/api/routes/auth/logout.ts | 8 +- src/api/routes/auth/mfa/totp.ts | 6 +- src/api/routes/auth/mfa/webauthn.ts | 6 +- src/api/routes/auth/register.ts | 5 +- src/api/routes/auth/reset.ts | 5 +- src/api/routes/auth/sessions.ts | 10 +- src/api/routes/auth/verify/index.ts | 6 +- src/api/routes/auth/verify/resend.ts | 6 +- .../auth/verify/view-backup-codes-challenge.ts | 6 +- src/api/routes/channels/#channel_id/attachments.ts | 5 +- src/api/routes/channels/#channel_id/greet.ts | 5 +- src/api/routes/channels/#channel_id/index.ts | 5 +- src/api/routes/channels/#channel_id/invites.ts | 5 +- .../#channel_id/messages/#message_id/ack.ts | 5 +- .../#channel_id/messages/#message_id/index.ts | 12 +- .../#channel_id/messages/#message_id/reactions.ts | 12 +- .../#channel_id/messages/#message_id/threads.ts | 6 +- .../channels/#channel_id/messages/bulk-delete.ts | 5 +- .../routes/channels/#channel_id/messages/index.ts | 10 +- .../channels/#channel_id/messages/pins/index.ts | 5 +- .../routes/channels/#channel_id/messages/search.ts | 5 +- src/api/routes/channels/#channel_id/permissions.ts | 5 +- src/api/routes/channels/#channel_id/pins.ts | 5 +- src/api/routes/channels/#channel_id/post-data.ts | 7 +- src/api/routes/channels/#channel_id/purge.ts | 9 +- src/api/routes/channels/#channel_id/recipients.ts | 5 +- src/api/routes/channels/#channel_id/tags.ts | 7 +- .../routes/channels/#channel_id/thread-members.ts | 8 +- src/api/routes/channels/#channel_id/threads.ts | 25 +- src/api/routes/channels/#channel_id/typing.ts | 5 +- src/api/routes/channels/#channel_id/webhooks.ts | 5 +- src/api/routes/channels/preload-messages.ts | 6 +- src/api/routes/discoverable-guilds.ts | 6 +- src/api/routes/discovery.ts | 4 +- src/api/routes/download.ts | 5 +- src/api/routes/emojis/#emoji_id/source.ts | 5 +- src/api/routes/guild-recommendations.ts | 6 +- .../guilds/#guild_id/application-command-index.ts | 5 +- .../guilds/#guild_id/auto-moderation/rules.ts | 4 +- src/api/routes/guilds/#guild_id/bans.ts | 5 +- src/api/routes/guilds/#guild_id/bulk-ban.ts | 5 +- src/api/routes/guilds/#guild_id/channels.ts | 6 +- src/api/routes/guilds/#guild_id/delete.ts | 5 +- src/api/routes/guilds/#guild_id/emojis.ts | 5 +- src/api/routes/guilds/#guild_id/index.ts | 18 +- src/api/routes/guilds/#guild_id/invites.ts | 4 +- .../guilds/#guild_id/members/#member_id/index.ts | 5 +- .../guilds/#guild_id/members/#member_id/nick.ts | 5 +- .../members/#member_id/roles/#role_id/index.ts | 2 +- src/api/routes/guilds/#guild_id/members/index.ts | 4 +- src/api/routes/guilds/#guild_id/messages/search.ts | 5 +- src/api/routes/guilds/#guild_id/profile.ts | 2 +- src/api/routes/guilds/#guild_id/profile/index.ts | 5 +- src/api/routes/guilds/#guild_id/prune.ts | 6 +- src/api/routes/guilds/#guild_id/regions.ts | 2 +- .../guilds/#guild_id/roles/#role_id/index.ts | 5 +- .../guilds/#guild_id/roles/#role_id/member-ids.ts | 2 +- .../guilds/#guild_id/roles/#role_id/members.ts | 5 +- src/api/routes/guilds/#guild_id/roles/index.ts | 5 +- .../routes/guilds/#guild_id/roles/member-counts.ts | 2 +- src/api/routes/guilds/#guild_id/shield.svg.ts | 11 +- src/api/routes/guilds/#guild_id/stickers.ts | 6 +- src/api/routes/guilds/#guild_id/templates.ts | 4 +- src/api/routes/guilds/#guild_id/vanity-url.ts | 4 +- .../#guild_id/voice-states/#user_id/index.ts | 5 +- src/api/routes/guilds/#guild_id/webhooks.ts | 6 +- src/api/routes/guilds/#guild_id/welcome-screen.ts | 4 +- src/api/routes/guilds/#guild_id/widget.json.ts | 5 +- src/api/routes/guilds/#guild_id/widget.png.ts | 9 +- src/api/routes/guilds/#guild_id/widget.ts | 2 +- src/api/routes/guilds/index.ts | 5 +- src/api/routes/guilds/templates/index.ts | 5 +- .../#interaction_id/#interaction_token/callback.ts | 7 +- src/api/routes/interactions/index.ts | 5 +- src/api/routes/invites/index.ts | 3 +- src/api/routes/oauth2/applications/@me.ts | 5 +- src/api/routes/oauth2/authorize.ts | 6 +- src/api/routes/policies/stats.ts | 5 +- src/api/routes/read-states/ack-bulk.ts | 5 +- src/api/routes/safety-hub/@me/index.ts | 2 +- src/api/routes/safety-hub/suspended/@me.ts | 4 +- src/api/routes/sticker-packs/index.ts | 2 +- src/api/routes/stickers/#sticker_id/index.ts | 5 +- src/api/routes/teams.ts | 4 +- src/api/routes/updates.ts | 5 +- src/api/routes/users/#user_id/delete.ts | 18 +- src/api/routes/users/#user_id/index.ts | 2 +- src/api/routes/users/#user_id/messages.ts | 6 +- src/api/routes/users/#user_id/profile.ts | 5 +- src/api/routes/users/#user_id/relationships.ts | 2 +- src/api/routes/users/@me/channels.ts | 5 +- .../#connection_id/access-token.ts | 6 +- .../#connection_name/#connection_id/index.ts | 6 +- src/api/routes/users/@me/connections/index.ts | 3 +- src/api/routes/users/@me/delete.ts | 4 +- src/api/routes/users/@me/disable.ts | 2 +- src/api/routes/users/@me/guilds.ts | 5 +- .../routes/users/@me/guilds/#guild_id/settings.ts | 5 +- src/api/routes/users/@me/index.ts | 5 +- src/api/routes/users/@me/mentions.ts | 5 +- src/api/routes/users/@me/mfa/codes-verification.ts | 5 +- src/api/routes/users/@me/mfa/codes.ts | 5 +- src/api/routes/users/@me/mfa/totp/disable.ts | 5 +- src/api/routes/users/@me/mfa/totp/enable.ts | 5 +- .../@me/mfa/webauthn/credentials/#key_id/index.ts | 2 +- .../users/@me/mfa/webauthn/credentials/index.ts | 6 +- src/api/routes/users/@me/notes.ts | 5 +- src/api/routes/users/@me/relationships.ts | 5 +- src/api/routes/users/@me/settings-proto/1.ts | 5 +- src/api/routes/users/@me/settings-proto/2.ts | 5 +- src/api/routes/users/@me/settings.ts | 5 +- .../routes/webhooks/#webhook_id/#token/index.ts | 25 +- src/api/routes/webhooks/#webhook_id/index.ts | 38 +- src/api/util/handlers/Instance.ts | 3 +- src/api/util/handlers/Message.ts | 15 +- src/api/util/handlers/Webhook.ts | 5 +- src/api/util/utility/EmbedHandlers.ts | 7 +- src/cdn/Server.ts | 4 +- src/cdn/routes/attachments.ts | 11 +- src/connections/BattleNet/index.ts | 4 +- src/connections/Discord/index.ts | 4 +- src/connections/EpicGames/index.ts | 4 +- src/connections/Facebook/index.ts | 4 +- src/connections/GitHub/index.ts | 5 +- src/connections/Reddit/index.ts | 4 +- src/connections/Spotify/index.ts | 5 +- src/connections/Twitch/index.ts | 4 +- src/connections/Twitter/index.ts | 5 +- src/connections/Xbox/index.ts | 4 +- src/connections/Youtube/index.ts | 4 +- src/database/Database.ts | 2 +- src/database/entities/Application.ts | 138 ++++ src/database/entities/ApplicationCommand.ts | 88 +++ src/database/entities/Attachment.ts | 92 +++ src/database/entities/AuditLog.ts | 61 ++ src/database/entities/AutomodRule.ts | 67 ++ src/database/entities/BackupCodes.ts | 56 ++ src/database/entities/Badge.ts | 37 ++ src/database/entities/Ban.ts | 61 ++ src/database/entities/BaseClass.ts | 125 ++++ src/database/entities/Categories.ts | 57 ++ src/database/entities/Channel.ts | 736 +++++++++++++++++++++ src/database/entities/ClientRelease.ts | 43 ++ src/database/entities/CloudAttachment.ts | 81 +++ src/database/entities/Config.ts | 31 + src/database/entities/ConnectedAccount.ts | 83 +++ src/database/entities/ConnectionConfigEntity.ts | 31 + src/database/entities/EmbedCache.ts | 38 ++ src/database/entities/Emoji.ts | 65 ++ src/database/entities/Encryption.ts | 43 ++ src/database/entities/Guild.ts | 492 ++++++++++++++ src/database/entities/InstanceBan.ts | 111 ++++ src/database/entities/Invite.ts | 126 ++++ src/database/entities/Member.ts | 488 ++++++++++++++ src/database/entities/Message.ts | 486 ++++++++++++++ src/database/entities/Migration.ts | 33 + src/database/entities/Note.ts | 38 ++ src/database/entities/RateLimit.ts | 37 ++ src/database/entities/ReadState.ts | 93 +++ src/database/entities/Recipient.ts | 50 ++ src/database/entities/Relationship.ts | 63 ++ src/database/entities/ReportMenu.ts | 41 ++ src/database/entities/Role.ts | 85 +++ src/database/entities/SecurityKey.ts | 48 ++ src/database/entities/Session.ts | 181 +++++ src/database/entities/Sticker.ts | 75 +++ src/database/entities/StickerPack.ts | 51 ++ src/database/entities/Stream.ts | 32 + src/database/entities/StreamSession.ts | 39 ++ src/database/entities/Tag.ts | 57 ++ src/database/entities/Team.ts | 47 ++ src/database/entities/TeamMember.ts | 56 ++ src/database/entities/Template.ts | 64 ++ src/database/entities/ThreadMember.ts | 195 ++++++ src/database/entities/User.ts | 409 ++++++++++++ src/database/entities/UserSettings.ts | 142 ++++ src/database/entities/UserSettingsProtos.ts | 145 ++++ src/database/entities/ValidRegistrationTokens.ts | 33 + src/database/entities/VoiceState.ts | 107 +++ src/database/entities/Webhook.ts | 104 +++ src/database/entities/index.ts | 66 ++ src/database/index.ts | 3 + src/gateway/events/Close.ts | 5 +- src/gateway/listener/listener.ts | 23 +- src/gateway/opcodes/GuildSync.ts | 5 +- src/gateway/opcodes/Heartbeat.ts | 4 +- src/gateway/opcodes/Identify.ts | 34 +- src/gateway/opcodes/LazyRequest.ts | 4 +- src/gateway/opcodes/PresenceUpdate.ts | 5 +- src/gateway/opcodes/RequestChannelInfo.ts | 3 +- src/gateway/opcodes/RequestGuildMembers.ts | 4 +- src/gateway/opcodes/StreamCreate.ts | 33 +- src/gateway/opcodes/StreamDelete.ts | 21 +- src/gateway/opcodes/StreamWatch.ts | 25 +- src/gateway/opcodes/VoiceStateUpdate.ts | 5 +- src/gateway/util/Utils.ts | 4 +- src/gateway/util/WebSocket.ts | 5 +- src/schemas/api/bots/InteractionCreateSchema.ts | 2 +- src/schemas/api/channels/Channel.ts | 2 +- src/schemas/api/guilds/VoiceState.ts | 2 +- src/schemas/api/messages/Message.ts | 2 +- src/schemas/api/users/ConnectedAccount.ts | 2 +- src/schemas/api/users/Member.ts | 4 +- src/schemas/api/users/User.ts | 2 +- src/schemas/api/users/index.ts | 1 + .../responses/DiscoverableGuildsResponse.ts | 2 +- .../EmailDomainLookupVerifyCodeResponse.ts | 2 +- src/schemas/responses/EmojiSourceResponse.ts | 2 +- .../responses/GuildMessagesSearchResponse.ts | 2 +- .../responses/GuildRecommendationsResponse.ts | 2 +- .../responses/HubDirectoryEntriesResponse.ts | 2 +- src/schemas/responses/MemberJoinGuildResponse.ts | 2 +- .../responses/PreloadMessagesResponseSchema.ts | 2 +- src/schemas/responses/TeamListResponse.ts | 2 +- src/schemas/responses/TokenResponse.ts | 2 +- src/schemas/responses/TypedResponses.ts | 2 +- src/schemas/responses/UserProfileResponse.ts | 2 +- src/schemas/responses/WebhookCreateResponse.ts | 2 +- src/util/connections/Connection.ts | 2 +- src/util/connections/ConnectionConfig.ts | 2 +- src/util/connections/RefreshableConnection.ts | 2 +- src/util/dtos/ConnectedAccountDTO.ts | 2 +- src/util/dtos/DmChannelDTO.ts | 2 +- src/util/dtos/ReadyGuildDTO.ts | 2 +- src/util/dtos/UserDTO.ts | 2 +- src/util/entities/Application.ts | 138 ---- src/util/entities/ApplicationCommand.ts | 88 --- src/util/entities/Attachment.ts | 92 --- src/util/entities/AuditLog.ts | 61 -- src/util/entities/AutomodRule.ts | 67 -- src/util/entities/BackupCodes.ts | 56 -- src/util/entities/Badge.ts | 37 -- src/util/entities/Ban.ts | 61 -- src/util/entities/BaseClass.ts | 125 ---- src/util/entities/Categories.ts | 57 -- src/util/entities/Channel.ts | 736 --------------------- src/util/entities/ClientRelease.ts | 43 -- src/util/entities/CloudAttachment.ts | 81 --- src/util/entities/Config.ts | 31 - src/util/entities/ConnectedAccount.ts | 83 --- src/util/entities/ConnectionConfigEntity.ts | 31 - src/util/entities/EmbedCache.ts | 38 -- src/util/entities/Emoji.ts | 65 -- src/util/entities/Encryption.ts | 43 -- src/util/entities/Guild.ts | 492 -------------- src/util/entities/InstanceBan.ts | 111 ---- src/util/entities/Invite.ts | 126 ---- src/util/entities/Member.ts | 488 -------------- src/util/entities/Message.ts | 486 -------------- src/util/entities/Migration.ts | 33 - src/util/entities/Note.ts | 38 -- src/util/entities/RateLimit.ts | 37 -- src/util/entities/ReadState.ts | 93 --- src/util/entities/Recipient.ts | 50 -- src/util/entities/Relationship.ts | 63 -- src/util/entities/ReportMenu.ts | 41 -- src/util/entities/Role.ts | 85 --- src/util/entities/SecurityKey.ts | 48 -- src/util/entities/Session.ts | 181 ----- src/util/entities/Sticker.ts | 75 --- src/util/entities/StickerPack.ts | 51 -- src/util/entities/Stream.ts | 32 - src/util/entities/StreamSession.ts | 39 -- src/util/entities/Tag.ts | 57 -- src/util/entities/Team.ts | 47 -- src/util/entities/TeamMember.ts | 56 -- src/util/entities/Template.ts | 64 -- src/util/entities/ThreadMember.ts | 195 ------ src/util/entities/User.ts | 408 ------------ src/util/entities/UserSettings.ts | 142 ---- src/util/entities/UserSettingsProtos.ts | 145 ---- src/util/entities/ValidRegistrationTokens.ts | 33 - src/util/entities/VoiceState.ts | 107 --- src/util/entities/Webhook.ts | 104 --- src/util/entities/index.ts | 66 -- src/util/index.ts | 2 +- src/util/interfaces/Event.ts | 23 +- src/util/util/Application.ts | 20 +- src/util/util/Config.ts | 2 +- src/util/util/Decorators.ts | 20 +- src/util/util/Permissions.ts | 2 +- src/util/util/Presence.ts | 3 +- src/util/util/Rights.ts | 2 +- src/util/util/Token.ts | 2 +- src/util/util/cdn.ts | 2 +- src/util/util/email/index.ts | 2 +- src/webrtc/opcodes/Identify.ts | 2 +- src/webrtc/opcodes/Video.ts | 3 +- 300 files changed, 6593 insertions(+), 6436 deletions(-) create mode 100644 src/database/entities/Application.ts create mode 100644 src/database/entities/ApplicationCommand.ts create mode 100644 src/database/entities/Attachment.ts create mode 100644 src/database/entities/AuditLog.ts create mode 100644 src/database/entities/AutomodRule.ts create mode 100644 src/database/entities/BackupCodes.ts create mode 100644 src/database/entities/Badge.ts create mode 100644 src/database/entities/Ban.ts create mode 100644 src/database/entities/BaseClass.ts create mode 100644 src/database/entities/Categories.ts create mode 100644 src/database/entities/Channel.ts create mode 100644 src/database/entities/ClientRelease.ts create mode 100644 src/database/entities/CloudAttachment.ts create mode 100644 src/database/entities/Config.ts create mode 100644 src/database/entities/ConnectedAccount.ts create mode 100644 src/database/entities/ConnectionConfigEntity.ts create mode 100644 src/database/entities/EmbedCache.ts create mode 100644 src/database/entities/Emoji.ts create mode 100644 src/database/entities/Encryption.ts create mode 100644 src/database/entities/Guild.ts create mode 100644 src/database/entities/InstanceBan.ts create mode 100644 src/database/entities/Invite.ts create mode 100644 src/database/entities/Member.ts create mode 100644 src/database/entities/Message.ts create mode 100644 src/database/entities/Migration.ts create mode 100644 src/database/entities/Note.ts create mode 100644 src/database/entities/RateLimit.ts create mode 100644 src/database/entities/ReadState.ts create mode 100644 src/database/entities/Recipient.ts create mode 100644 src/database/entities/Relationship.ts create mode 100644 src/database/entities/ReportMenu.ts create mode 100644 src/database/entities/Role.ts create mode 100644 src/database/entities/SecurityKey.ts create mode 100644 src/database/entities/Session.ts create mode 100644 src/database/entities/Sticker.ts create mode 100644 src/database/entities/StickerPack.ts create mode 100644 src/database/entities/Stream.ts create mode 100644 src/database/entities/StreamSession.ts create mode 100644 src/database/entities/Tag.ts create mode 100644 src/database/entities/Team.ts create mode 100644 src/database/entities/TeamMember.ts create mode 100644 src/database/entities/Template.ts create mode 100644 src/database/entities/ThreadMember.ts create mode 100644 src/database/entities/User.ts create mode 100644 src/database/entities/UserSettings.ts create mode 100644 src/database/entities/UserSettingsProtos.ts create mode 100644 src/database/entities/ValidRegistrationTokens.ts create mode 100644 src/database/entities/VoiceState.ts create mode 100644 src/database/entities/Webhook.ts create mode 100644 src/database/entities/index.ts delete mode 100644 src/util/entities/Application.ts delete mode 100644 src/util/entities/ApplicationCommand.ts delete mode 100644 src/util/entities/Attachment.ts delete mode 100644 src/util/entities/AuditLog.ts delete mode 100644 src/util/entities/AutomodRule.ts delete mode 100644 src/util/entities/BackupCodes.ts delete mode 100644 src/util/entities/Badge.ts delete mode 100644 src/util/entities/Ban.ts delete mode 100644 src/util/entities/BaseClass.ts delete mode 100644 src/util/entities/Categories.ts delete mode 100644 src/util/entities/Channel.ts delete mode 100644 src/util/entities/ClientRelease.ts delete mode 100644 src/util/entities/CloudAttachment.ts delete mode 100644 src/util/entities/Config.ts delete mode 100644 src/util/entities/ConnectedAccount.ts delete mode 100644 src/util/entities/ConnectionConfigEntity.ts delete mode 100644 src/util/entities/EmbedCache.ts delete mode 100644 src/util/entities/Emoji.ts delete mode 100644 src/util/entities/Encryption.ts delete mode 100644 src/util/entities/Guild.ts delete mode 100644 src/util/entities/InstanceBan.ts delete mode 100644 src/util/entities/Invite.ts delete mode 100644 src/util/entities/Member.ts delete mode 100644 src/util/entities/Message.ts delete mode 100644 src/util/entities/Migration.ts delete mode 100644 src/util/entities/Note.ts delete mode 100644 src/util/entities/RateLimit.ts delete mode 100644 src/util/entities/ReadState.ts delete mode 100644 src/util/entities/Recipient.ts delete mode 100644 src/util/entities/Relationship.ts delete mode 100644 src/util/entities/ReportMenu.ts delete mode 100644 src/util/entities/Role.ts delete mode 100644 src/util/entities/SecurityKey.ts delete mode 100644 src/util/entities/Session.ts delete mode 100644 src/util/entities/Sticker.ts delete mode 100644 src/util/entities/StickerPack.ts delete mode 100644 src/util/entities/Stream.ts delete mode 100644 src/util/entities/StreamSession.ts delete mode 100644 src/util/entities/Tag.ts delete mode 100644 src/util/entities/Team.ts delete mode 100644 src/util/entities/TeamMember.ts delete mode 100644 src/util/entities/Template.ts delete mode 100644 src/util/entities/ThreadMember.ts delete mode 100644 src/util/entities/User.ts delete mode 100644 src/util/entities/UserSettings.ts delete mode 100644 src/util/entities/UserSettingsProtos.ts delete mode 100644 src/util/entities/ValidRegistrationTokens.ts delete mode 100644 src/util/entities/VoiceState.ts delete mode 100644 src/util/entities/Webhook.ts delete mode 100644 src/util/entities/index.ts (limited to 'src') diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index f6741342..9e2fa817 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { checkToken, Rights, Session, User, UserTokenData } from "@spacebar/util"; import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { Session, User } from "@spacebar/database"; +import { checkToken, Rights, UserTokenData } from "@spacebar/util"; export const NO_AUTHORIZATION_ROUTES = [ // Authentication routes diff --git a/src/api/routes/applications/#application_id/bot/index.ts b/src/api/routes/applications/#application_id/bot/index.ts index bd871745..443b5d02 100644 --- a/src/api/routes/applications/#application_id/bot/index.ts +++ b/src/api/routes/applications/#application_id/bot/index.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Application, DiscordApiErrors, FieldErrors, User, createAppBotUser, generateToken, handleFile } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { verifyToken } from "node-2fa"; +import { route } from "@spacebar/api"; +import { Application, User } from "@spacebar/database"; +import { DiscordApiErrors, FieldErrors, createAppBotUser, generateToken, handleFile } from "@spacebar/util"; import { BotModifySchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/#application_id/commands/#command_id/index.ts b/src/api/routes/applications/#application_id/commands/#command_id/index.ts index e0618613..5aec66cd 100644 --- a/src/api/routes/applications/#application_id/commands/#command_id/index.ts +++ b/src/api/routes/applications/#application_id/commands/#command_id/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Application, ApplicationCommand, FieldErrors, Snowflake } from "@spacebar/util"; +import { route } from "@spacebar/api"; +import { Application, ApplicationCommand } from "@spacebar/database"; +import { FieldErrors, Snowflake } from "@spacebar/util"; +import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/#application_id/commands/index.ts b/src/api/routes/applications/#application_id/commands/index.ts index a3aaec2f..c01a99f4 100644 --- a/src/api/routes/applications/#application_id/commands/index.ts +++ b/src/api/routes/applications/#application_id/commands/index.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Application, ApplicationCommand, FieldErrors, Snowflake } from "@spacebar/util"; import { IsNull } from "typeorm"; +import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; +import { route } from "@spacebar/api"; +import { Application, ApplicationCommand } from "@spacebar/database"; +import { FieldErrors, Snowflake } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts index 16d7786b..847d1824 100644 --- a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts +++ b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Application, ApplicationCommand, FieldErrors, Guild, Member, Snowflake } from "@spacebar/util"; +import { route } from "@spacebar/api"; +import { Application, ApplicationCommand, Guild, Member } from "@spacebar/database"; +import { FieldErrors, Snowflake } from "@spacebar/util"; +import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts index e51a4318..3664aa0c 100644 --- a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts +++ b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Application, ApplicationCommand, FieldErrors, Guild, Member, Snowflake } from "@spacebar/util"; +import { route } from "@spacebar/api"; +import { Application, ApplicationCommand, Guild, Member } from "@spacebar/database"; +import { FieldErrors, Snowflake } from "@spacebar/util"; +import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/#application_id/index.ts b/src/api/routes/applications/#application_id/index.ts index 5b885332..2a520def 100644 --- a/src/api/routes/applications/#application_id/index.ts +++ b/src/api/routes/applications/#application_id/index.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Application, DiscordApiErrors, FieldErrors, Guild, handleFile, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { verifyToken } from "node-2fa"; +import { route } from "@spacebar/api"; +import { Application, Guild, User } from "@spacebar/database"; +import { DiscordApiErrors, FieldErrors, handleFile } from "@spacebar/util"; import { ApplicationModifySchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/@me.ts b/src/api/routes/applications/@me.ts index 887dcb13..308bb4ee 100644 --- a/src/api/routes/applications/@me.ts +++ b/src/api/routes/applications/@me.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Application, Guild, handleFile } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Application, Guild } from "@spacebar/database"; +import { handleFile } from "@spacebar/util"; import { ApplicationModifySchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts index 2240379a..820716b5 100644 --- a/src/api/routes/applications/index.ts +++ b/src/api/routes/applications/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Application, Config, createAppBotUser, trimSpecial } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Application } from "@spacebar/database"; +import { Config, createAppBotUser, trimSpecial } from "@spacebar/util"; import { ApplicationCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts index 4ebb9654..3c1258f2 100644 --- a/src/api/routes/auth/forgot.ts +++ b/src/api/routes/auth/forgot.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route, verifyCaptcha } from "@spacebar/api"; -import { Config, Email, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { Config, Email } from "@spacebar/util"; import { ForgotPasswordSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts index 1493c2dc..626c6f66 100644 --- a/src/api/routes/auth/generate-registration-tokens.ts +++ b/src/api/routes/auth/generate-registration-tokens.ts @@ -16,12 +16,12 @@ along with this program. If not, see . */ -import { randomString, route } from "@spacebar/api"; -import { Config, ValidRegistrationToken } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { randomString, route } from "@spacebar/api"; +import { ValidRegistrationToken } from "@spacebar/database"; +import { Config } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); -export default router; router.get( "/", @@ -68,3 +68,5 @@ router.get( return res.json({ tokens: ret }); }, ); + +export default router; diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 80abf141..49889c2f 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -16,15 +16,15 @@ along with this program. If not, see . */ -import { route, verifyCaptcha } from "@spacebar/api"; -import { Config, FieldErrors, User, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util"; -import bcrypt from "bcrypt"; import crypto from "node:crypto"; +import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { Config, FieldErrors, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util"; import { LoginSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); -export default router; router.post( "/", @@ -197,3 +197,5 @@ router.post( * @returns {"token": "USERTOKEN", "settings": {"locale": "en", "theme": "dark"}} */ + +export default router; diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts index 331fa286..9ad9a873 100644 --- a/src/api/routes/auth/logout.ts +++ b/src/api/routes/auth/logout.ts @@ -16,12 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { emitEvent, Session } from "@spacebar/util"; +import { route } from "@spacebar/api"; +import { Session } from "@spacebar/database"; +import { emitEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); -export default router; router.post( "/", @@ -51,3 +51,5 @@ router.post( }); }, ); + +export default router; diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts index 1696f114..2d0d8fa0 100644 --- a/src/api/routes/auth/mfa/totp.ts +++ b/src/api/routes/auth/mfa/totp.ts @@ -16,12 +16,14 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { BackupCode, User, generateToken } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { verifyToken } from "node-2fa"; +import { route } from "@spacebar/api"; +import { BackupCode, User } from "@spacebar/database"; +import { generateToken } from "@spacebar/util"; import { TotpSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/auth/mfa/webauthn.ts b/src/api/routes/auth/mfa/webauthn.ts index 645c7100..d948ccac 100644 --- a/src/api/routes/auth/mfa/webauthn.ts +++ b/src/api/routes/auth/mfa/webauthn.ts @@ -16,12 +16,14 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { generateToken, SecurityKey, User, verifyWebAuthnToken, WebAuthn } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ExpectedAssertionResult } from "fido2-lib"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { SecurityKey, User } from "@spacebar/database"; +import { generateToken, verifyWebAuthnToken, WebAuthn } from "@spacebar/util"; import { WebAuthnTotpSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); function toArrayBuffer(buf: Buffer) { diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index a937b6a6..13721c04 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -16,12 +16,13 @@ along with this program. If not, see . */ -import { route, verifyCaptcha } from "@spacebar/api"; -import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient, TimeSpan, Stopwatch } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { MoreThan } from "typeorm"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { Invite, User, ValidRegistrationToken } from "@spacebar/database"; +import { Config, FieldErrors, generateToken, IpDataClient, AbuseIpDbClient, TimeSpan, Stopwatch } from "@spacebar/util"; import { RegisterSchema } from "@spacebar/schemas"; import { BcryptWorkerPool } from "../../../util/util/workers/bcrypt/BcryptWorkerPool"; diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts index 77e93e84..5460b17d 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { checkToken, Email, FieldErrors, generateToken, User } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { checkToken, Email, FieldErrors, generateToken } from "@spacebar/util"; import { PasswordResetSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/auth/sessions.ts b/src/api/routes/auth/sessions.ts index d1e1c649..c83c654c 100644 --- a/src/api/routes/auth/sessions.ts +++ b/src/api/routes/auth/sessions.ts @@ -15,12 +15,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { route } from "@spacebar/api"; + import { createHash } from "node:crypto"; -import { emitEvent, Session } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { SessionsLogoutSchema } from "../../../schemas/api/users/SessionsSchemas"; import { In } from "typeorm"; +import { route } from "@spacebar/api"; +import { emitEvent } from "@spacebar/util"; +import { SessionsLogoutSchema } from "@spacebar/schemas"; +import { Session } from "@spacebar/database"; + const router = Router({ mergeParams: true }); router.get( "/", @@ -78,4 +81,5 @@ router.post( res.status(204).send(); }, ); + export default router; diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index 7c8ca187..bdf4dc6d 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -16,9 +16,11 @@ along with this program. If not, see . */ -import { route, verifyCaptcha } from "@spacebar/api"; -import { checkToken, Config, FieldErrors, generateToken, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route, verifyCaptcha } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { checkToken, Config, FieldErrors, generateToken } from "@spacebar/util"; + const router = Router({ mergeParams: true }); async function getToken(user: User) { diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts index 29175696..98d5d69c 100644 --- a/src/api/routes/auth/verify/resend.ts +++ b/src/api/routes/auth/verify/resend.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Email, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { Email } from "@spacebar/util"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts index 7bfd6696..4cfd476b 100644 --- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts +++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts @@ -16,11 +16,13 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { FieldErrors, User } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { FieldErrors } from "@spacebar/util"; import { BackupCodesChallengeSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/channels/#channel_id/attachments.ts b/src/api/routes/channels/#channel_id/attachments.ts index 3bb3fc19..e5b9e49c 100644 --- a/src/api/routes/channels/#channel_id/attachments.ts +++ b/src/api/routes/channels/#channel_id/attachments.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { randomString, route } from "@spacebar/api"; -import { CloudAttachment, Channel, Config, Permissions } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { randomString, route } from "@spacebar/api"; +import { Channel, CloudAttachment } from "@spacebar/database"; +import { Config, Permissions } from "@spacebar/util"; import { UploadAttachmentRequestSchema, UploadAttachmentResponseSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/greet.ts b/src/api/routes/channels/#channel_id/greet.ts index 37fd5589..c1c08781 100644 --- a/src/api/routes/channels/#channel_id/greet.ts +++ b/src/api/routes/channels/#channel_id/greet.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, emitEvent, Message, MessageCreateEvent, Permissions, Sticker } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { In } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Message, Sticker } from "@spacebar/database"; +import { emitEvent, MessageCreateEvent, Permissions } from "@spacebar/util"; import { GreetRequestSchema, MessageType } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts index 3318eca2..236ddcf6 100644 --- a/src/api/routes/channels/#channel_id/index.ts +++ b/src/api/routes/channels/#channel_id/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, ChannelDeleteEvent, ChannelUpdateEvent, Recipient, emitEvent, handleFile, Config, FieldError, ErrorList, makeObjectErrorContent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Recipient } from "@spacebar/database"; +import { ChannelDeleteEvent, ChannelUpdateEvent, emitEvent, handleFile, Config, FieldError, ErrorList, makeObjectErrorContent } from "@spacebar/util"; import { ChannelModifySchema, ChannelType } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 44e431dc..8afc7dc0 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { randomString, route } from "@spacebar/api"; -import { Channel, Guild, Invite, InviteCreateEvent, PublicInviteRelation, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { randomString, route } from "@spacebar/api"; +import { Channel, Guild, Invite, PublicInviteRelation, User } from "@spacebar/database"; +import { InviteCreateEvent, emitEvent } from "@spacebar/util"; import { InviteCreateSchema, isTextChannel } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts index 28f52649..ec187d8c 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ReadState } from "@spacebar/database"; +import { emitEvent, getPermission, MessageAckEvent } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts index 749121dd..6b59389d 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server/HTTPError"; +import multer from "multer"; +import { handleMessage, postHandleMessage, route } from "@spacebar/api"; +import { Attachment, Channel, Message } from "@spacebar/database"; import { - Attachment, - Channel, - Message, MessageCreateEvent, MessageDeleteEvent, MessageUpdateEvent, @@ -31,10 +33,6 @@ import { uploadFile, NewUrlUserSignatureData, } from "@spacebar/util"; -import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server/HTTPError"; -import multer from "multer"; -import { handleMessage, postHandleMessage, route } from "@spacebar/api"; import { MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageEditSchema, ChannelType } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index aa64d8c5..c045bb54 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -16,25 +16,21 @@ along with this program. If not, see . */ +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server/HTTPError"; +import { In } from "typeorm"; import { route } from "@spacebar/api"; +import { Channel, Emoji, Member, Message, User } from "@spacebar/database"; import { arrayRemove } from "@spacebar/extensions"; import { - Channel, emitEvent, - Emoji, getPermission, - Member, - Message, MessageReactionAddEvent, MessageReactionRemoveAllEvent, MessageReactionRemoveEmojiEvent, MessageReactionRemoveEvent, - User, ReactionType, } from "@spacebar/util"; -import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server/HTTPError"; -import { In } from "typeorm"; import { PartialEmoji, PublicMemberProjection, PublicUserProjection } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts index de9feaee..bb48b4fb 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/threads.ts @@ -16,12 +16,12 @@ along with this program. If not, see . */ +import { Request, Response, Router } from "express"; import { route, sendMessage } from "@spacebar/api"; -import { Message, Channel, emitEvent, User, MessageUpdateEvent } from "@spacebar/util"; +import { Channel, Message, User } from "@spacebar/database"; +import { emitEvent, MessageUpdateEvent } from "@spacebar/util"; import { MessageThreadCreationSchema, ChannelType, MessageType } from "@spacebar/schemas"; -import { Request, Response, Router } from "express"; - const router = Router({ mergeParams: true }); // TODO: public read receipts & privacy scoping diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts index d0f2a4f5..70cbb3e4 100644 --- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Config, emitEvent, getPermission, getRights, Message, MessageDeleteBulkEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Channel, Message } from "@spacebar/database"; +import { Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index d23443cb..ccfd54fd 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -17,9 +17,8 @@ */ import { handleMessage, postHandleMessage, route } from "@spacebar/api"; +import { Attachment, Channel, Member, Message, ReadState, Relationship, User, ThreadMember, ThreadMemberFlags } from "@spacebar/database"; import { - Attachment, - Channel, Config, DiscordApiErrors, DmChannelDTO, @@ -27,19 +26,12 @@ import { FieldErrors, getPermission, getUrlSignature, - Member, - Message, MessageCreateEvent, NewUrlSignatureData, NewUrlUserSignatureData, - ReadState, - Relationship, Rights, Snowflake, uploadFile, - User, - ThreadMember, - ThreadMemberFlags, ThreadMembersUpdateEvent, ThreadCreateEvent, } from "@spacebar/util"; diff --git a/src/api/routes/channels/#channel_id/messages/pins/index.ts b/src/api/routes/channels/#channel_id/messages/pins/index.ts index e72352cb..766af429 100644 --- a/src/api/routes/channels/#channel_id/messages/pins/index.ts +++ b/src/api/routes/channels/#channel_id/messages/pins/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ChannelPinsUpdateEvent, Config, DiscordApiErrors, emitEvent, Message, MessageCreateEvent, MessageUpdateEvent, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { IsNull, Not } from "typeorm"; +import { route } from "@spacebar/api"; +import { Message, User } from "@spacebar/database"; +import { ChannelPinsUpdateEvent, Config, DiscordApiErrors, emitEvent, MessageCreateEvent, MessageUpdateEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/messages/search.ts b/src/api/routes/channels/#channel_id/messages/search.ts index 58a62bd8..ab55f8ca 100644 --- a/src/api/routes/channels/#channel_id/messages/search.ts +++ b/src/api/routes/channels/#channel_id/messages/search.ts @@ -18,11 +18,12 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { route } from "@spacebar/api"; -import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { FindManyOptions, Like } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Message } from "@spacebar/database"; +import { FieldErrors, getPermission } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts index 62386177..b1129d21 100644 --- a/src/api/routes/channels/#channel_id/permissions.ts +++ b/src/api/routes/channels/#channel_id/permissions.ts @@ -16,12 +16,13 @@ along with this program. If not, see . */ -import { Channel, ChannelUpdateEvent, emitEvent, Member, Role } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; - import { route } from "@spacebar/api"; +import { Channel, Member, Role } from "@spacebar/database"; +import { ChannelUpdateEvent, emitEvent } from "@spacebar/util"; import { ChannelPermissionOverwriteSchema, ChannelPermissionOverwrite, ChannelPermissionOverwriteType } from "@spacebar/schemas"; + const router: Router = Router({ mergeParams: true }); // TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel) diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts index 5105421d..27aeaac4 100644 --- a/src/api/routes/channels/#channel_id/pins.ts +++ b/src/api/routes/channels/#channel_id/pins.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ChannelPinsUpdateEvent, Config, DiscordApiErrors, emitEvent, Message, MessageCreateEvent, MessageUpdateEvent, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { IsNull, Not } from "typeorm"; +import { route } from "@spacebar/api"; +import { ChannelPinsUpdateEvent, Config, DiscordApiErrors, emitEvent, MessageCreateEvent, MessageUpdateEvent } from "@spacebar/util"; +import { Message, User } from "@spacebar/database"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/post-data.ts b/src/api/routes/channels/#channel_id/post-data.ts index 79d02863..f8359f99 100644 --- a/src/api/routes/channels/#channel_id/post-data.ts +++ b/src/api/routes/channels/#channel_id/post-data.ts @@ -16,13 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Member, Message } from "@spacebar/util"; -import { PostDataSchema, PublicMessage } from "@spacebar/schemas"; - import { Request, Response, Router } from "express"; import { messageUpload } from "./messages"; import { In } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Member, Message } from "@spacebar/database"; +import { PostDataSchema, PublicMessage } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts index ab8ae201..69bdd4e7 100644 --- a/src/api/routes/channels/#channel_id/purge.ts +++ b/src/api/routes/channels/#channel_id/purge.ts @@ -16,17 +16,16 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Message, MessageDeleteBulkEvent, emitEvent, getPermission, getRights } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { Between, FindManyOptions, FindOperator, Not } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Message } from "@spacebar/database"; +import { MessageDeleteBulkEvent, emitEvent, getPermission, getRights } from "@spacebar/util"; import { isTextChannel, PurgeSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); -export default router; - /** TODO: apply the delete bit by bit to prevent client and database stress **/ @@ -99,3 +98,5 @@ router.post( res.sendStatus(204); }, ); + +export default router; diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts index 28200cb5..c72b3aa4 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, ChannelRecipientAddEvent, DiscordApiErrors, DmChannelDTO, emitEvent, Recipient, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Recipient, User } from "@spacebar/database"; +import { ChannelRecipientAddEvent, DiscordApiErrors, DmChannelDTO, emitEvent } from "@spacebar/util"; import { ChannelType, PublicUserProjection } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/tags.ts b/src/api/routes/channels/#channel_id/tags.ts index 3dec8a81..ffdb428d 100644 --- a/src/api/routes/channels/#channel_id/tags.ts +++ b/src/api/routes/channels/#channel_id/tags.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, ChannelUpdateEvent, emitEvent, Tag } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { TagCreateSchema } from "@spacebar/schemas"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Channel, Tag } from "@spacebar/database"; +import { ChannelUpdateEvent, emitEvent } from "@spacebar/util"; +import { TagCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/thread-members.ts b/src/api/routes/channels/#channel_id/thread-members.ts index 9006fa14..036bc3dc 100644 --- a/src/api/routes/channels/#channel_id/thread-members.ts +++ b/src/api/routes/channels/#channel_id/thread-members.ts @@ -16,12 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, emitEvent, Member, Permissions, ThreadCreateEvent, ThreadDeleteEvent, ThreadMember, ThreadMemberFlags, ThreadMembersUpdateEvent } from "@spacebar/util"; -import { ChannelType, Snowflake } from "@spacebar/schemas"; - import { Request, Response, Router } from "express"; import { MoreThan } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Member, ThreadMember, ThreadMemberFlags } from "@spacebar/database"; +import { emitEvent, Permissions, ThreadCreateEvent, ThreadDeleteEvent, ThreadMembersUpdateEvent } from "@spacebar/util"; +import { ChannelType, Snowflake } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/threads.ts b/src/api/routes/channels/#channel_id/threads.ts index 1fdc772d..9485ab75 100644 --- a/src/api/routes/channels/#channel_id/threads.ts +++ b/src/api/routes/channels/#channel_id/threads.ts @@ -16,29 +16,14 @@ along with this program. If not, see . */ -import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api"; -import { - Channel, - emitEvent, - User, - uploadFile, - Attachment, - Member, - ReadState, - MessageCreateEvent, - FieldErrors, - getPermission, - ThreadMember, - Message, - ChannelFlags, - Snowflake, -} from "@spacebar/util"; -import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment } from "@spacebar/schemas"; - import { Request, Response, Router } from "express"; -import { messageUpload } from "./messages"; import { HTTPError } from "lambert-server/HTTPError"; import { FindManyOptions, FindOptionsOrder, In, Like, ArrayContains, ArrayOverlap } from "typeorm"; +import { handleMessage, postHandleMessage, route, sendMessage } from "@spacebar/api"; +import { Attachment, Channel, Member, Message, ReadState, ThreadMember, User } from "@spacebar/database"; +import { emitEvent, uploadFile, MessageCreateEvent, FieldErrors, getPermission, ChannelFlags } from "@spacebar/util"; +import { ChannelType, MessageType, ThreadCreationSchema, MessageCreateAttachment, MessageCreateCloudAttachment } from "@spacebar/schemas"; +import { messageUpload } from "./messages"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts index 36331647..b8ef6433 100644 --- a/src/api/routes/channels/#channel_id/typing.ts +++ b/src/api/routes/channels/#channel_id/typing.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Member } from "@spacebar/database"; +import { emitEvent, TypingStartEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 137e3139..b3efb187 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Config, DiscordApiErrors, User, Webhook, handleFile, trimSpecial, ValidateName, Application } from "@spacebar/util"; import crypto from "node:crypto"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Application, Channel, User, Webhook } from "@spacebar/database"; +import { Config, DiscordApiErrors, handleFile, trimSpecial, ValidateName } from "@spacebar/util"; import { isTextChannel, WebhookCreateSchema, WebhookType } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/preload-messages.ts b/src/api/routes/channels/preload-messages.ts index f14b387b..39fbd984 100644 --- a/src/api/routes/channels/preload-messages.ts +++ b/src/api/routes/channels/preload-messages.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, Message } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Message } from "@spacebar/database"; +import { Config } from "@spacebar/util"; import { PreloadMessagesRequestSchema, PreloadMessagesResponseSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts index 27c6cfe8..bc850bc3 100644 --- a/src/api/routes/discoverable-guilds.ts +++ b/src/api/routes/discoverable-guilds.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ -import { Config, Guild, Member } from "@spacebar/util"; - -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; import { ArrayContains, In, Not } from "typeorm"; +import { route } from "@spacebar/api"; +import { Guild, Member } from "@spacebar/database"; +import { Config } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts index d865dfe8..1afe7b23 100644 --- a/src/api/routes/discovery.ts +++ b/src/api/routes/discovery.ts @@ -16,9 +16,9 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Categories } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Categories } from "@spacebar/database"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/download.ts b/src/api/routes/download.ts index f54a3bb2..9af0f47c 100644 --- a/src/api/routes/download.ts +++ b/src/api/routes/download.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { FieldErrors, ClientRelease } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ClientRelease } from "@spacebar/database"; +import { FieldErrors } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/emojis/#emoji_id/source.ts b/src/api/routes/emojis/#emoji_id/source.ts index 10fdba0d..d7556fd2 100644 --- a/src/api/routes/emojis/#emoji_id/source.ts +++ b/src/api/routes/emojis/#emoji_id/source.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Emoji, DiscordApiErrors, Guild, Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Emoji, Guild, Member } from "@spacebar/database"; +import { DiscordApiErrors } from "@spacebar/util"; import { APIErrorResponse, EmojiGuild, EmojiSourceResponse } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts index cbe6012a..0245af83 100644 --- a/src/api/routes/guild-recommendations.ts +++ b/src/api/routes/guild-recommendations.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ -import { Config, Guild } from "@spacebar/util"; - -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; import { ArrayContains } from "typeorm"; +import { route } from "@spacebar/api"; +import { Guild } from "@spacebar/database"; +import { Config } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/application-command-index.ts b/src/api/routes/guilds/#guild_id/application-command-index.ts index d5b8d6e6..f2e5cddc 100644 --- a/src/api/routes/guilds/#guild_id/application-command-index.ts +++ b/src/api/routes/guilds/#guild_id/application-command-index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Application, ApplicationCommand, Member, Snowflake } from "@spacebar/util"; import { IsNull } from "typeorm"; +import { route } from "@spacebar/api"; +import { Application, ApplicationCommand, Member } from "@spacebar/database"; +import { Snowflake } from "@spacebar/util"; import { ApplicationCommandSchema, ApplicationCommandType } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/auto-moderation/rules.ts b/src/api/routes/guilds/#guild_id/auto-moderation/rules.ts index 7f7717d3..6ad57899 100644 --- a/src/api/routes/guilds/#guild_id/auto-moderation/rules.ts +++ b/src/api/routes/guilds/#guild_id/auto-moderation/rules.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { User, AutomodRule } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { User, AutomodRule } from "@spacebar/database"; import { AutomodRuleSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index d6541d5f..6afac611 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Ban, Member, User } from "@spacebar/database"; +import { DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, emitEvent } from "@spacebar/util"; import { BanCreateSchema, BanRegistrySchema, GuildBanResponse, GuildBansResponse, PublicUser } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts index 0d97ff75..29d7e3a1 100644 --- a/src/api/routes/guilds/#guild_id/bulk-ban.ts +++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Ban, Config, DiscordApiErrors, GuildBanAddEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Ban, Member, User } from "@spacebar/database"; +import { Config, DiscordApiErrors, GuildBanAddEvent, emitEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts index 5c87a1dc..267adc16 100644 --- a/src/api/routes/guilds/#guild_id/channels.ts +++ b/src/api/routes/guilds/#guild_id/channels.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, ChannelUpdateEvent, Guild, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Guild } from "@spacebar/database"; +import { ChannelUpdateEvent, emitEvent } from "@spacebar/util"; import { ChannelCreateSchema, ChannelReorderSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts index a855cf00..88d222c0 100644 --- a/src/api/routes/guilds/#guild_id/delete.ts +++ b/src/api/routes/guilds/#guild_id/delete.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Guild } from "@spacebar/database"; +import { GuildDeleteEvent, emitEvent } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts index 033daaf5..1d88de5d 100644 --- a/src/api/routes/guilds/#guild_id/emojis.ts +++ b/src/api/routes/guilds/#guild_id/emojis.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, Emoji, GuildEmojisUpdateEvent, Member, Snowflake, emitEvent, handleFile } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Emoji, Member } from "@spacebar/database"; +import { Config, DiscordApiErrors, GuildEmojisUpdateEvent, Snowflake, emitEvent, handleFile } from "@spacebar/util"; import { EmojiCreateSchema, EmojiModifySchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts index 115cd1a9..72e59f87 100644 --- a/src/api/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts @@ -16,23 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { - Channel, - DiscordApiErrors, - Guild, - GuildUpdateEvent, - Member, - Permissions, - SpacebarApiErrors, - emitEvent, - getPermission, - getRights, - handleFile, - Config, -} from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Channel, Guild, Member } from "@spacebar/database"; +import { DiscordApiErrors, GuildUpdateEvent, Permissions, SpacebarApiErrors, emitEvent, getPermission, getRights, handleFile } from "@spacebar/util"; import { GuildCreateResponse, GuildUpdateSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index 656d06d6..7fb330de 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -16,9 +16,9 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Invite, PublicInviteRelation } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Invite, PublicInviteRelation } from "@spacebar/database"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts index d20710de..ef5b3bf5 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, emitEvent, Emoji, getPermission, getRights, Guild, GuildMemberUpdateEvent, handleFile, Member, Role, Sticker } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Emoji, Guild, Member, Role, Sticker } from "@spacebar/database"; +import { Config, DiscordApiErrors, emitEvent, getPermission, getRights, GuildMemberUpdateEvent, handleFile } from "@spacebar/util"; import { MemberChangeSchema, PublicMemberProjection, PublicUserProjection } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts index 0e6a7465..4932bec9 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { getPermission, Member, PermissionResolvable } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Member } from "@spacebar/database"; +import { getPermission, PermissionResolvable } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts index ccfc37b1..6874abbb 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { Member } from "@spacebar/util"; +import { Member } from "@spacebar/database"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts index 2ca8e32c..2586d84d 100644 --- a/src/api/routes/guilds/#guild_id/members/index.ts +++ b/src/api/routes/guilds/#guild_id/members/index.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { MoreThan } from "typeorm"; +import { route } from "@spacebar/api"; +import { Member } from "@spacebar/database"; import { PublicMemberProjection } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts index 5c50d220..6d9bbe5e 100644 --- a/src/api/routes/guilds/#guild_id/messages/search.ts +++ b/src/api/routes/guilds/#guild_id/messages/search.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, FieldErrors, Member, Message, Snowflake, getPermission } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { Between, FindManyOptions, FindOptionsWhere, In, LessThan, Like, MoreThan } from "typeorm"; +import { route } from "@spacebar/api"; +import { Channel, Member, Message } from "@spacebar/database"; +import { FieldErrors, Snowflake, getPermission } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/profile.ts b/src/api/routes/guilds/#guild_id/profile.ts index 8bbdce1d..3156629b 100644 --- a/src/api/routes/guilds/#guild_id/profile.ts +++ b/src/api/routes/guilds/#guild_id/profile.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; import { Request, Response, Router } from "express"; import { GuildProfileResponse, GuildVisibilityLevel } from "@spacebar/schemas"; diff --git a/src/api/routes/guilds/#guild_id/profile/index.ts b/src/api/routes/guilds/#guild_id/profile/index.ts index 30a600b0..8352a379 100644 --- a/src/api/routes/guilds/#guild_id/profile/index.ts +++ b/src/api/routes/guilds/#guild_id/profile/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { emitEvent, getPermission, getRights, GuildMemberUpdateEvent, handleFile, Member, OrmUtils, Permissions } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Member } from "@spacebar/database"; +import { emitEvent, getPermission, getRights, GuildMemberUpdateEvent, handleFile, OrmUtils, Permissions } from "@spacebar/util"; import { MemberChangeProfileSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts index e2504bf6..7b19737e 100644 --- a/src/api/routes/guilds/#guild_id/prune.ts +++ b/src/api/routes/guilds/#guild_id/prune.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Guild, Member, Snowflake } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { IsNull, LessThan } from "typeorm"; +import { route } from "@spacebar/api"; +import { Guild, Member } from "@spacebar/database"; +import { Snowflake } from "@spacebar/util"; + const router = Router({ mergeParams: true }); //Returns all inactive members, respecting role hierarchy diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index 66c1e685..4edf582c 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -17,7 +17,7 @@ */ import { getVoiceRegions, route } from "@spacebar/api"; -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts index eaacce76..b3c276df 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { emitEvent, GuildRoleDeleteEvent, GuildRoleUpdateEvent, handleFile, Member, Role } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Member, Role } from "@spacebar/database"; +import { emitEvent, GuildRoleDeleteEvent, GuildRoleUpdateEvent, handleFile } from "@spacebar/util"; import { RoleModifySchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts index afa8d189..20ec5dac 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts @@ -17,7 +17,7 @@ */ import { Router, Request, Response } from "express"; -import { Member } from "@spacebar/util"; +import { Member } from "@spacebar/database"; import { route } from "@spacebar/api"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts index 5a55c731..349db75d 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts @@ -17,9 +17,10 @@ */ import { Router, Request, Response } from "express"; -import { arrayPartition } from "@spacebar/extensions"; -import { DiscordApiErrors, Member } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Member } from "@spacebar/database"; +import { arrayPartition } from "@spacebar/extensions"; +import { DiscordApiErrors } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts index d7c493d3..4cb82663 100644 --- a/src/api/routes/guilds/#guild_id/roles/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, emitEvent, GuildRoleCreateEvent, GuildRoleUpdateEvent, Member, Role, Snowflake } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { Not } from "typeorm"; +import { route } from "@spacebar/api"; +import { Member, Role } from "@spacebar/database"; +import { Config, DiscordApiErrors, emitEvent, GuildRoleCreateEvent, GuildRoleUpdateEvent, Snowflake } from "@spacebar/util"; import { RoleModifySchema, RolePositionUpdateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/roles/member-counts.ts b/src/api/routes/guilds/#guild_id/roles/member-counts.ts index 59783c3a..2aa8aaec 100644 --- a/src/api/routes/guilds/#guild_id/roles/member-counts.ts +++ b/src/api/routes/guilds/#guild_id/roles/member-counts.ts @@ -17,7 +17,7 @@ */ import { Request, Response, Router } from "express"; -import { Role, Member } from "@spacebar/util"; +import { Role, Member } from "@spacebar/database"; import { route } from "@spacebar/api"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/shield.svg.ts b/src/api/routes/guilds/#guild_id/shield.svg.ts index 983e5a35..5ccf082c 100644 --- a/src/api/routes/guilds/#guild_id/shield.svg.ts +++ b/src/api/routes/guilds/#guild_id/shield.svg.ts @@ -16,12 +16,13 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { DiscordApiErrors, Guild, Member } from "@spacebar/util"; -import { Request, Response, Router } from "express"; -import { makeBadge } from "badge-maker"; -import path from "node:path"; import fs from "node:fs"; +import path from "node:path"; +import { makeBadge } from "badge-maker"; +import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Guild, Member } from "@spacebar/database"; +import { DiscordApiErrors } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts index 7c3e2921..127d30f3 100644 --- a/src/api/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts @@ -16,12 +16,14 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { GuildStickersUpdateEvent, Member, Snowflake, Sticker, emitEvent, uploadFile, Config, DiscordApiErrors } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import multer from "multer"; +import { route } from "@spacebar/api"; +import { Member, Sticker } from "@spacebar/database"; +import { GuildStickersUpdateEvent, Snowflake, emitEvent, uploadFile, Config, DiscordApiErrors } from "@spacebar/util"; import { ModifyGuildStickerSchema, StickerFormatType, StickerType } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index 2719f88b..9942fef1 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { generateCode, route } from "@spacebar/api"; -import { Guild, Template } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { generateCode, route } from "@spacebar/api"; +import { Guild, Template } from "@spacebar/database"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts index f5289072..6a182fb3 100644 --- a/src/api/routes/guilds/#guild_id/vanity-url.ts +++ b/src/api/routes/guilds/#guild_id/vanity-url.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Guild, Invite } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Channel, Guild, Invite } from "@spacebar/database"; import { ChannelType, VanityUrlSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts index ba801298..75824e9d 100644 --- a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, DiscordApiErrors, emitEvent, getPermission, Member, VoiceState, VoiceStateUpdateEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Member, VoiceState } from "@spacebar/database"; +import { DiscordApiErrors, emitEvent, getPermission, VoiceStateUpdateEvent } from "@spacebar/util"; import { ChannelType, VoiceStateUpdateSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts index cb717f90..4b46af01 100644 --- a/src/api/routes/guilds/#guild_id/webhooks.ts +++ b/src/api/routes/guilds/#guild_id/webhooks.ts @@ -16,9 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, Webhook } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Webhook } from "@spacebar/database"; +import { Config } from "@spacebar/util"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/guilds/#guild_id/welcome-screen.ts b/src/api/routes/guilds/#guild_id/welcome-screen.ts index dee7fb45..c60cf4f6 100644 --- a/src/api/routes/guilds/#guild_id/welcome-screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts @@ -16,9 +16,9 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Guild, Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Guild, Member } from "@spacebar/database"; import { GuildUpdateWelcomeScreenSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts index f1741e88..9f3ecd31 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { randomString, route } from "@spacebar/api"; -import { Channel, Config, DiscordApiErrors, Guild, Invite, Member, Permissions } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { randomString, route } from "@spacebar/api"; +import { Channel, Guild, Member, Invite } from "@spacebar/database"; +import { Config, DiscordApiErrors, Permissions } from "@spacebar/util"; import { ChannelType, GuildWidgetJsonResponse } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts index e095548e..d3d8c7c9 100644 --- a/src/api/routes/guilds/#guild_id/widget.png.ts +++ b/src/api/routes/guilds/#guild_id/widget.png.ts @@ -18,13 +18,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { route } from "@spacebar/api"; -import { DiscordApiErrors, Guild } from "@spacebar/util"; -import { Request, Response, Router } from "express"; import fs from "node:fs"; -import { HTTPError } from "lambert-server/HTTPError"; import path from "node:path"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; import { storage } from "@spacebar/cdn"; +import { Guild } from "@spacebar/database"; +import { DiscordApiErrors } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts index 122789dc..2e2b3118 100644 --- a/src/api/routes/guilds/#guild_id/widget.ts +++ b/src/api/routes/guilds/#guild_id/widget.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; import { Request, Response, Router } from "express"; import { WidgetModifySchema } from "@spacebar/schemas"; diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts index 663677dc..f9370f27 100644 --- a/src/api/routes/guilds/index.ts +++ b/src/api/routes/guilds/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, Guild, Member, getRights } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Guild, Member } from "@spacebar/database"; +import { Config, DiscordApiErrors, getRights } from "@spacebar/util"; import { GuildCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts index a14f73dd..d934d299 100644 --- a/src/api/routes/guilds/templates/index.ts +++ b/src/api/routes/guilds/templates/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, Guild, Member, Tag, Template } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Guild, Member, Tag, Template } from "@spacebar/database"; +import { Config, DiscordApiErrors } from "@spacebar/util"; import { ChannelType, GuildTemplateCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts index 6ba97b6f..fde2c556 100644 --- a/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts +++ b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { BaseMessageComponents, InteractionCallbackSchema, InteractionCallbacksSchema, InteractionCallbackType, InteractionFailureReason, MessageType } from "@spacebar/schemas"; -import { handleComps, route, sendMessage } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { Config, emitEvent, InteractionSuccessEvent, Message, MessageUpdateEvent, pendingInteractions, User, InteractionFailureEvent } from "@spacebar/util"; import { HTTPError } from "lambert-server/HTTPError"; +import { handleComps, route, sendMessage } from "@spacebar/api"; +import { Message, User } from "@spacebar/database"; +import { InteractionCallbacksSchema, InteractionCallbackType, InteractionFailureReason, MessageType } from "@spacebar/schemas"; +import { Config, emitEvent, InteractionSuccessEvent, MessageUpdateEvent, pendingInteractions, InteractionFailureEvent } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/interactions/index.ts b/src/api/routes/interactions/index.ts index fd42050c..47b189fd 100644 --- a/src/api/routes/interactions/index.ts +++ b/src/api/routes/interactions/index.ts @@ -17,10 +17,11 @@ */ import { randomBytes } from "node:crypto"; +import { Request, Response, Router } from "express"; import { InteractionFailureReason, InteractionSchema, InteractionType } from "@spacebar/schemas"; import { route } from "@spacebar/api"; -import { Request, Response, Router } from "express"; -import { Config, emitEvent, getPermission, Guild, InteractionCreateEvent, InteractionFailureEvent, Member, Message, Snowflake } from "@spacebar/util"; +import { Guild, Member, Message } from "@spacebar/database"; +import { Config, emitEvent, getPermission, InteractionCreateEvent, InteractionFailureEvent, Snowflake } from "@spacebar/util"; import { pendingInteractions } from "@spacebar/util/imports/Interactions"; import { InteractionCreateSchema } from "@spacebar/schemas/api/bots/InteractionCreateSchema"; diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts index 421d5670..a1ba3131 100644 --- a/src/api/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts @@ -17,7 +17,8 @@ */ import { route } from "@spacebar/api"; -import { Ban, Config, DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, PublicInviteRelation } from "@spacebar/util"; +import { Ban, Guild, Invite, PublicInviteRelation } from "@spacebar/database"; +import { Config, DiscordApiErrors, emitEvent, getPermission, InviteDeleteEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { UserFlags } from "@spacebar/schemas"; diff --git a/src/api/routes/oauth2/applications/@me.ts b/src/api/routes/oauth2/applications/@me.ts index c8aa84f6..4fd68780 100644 --- a/src/api/routes/oauth2/applications/@me.ts +++ b/src/api/routes/oauth2/applications/@me.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Application, DiscordApiErrors } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Application } from "@spacebar/database"; +import { DiscordApiErrors } from "@spacebar/util"; import { PublicUserProjection } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts index 47a21eb4..55ae6a1d 100644 --- a/src/api/routes/oauth2/authorize.ts +++ b/src/api/routes/oauth2/authorize.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ApiError, Application, DiscordApiErrors, FieldErrors, Member, Permissions, User, getPermission, Role } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Application, Member, Role, User } from "@spacebar/database"; +import { ApiError, DiscordApiErrors, FieldErrors, Permissions, getPermission } from "@spacebar/util"; import { ApplicationAuthorizeSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); // TODO: scopes, other oauth types diff --git a/src/api/routes/policies/stats.ts b/src/api/routes/policies/stats.ts index 9090d787..043dbb26 100644 --- a/src/api/routes/policies/stats.ts +++ b/src/api/routes/policies/stats.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, getRights, Guild, Member, Message, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Guild, Member, Message, User } from "@spacebar/database"; +import { Config, getRights } from "@spacebar/util"; const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts index d0212c0b..c9c903d4 100644 --- a/src/api/routes/read-states/ack-bulk.ts +++ b/src/api/routes/read-states/ack-bulk.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ReadState } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ReadState } from "@spacebar/database"; import { AckBulkSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.post( diff --git a/src/api/routes/safety-hub/@me/index.ts b/src/api/routes/safety-hub/@me/index.ts index e5701292..ee55370b 100644 --- a/src/api/routes/safety-hub/@me/index.ts +++ b/src/api/routes/safety-hub/@me/index.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; +import { User } from "@spacebar/database"; import { Request, Response, Router } from "express"; import { AccountStandingResponse, AccountStandingState, AppealEligibility } from "@spacebar/schemas"; diff --git a/src/api/routes/safety-hub/suspended/@me.ts b/src/api/routes/safety-hub/suspended/@me.ts index e5701292..043f7364 100644 --- a/src/api/routes/safety-hub/suspended/@me.ts +++ b/src/api/routes/safety-hub/suspended/@me.ts @@ -16,9 +16,9 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { User } from "@spacebar/database"; import { AccountStandingResponse, AccountStandingState, AppealEligibility } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts index ce4f214d..86a18ef6 100644 --- a/src/api/routes/sticker-packs/index.ts +++ b/src/api/routes/sticker-packs/index.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { StickerPack } from "@spacebar/util"; +import { StickerPack } from "@spacebar/database"; import { Request, Response, Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts index 7188d93f..ac265c64 100644 --- a/src/api/routes/stickers/#sticker_id/index.ts +++ b/src/api/routes/stickers/#sticker_id/index.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Sticker } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Sticker } from "@spacebar/database"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/teams.ts b/src/api/routes/teams.ts index da0ea784..fa3a2413 100644 --- a/src/api/routes/teams.ts +++ b/src/api/routes/teams.ts @@ -17,9 +17,9 @@ */ import { Request, Response, Router } from "express"; -import { route } from "@spacebar/api"; -import { Team, TeamMember, User } from "@spacebar/util"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Team, TeamMember, User } from "@spacebar/database"; import { TeamCreateSchema, TeamMemberRole, TeamMemberState } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts index 7228a761..ca14507f 100644 --- a/src/api/routes/updates.ts +++ b/src/api/routes/updates.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { FieldErrors, ClientRelease } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ClientRelease } from "@spacebar/database"; +import { FieldErrors } from "@spacebar/util"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/#user_id/delete.ts b/src/api/routes/users/#user_id/delete.ts index d811fe06..49be55e2 100644 --- a/src/api/routes/users/#user_id/delete.ts +++ b/src/api/routes/users/#user_id/delete.ts @@ -17,22 +17,8 @@ */ import { route } from "@spacebar/api"; -import { - Channel, - ChannelDeleteEvent, - ChannelRecipientRemoveEvent, - emitEvent, - Emoji, - Guild, - InstanceBan, - Member, - Recipient, - Sticker, - Stopwatch, - User, - UserDeleteEvent, - UserSettingsProtos, -} from "@spacebar/util"; +import { Channel, Emoji, Guild, InstanceBan, Member, Recipient, Sticker, User, UserSettingsProtos } from "@spacebar/database"; +import { ChannelDeleteEvent, ChannelRecipientRemoveEvent, emitEvent, Stopwatch, UserDeleteEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas"; import { Not } from "typeorm"; diff --git a/src/api/routes/users/#user_id/index.ts b/src/api/routes/users/#user_id/index.ts index ada0e916..82a19420 100644 --- a/src/api/routes/users/#user_id/index.ts +++ b/src/api/routes/users/#user_id/index.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; +import { User } from "@spacebar/database"; import { Request, Response, Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/#user_id/messages.ts b/src/api/routes/users/#user_id/messages.ts index 5d86c282..1e32ca45 100644 --- a/src/api/routes/users/#user_id/messages.ts +++ b/src/api/routes/users/#user_id/messages.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, Message, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Message, User } from "@spacebar/database"; +import { Config } from "@spacebar/util"; import { DmMessagesResponseSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/routes/users/#user_id/profile.ts b/src/api/routes/users/#user_id/profile.ts index 372f8927..84082afa 100644 --- a/src/api/routes/users/#user_id/profile.ts +++ b/src/api/routes/users/#user_id/profile.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Badge, Config, emitEvent, FieldErrors, handleFile, Member, Relationship, User, UserUpdateEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { In } from "typeorm"; +import { route } from "@spacebar/api"; +import { Badge, Member, Relationship, User } from "@spacebar/database"; +import { Config, emitEvent, FieldErrors, handleFile, UserUpdateEvent } from "@spacebar/util"; import { PartialConnectedAccountResponse, PrivateUserProjection, PublicUser, PublicUserProjection, RelationshipType, UserProfileModifySchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/#user_id/relationships.ts b/src/api/routes/users/#user_id/relationships.ts index 38bd64b6..255d12c3 100644 --- a/src/api/routes/users/#user_id/relationships.ts +++ b/src/api/routes/users/#user_id/relationships.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; +import { User } from "@spacebar/database"; import { Request, Response, Router } from "express"; import { UserRelationsResponse } from "@spacebar/schemas"; diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts index dc44935e..081aa862 100644 --- a/src/api/routes/users/@me/channels.ts +++ b/src/api/routes/users/@me/channels.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, DmChannelDTO, Recipient } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Recipient } from "@spacebar/database"; +import { DmChannelDTO } from "@spacebar/util"; import { DmChannelCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts index 6e6d7e2f..595f33b5 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts @@ -16,9 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ApiError, ConnectedAccount, ConnectionStore, DiscordApiErrors, FieldErrors, RefreshableConnection } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ConnectedAccount } from "@spacebar/database"; +import { ApiError, ConnectionStore, DiscordApiErrors, FieldErrors, RefreshableConnection } from "@spacebar/util"; + const router = Router({ mergeParams: true }); // TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also) diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts index 0fc6f3fb..f633b35e 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts @@ -16,10 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { ConnectedAccount, DiscordApiErrors, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { ConnectedAccount } from "@spacebar/database"; +import { DiscordApiErrors, emitEvent } from "@spacebar/util"; import { ConnectionUpdateSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); // TODO: connection update schema diff --git a/src/api/routes/users/@me/connections/index.ts b/src/api/routes/users/@me/connections/index.ts index fe2fbba0..9dffbef3 100644 --- a/src/api/routes/users/@me/connections/index.ts +++ b/src/api/routes/users/@me/connections/index.ts @@ -18,7 +18,8 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; -import { ConnectedAccount, ConnectedAccountDTO } from "@spacebar/util"; +import { ConnectedAccount } from "@spacebar/database"; +import { ConnectedAccountDTO } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts index fab936a7..aa112dca 100644 --- a/src/api/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Guild, Member, User, UserSettingsProtos } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Guild, Member, User, UserSettingsProtos } from "@spacebar/database"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts index e95ec457..7ae17b49 100644 --- a/src/api/routes/users/@me/disable.ts +++ b/src/api/routes/users/@me/disable.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { User } from "@spacebar/util"; +import { User } from "@spacebar/database"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts index d64054ef..50f5a59c 100644 --- a/src/api/routes/users/@me/guilds.ts +++ b/src/api/routes/users/@me/guilds.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, Guild, Member } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Guild, Member } from "@spacebar/database"; +import { Config } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts index 72ba0773..157ad985 100644 --- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts +++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Channel, Member, OrmUtils } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Channel, Member } from "@spacebar/database"; +import { OrmUtils } from "@spacebar/util"; import { UserGuildSettingsSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index ae04e69f..1fda0afb 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, emitEvent, FieldErrors, generateToken, handleFile, User, UserUpdateEvent } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { User } from "@spacebar/database"; +import { Config, emitEvent, FieldErrors, generateToken, handleFile, UserUpdateEvent } from "@spacebar/util"; import { DisplayNameStyle, PrivateUserProjection, UserModifySchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mentions.ts b/src/api/routes/users/@me/mentions.ts index 87602b3c..e0908458 100644 --- a/src/api/routes/users/@me/mentions.ts +++ b/src/api/routes/users/@me/mentions.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Snowflake, Message, Member, Channel, Permissions, NewUrlUserSignatureData, Stopwatch, Attachment } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { In, LessThan, FindOptionsWhere } from "typeorm"; +import { route } from "@spacebar/api"; +import { Message, Member, Channel, Attachment } from "@spacebar/database"; +import { Snowflake, Permissions, NewUrlUserSignatureData, Stopwatch } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts index 94213037..ca6447f3 100644 --- a/src/api/routes/users/@me/mfa/codes-verification.ts +++ b/src/api/routes/users/@me/mfa/codes-verification.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { BackupCode, DiscordApiErrors, User, generateMfaBackupCodes } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { BackupCode, generateMfaBackupCodes, User } from "@spacebar/database"; +import { DiscordApiErrors } from "@spacebar/util"; import { CodesVerificationSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts index 0b1b94da..26f555bd 100644 --- a/src/api/routes/users/@me/mfa/codes.ts +++ b/src/api/routes/users/@me/mfa/codes.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { BackupCode, FieldErrors, generateMfaBackupCodes, User } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { BackupCode, generateMfaBackupCodes, User } from "@spacebar/database"; +import { FieldErrors } from "@spacebar/util"; import { MfaCodesSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts index b26161a3..c75365ac 100644 --- a/src/api/routes/users/@me/mfa/totp/disable.ts +++ b/src/api/routes/users/@me/mfa/totp/disable.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { BackupCode, User, generateToken } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { verifyToken } from "node-2fa"; +import { route } from "@spacebar/api"; +import { BackupCode, User } from "@spacebar/database"; +import { generateToken } from "@spacebar/util"; import { TotpDisableSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts index e626a736..fd098c2d 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts @@ -16,12 +16,13 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { User, generateMfaBackupCodes, generateToken } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { verifyToken } from "node-2fa"; +import { route } from "@spacebar/api"; +import { User, generateMfaBackupCodes } from "@spacebar/database"; +import { generateToken } from "@spacebar/util"; import { TotpEnableSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts index 65c01806..c15a0144 100644 --- a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts @@ -17,7 +17,7 @@ */ import { route } from "@spacebar/api"; -import { SecurityKey, User } from "@spacebar/util"; +import { SecurityKey, User } from "@spacebar/database"; import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts index 6c877189..774c4138 100644 --- a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts @@ -16,13 +16,15 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { DiscordApiErrors, FieldErrors, generateWebAuthnTicket, SecurityKey, User, verifyWebAuthnToken, WebAuthn } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { ExpectedAttestationResult } from "fido2-lib"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { SecurityKey, User } from "@spacebar/database"; +import { DiscordApiErrors, FieldErrors, generateWebAuthnTicket, verifyWebAuthnToken, WebAuthn } from "@spacebar/util"; import { CreateWebAuthnCredentialSchema, GenerateWebAuthnCredentialsSchema, WebAuthnPostSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); const isGenerateSchema = (body: WebAuthnPostSchema): body is GenerateWebAuthnCredentialsSchema => "password" in body; diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts index 3fe1682e..b988b2cd 100644 --- a/src/api/routes/users/@me/notes.ts +++ b/src/api/routes/users/@me/notes.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Note, Snowflake, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Note, User } from "@spacebar/database"; +import { Snowflake, emitEvent } from "@spacebar/util"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index be5629ab..d4d1eb1e 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, Relationship, RelationshipAddEvent, RelationshipRemoveEvent, RelationshipUpdateEvent, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; +import { route } from "@spacebar/api"; +import { Relationship, User } from "@spacebar/database"; +import { Config, DiscordApiErrors, RelationshipAddEvent, RelationshipRemoveEvent, RelationshipUpdateEvent, emitEvent } from "@spacebar/util"; import { PublicUserProjection, RelationshipType, RelationshipPatchSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/settings-proto/1.ts b/src/api/routes/users/@me/settings-proto/1.ts index 510af975..531dcdcb 100644 --- a/src/api/routes/users/@me/settings-proto/1.ts +++ b/src/api/routes/users/@me/settings-proto/1.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { emitEvent, OrmUtils, UserSettingsProtos } from "@spacebar/util"; import { PreloadedUserSettings } from "discord-protos"; import { JsonValue } from "@protobuf-ts/runtime"; +import { route } from "@spacebar/api"; +import { UserSettingsProtos } from "@spacebar/database"; +import { emitEvent, OrmUtils } from "@spacebar/util"; import { SettingsProtoJsonResponse, SettingsProtoResponse, SettingsProtoUpdateJsonSchema, SettingsProtoUpdateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/settings-proto/2.ts b/src/api/routes/users/@me/settings-proto/2.ts index 93017480..e4e9ff1d 100644 --- a/src/api/routes/users/@me/settings-proto/2.ts +++ b/src/api/routes/users/@me/settings-proto/2.ts @@ -16,11 +16,12 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; import { Request, Response, Router } from "express"; -import { emitEvent, OrmUtils, UserSettingsProtos } from "@spacebar/util"; import { FrecencyUserSettings } from "discord-protos"; import { JsonValue } from "@protobuf-ts/runtime"; +import { route } from "@spacebar/api"; +import { UserSettingsProtos } from "@spacebar/database"; +import { emitEvent, OrmUtils } from "@spacebar/util"; import { SettingsProtoJsonResponse, SettingsProtoResponse, SettingsProtoUpdateJsonSchema, SettingsProtoUpdateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index 5136c4ab..3bf8b887 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { route } from "@spacebar/api"; -import { User, UserSettings, emitEvent, Session, PresenceUpdateEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { route } from "@spacebar/api"; +import { Session, User, UserSettings } from "@spacebar/database"; +import { emitEvent, PresenceUpdateEvent } from "@spacebar/util"; import { UserSettingsUpdateSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts index cab73d64..a3984a85 100644 --- a/src/api/routes/webhooks/#webhook_id/#token/index.ts +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -1,9 +1,28 @@ -import { route } from "@spacebar/api"; -import { Config, DiscordApiErrors, emitEvent, handleFile, ValidateName, Webhook, WebhooksUpdateEvent } from "@spacebar/util"; +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import multer from "multer"; -import { executeWebhook } from "../../../../util/handlers/Webhook"; +import { route } from "@spacebar/api"; +import { Webhook } from "@spacebar/database"; +import { Config, DiscordApiErrors, emitEvent, handleFile, ValidateName, WebhooksUpdateEvent } from "@spacebar/util"; +import { executeWebhook } from "@spacebar/api/util/handlers/Webhook"; import { WebhookUpdateSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts index 5c08ec3d..71a02b22 100644 --- a/src/api/routes/webhooks/#webhook_id/index.ts +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -1,21 +1,29 @@ -import { route } from "@spacebar/api"; -import { - Config, - DiscordApiErrors, - getPermission, - Webhook, - WebhooksUpdateEvent, - emitEvent, - Channel, - handleFile, - ValidateName, - Message, - MessageDeleteBulkEvent, -} from "@spacebar/util"; +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server/HTTPError"; -import { WebhookUpdateSchema } from "@spacebar/schemas"; import { In } from "typeorm"; +import { route } from "@spacebar/api"; +import { Webhook, Channel, Message } from "@spacebar/database"; +import { Config, DiscordApiErrors, getPermission, WebhooksUpdateEvent, emitEvent, handleFile, ValidateName, MessageDeleteBulkEvent } from "@spacebar/util"; +import { WebhookUpdateSchema } from "@spacebar/schemas"; + const router = Router({ mergeParams: true }); router.get( diff --git a/src/api/util/handlers/Instance.ts b/src/api/util/handlers/Instance.ts index f899d07d..52d241aa 100644 --- a/src/api/util/handlers/Instance.ts +++ b/src/api/util/handlers/Instance.ts @@ -16,7 +16,8 @@ along with this program. If not, see . */ -import { Session, TimeSpan } from "@spacebar/util"; +import { Session } from "@spacebar/database"; +import { TimeSpan } from "@spacebar/util"; import { setInterval } from "node:timers"; export async function initInstance() { diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 4e6d7316..65d41e43 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -19,13 +19,9 @@ import { HTTPError } from "lambert-server/HTTPError"; import { Equal, In, Or } from "typeorm"; import { fillMessageUrlEmbeds, randomString } from "@spacebar/api"; -import { getDatabase } from "@spacebar/database"; +import { getDatabase, Application, Attachment, Channel, CloudAttachment, Guild, Member, Message, ReadState, Role, Session, Sticker, User, Webhook } from "@spacebar/database"; import { mathLogBase, arrayDistributeSequentially } from "@spacebar/extensions"; import { - Application, - Attachment, - Channel, - CloudAttachment, Config, DiscordApiErrors, emitEvent, @@ -33,27 +29,18 @@ import { FieldErrors, getPermission, getRights, - Guild, handleFile, HERE_MENTION, - Member, - Message, MessageCreateEvent, MessageFlags, Permissions, - ReadState, - Role, ROLE_MENTION, - Session, Snowflake, - Sticker, Stopwatch, TraceNode, TraceRoot, TraceSubTree, - User, USER_MENTION, - Webhook, } from "@spacebar/util"; import { ActionRowComponent, diff --git a/src/api/util/handlers/Webhook.ts b/src/api/util/handlers/Webhook.ts index 48032c8f..02fe6690 100644 --- a/src/api/util/handlers/Webhook.ts +++ b/src/api/util/handlers/Webhook.ts @@ -1,8 +1,9 @@ -import { handleMessage, postHandleMessage } from "@spacebar/api"; -import { Attachment, Channel, Config, DiscordApiErrors, emitEvent, FieldErrors, Message, MessageCreateEvent, Snowflake, uploadFile, ValidateName, Webhook } from "@spacebar/util"; import { Request, Response } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { MoreThan } from "typeorm"; +import { handleMessage, postHandleMessage } from "@spacebar/api"; +import { Attachment, Channel, Message, Webhook } from "@spacebar/database"; +import { Config, DiscordApiErrors, emitEvent, FieldErrors, MessageCreateEvent, Snowflake, uploadFile, ValidateName } from "@spacebar/util"; import { WebhookExecuteSchema } from "@spacebar/schemas"; export const executeWebhook = async (req: Request, res: Response) => { diff --git a/src/api/util/utility/EmbedHandlers.ts b/src/api/util/utility/EmbedHandlers.ts index fa3d74f0..c18e4f79 100644 --- a/src/api/util/utility/EmbedHandlers.ts +++ b/src/api/util/utility/EmbedHandlers.ts @@ -16,14 +16,15 @@ along with this program. If not, see . */ -import { sleep, arrayDistinctBy, arrayGroupBy } from "@spacebar/extensions"; -import { Config, EmbedCache, emitEvent, Message, MessageUpdateEvent, normalizeUrl, OrmUtils } from "@spacebar/util"; -import { Embed, EmbedImage, EmbedType } from "@spacebar/schemas"; import * as cheerio from "cheerio"; import crypto from "node:crypto"; import { yellow } from "picocolors"; import probe from "probe-image-size"; import { FindOptionsWhere, In } from "typeorm"; +import { EmbedCache, Message } from "@spacebar/database"; +import { sleep, arrayDistinctBy, arrayGroupBy } from "@spacebar/extensions"; +import { Config, emitEvent, MessageUpdateEvent, normalizeUrl, OrmUtils } from "@spacebar/util"; +import { Embed, EmbedImage, EmbedType } from "@spacebar/schemas"; export function getDefaultFetchOptions(): RequestInit { return { diff --git a/src/cdn/Server.ts b/src/cdn/Server.ts index 1c0f7802..cd0d8dee 100644 --- a/src/cdn/Server.ts +++ b/src/cdn/Server.ts @@ -20,8 +20,8 @@ import path from "node:path"; import morgan from "morgan"; import { Server, ServerOptions } from "lambert-server/Server"; import { CORS, BodyParser } from "@spacebar/api"; -import { initDatabase } from "@spacebar/database"; -import { Attachment, Config, registerRoutes } from "@spacebar/util"; +import { Attachment, initDatabase } from "@spacebar/database"; +import { Config, registerRoutes } from "@spacebar/util"; import { ProcessLifecycle } from "../util/util/ProcessLifecycle"; import { Monitoring } from "../util/monitoring/Monitoring"; import guildProfilesRoute from "./routes/guild-profiles"; diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index d1defe85..01621be1 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -16,14 +16,15 @@ along with this program. If not, see . */ -import { Config, CloudAttachment, hasValidSignature, NewUrlUserSignatureData, Snowflake, UrlSignResult } from "@spacebar/util"; import { Request, Response, Router } from "express"; +import { fileTypeFromBuffer } from "file-type"; import imageSize from "image-size"; import { HTTPError } from "lambert-server/HTTPError"; -import { multer } from "../util/multer"; -import { storage } from "@spacebar/cdn"; -import { fileTypeFromBuffer } from "file-type"; -import { cache } from "../util/cache"; +import { CloudAttachment } from "@spacebar/database"; +import { Config, hasValidSignature, NewUrlUserSignatureData, Snowflake, UrlSignResult } from "@spacebar/util"; +import { storage } from "@spacebar/cdn/util/Storage"; +import { multer } from "@spacebar/cdn/util/multer"; +import { cache } from "@spacebar/cdn/util/cache"; const router = Router({ mergeParams: true }); diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 30488e3c..d7bdc131 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as BattleNetSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts index 5a15f097..3a1ceb33 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as DiscordSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts index 5fc2701f..57305460 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as EpicGamesSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts index 3abd5669..46cb7344 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as FacebookSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index a5e0f704..3ec30e2b 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; -import { GenericOAuthSettings as GitHubSettings } from "../GenericOAuthSettings"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; +import { GenericOAuthSettings as GitHubSettings } from "../GenericOAuthSettings"; interface UserResponse { login: string; diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts index 92f3737e..01e0f738 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as RedditSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index 97884b03..ac8f98e1 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ApiError, ConnectedAccount, ConnectionLoader, DiscordApiErrors, RefreshableConnection } from "@spacebar/util"; import wretch from "wretch"; -import { GenericOAuthSettings as SpotifySettings } from "../GenericOAuthSettings"; +import { ConnectedAccount } from "@spacebar/database"; +import { ConnectionLoader, DiscordApiErrors, RefreshableConnection } from "@spacebar/util"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; +import { GenericOAuthSettings as SpotifySettings } from "../GenericOAuthSettings"; export interface UserResponse { display_name: string; diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts index da2d1f63..8ce8fc2e 100644 --- a/src/connections/Twitch/index.ts +++ b/src/connections/Twitch/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, ConnectionLoader, DiscordApiErrors, RefreshableConnection } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { RefreshableConnection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as TwitchSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts index 7a56aa41..4721a76a 100644 --- a/src/connections/Twitter/index.ts +++ b/src/connections/Twitter/index.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { ConnectedAccount, ConnectionLoader, DiscordApiErrors, RefreshableConnection } from "@spacebar/util"; import wretch from "wretch"; -import { GenericOAuthSettings as TwitterSettings } from "../GenericOAuthSettings"; +import { ConnectedAccount } from "@spacebar/database"; +import { RefreshableConnection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; +import { GenericOAuthSettings as TwitterSettings } from "../GenericOAuthSettings"; interface TwitterUserResponse { data: { diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts index 95cd6ecb..04f6eab7 100644 --- a/src/connections/Xbox/index.ts +++ b/src/connections/Xbox/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as XboxSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts index 64debb83..8e4b3a5b 100644 --- a/src/connections/Youtube/index.ts +++ b/src/connections/Youtube/index.ts @@ -16,8 +16,10 @@ along with this program. If not, see . */ -import { ConnectedAccount, Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; import wretch from "wretch"; +import { ConnectedAccount } from "@spacebar/database"; +import { Connection, ConnectionLoader, DiscordApiErrors } from "@spacebar/util"; + import { GenericOAuthSettings as YoutubeSettings } from "../GenericOAuthSettings"; import { ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema } from "@spacebar/schemas"; diff --git a/src/database/Database.ts b/src/database/Database.ts index adafeac0..a1d39478 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -21,7 +21,7 @@ import path from "node:path"; import { green, red, yellow } from "picocolors"; import { DataSource } from "typeorm"; // noinspection ES6PreferShortImport -import { ConfigEntity } from "../util/entities/Config"; +import { ConfigEntity } from "@spacebar/database/entities/Config"; import fs from "node:fs"; import { ProcessLifecycle } from "../util/util/ProcessLifecycle"; diff --git a/src/database/entities/Application.ts b/src/database/entities/Application.ts new file mode 100644 index 00000000..c4f40343 --- /dev/null +++ b/src/database/entities/Application.ts @@ -0,0 +1,138 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Team } from "./Team"; +import { User } from "./User"; +import { Guild } from "./Guild"; + +@Entity({ + name: "applications", +}) +export class Application extends BaseClass { + @Column() + name: string; + + @Column({ nullable: true }) + icon?: string; + + @Column({ nullable: true }) + description: string; + + @Column({ nullable: true }) + summary: string = ""; + + @Column({ type: "jsonb", nullable: true }) + type?: object; // TODO: this type is bad + + @Column() + hook: boolean = true; + + @Column() + bot_public?: boolean = true; + + @Column() + bot_require_code_grant?: boolean = false; + + @Column() + verify_key: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User, { onDelete: "CASCADE" }) + owner: User; + + // TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags + @Column() + flags: number = 0; + + @Column({ type: "varchar", nullable: true }) + redirect_uris: string[] = []; + + @Column({ nullable: true }) + rpc_application_state: number = 0; + + @Column({ nullable: true }) + store_application_state: number = 1; + + @Column({ nullable: true }) + verification_state: number = 1; + + @Column({ nullable: true }) + interactions_endpoint_url?: string; + + @Column({ nullable: true }) + integration_public: boolean = true; + + @Column({ nullable: true }) + integration_require_code_grant: boolean = false; + + @Column({ nullable: true }) + discoverability_state: number = 1; + + @Column({ nullable: true }) + discovery_eligibility_flags: number = 2240; + + @JoinColumn({ name: "bot_user_id" }) + @OneToOne(() => User, { onDelete: "CASCADE" }) + bot?: User; + + @Column({ type: "varchar", array: true, nullable: true }) + tags?: string[]; + + @Column({ nullable: true }) + cover_image?: string; // the application's default rich presence invite cover image hash + + @Column({ type: "jsonb", nullable: true }) + install_params?: { scopes: string[]; permissions: string }; + + @Column({ nullable: true }) + terms_of_service_url?: string; + + @Column({ nullable: true }) + privacy_policy_url?: string; + + @Column({ nullable: true }) + @RelationId((application: Application) => application.guild) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild) + guild?: Guild; // guild to which the app is linked, e.g. a developer support server + + @Column({ nullable: true }) + custom_install_url?: string; + + //just for us + + //@Column({ type: "varchar", array: true, nullable: true }) + //rpc_origins?: string[]; + + //@Column({ nullable: true }) + //primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created, + + //@Column({ nullable: true }) + //slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page + + @JoinColumn({ name: "team_id" }) + @ManyToOne(() => Team, { + onDelete: "CASCADE", + nullable: true, + }) + team?: Team; +} diff --git a/src/database/entities/ApplicationCommand.ts b/src/database/entities/ApplicationCommand.ts new file mode 100644 index 00000000..892bb833 --- /dev/null +++ b/src/database/entities/ApplicationCommand.ts @@ -0,0 +1,88 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { + ApplicationCommandHandlerType, + ApplicationCommandOption, + ApplicationCommandIndexPermissions, + ApplicationCommandType, + Snowflake, + ApplicationIntegrationType, + InteractionContextType, +} from "@spacebar/schemas"; + +@Entity({ + name: "application_commands", +}) +export class ApplicationCommand extends BaseClass { + @Column({ default: ApplicationCommandType.CHAT_INPUT }) + type?: ApplicationCommandType; + + @Column() + application_id: Snowflake; + + @Column({ nullable: true }) + guild_id?: Snowflake; + + @Column() + name: string; + + @Column({ nullable: true, type: "jsonb" }) + name_localizations?: Record; + + @Column() + description: string; + + @Column({ nullable: true, type: "jsonb" }) + description_localizations?: Record; + + @Column({ type: "jsonb", default: [] }) + options: ApplicationCommandOption[]; + + @Column({ nullable: true, type: String }) + default_member_permissions: string | null; + + /* + * @deprecated + */ + @Column({ default: true }) + dm_permission?: boolean; + + @Column({ nullable: true, type: "jsonb" }) + permissions?: ApplicationCommandIndexPermissions; + + @Column({ default: false }) + nsfw?: boolean; + + @Column({ nullable: true, type: "jsonb" }) + integration_types?: ApplicationIntegrationType[]; + + @Column({ default: 0 }) + global_popularity_rank?: number; + + @Column({ nullable: true, type: "jsonb" }) + contexts?: InteractionContextType[]; + + @Column({ default: 0 }) + version: Snowflake; + + @Column({ default: 0 }) + handler?: ApplicationCommandHandlerType; +} diff --git a/src/database/entities/Attachment.ts b/src/database/entities/Attachment.ts new file mode 100644 index 00000000..b9756bf7 --- /dev/null +++ b/src/database/entities/Attachment.ts @@ -0,0 +1,92 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { Config, deleteFile } from "../../util/util"; +import { BaseClass } from "./BaseClass"; +import { getUrlSignature, NewUrlUserSignatureData, NewUrlSignatureData } from "../../util/Signing"; +import { PublicAttachment } from "../../schemas/api/messages/Attachments"; + +@Entity({ + name: "attachments", +}) +export class Attachment extends BaseClass { + @Column() + filename: string; // name of file attached + + @Column() + size: number; // size of file in bytes + + @Column({ nullable: true }) + height?: number; // height of file (if image) + + @Column({ nullable: true }) + width?: number; // width of file (if image) + + @Column({ nullable: true }) + content_type?: string; + + @Column({ nullable: true }) + @RelationId((attachment: Attachment) => attachment.message) + message_id: string; + + @Column({ nullable: true }) + @RelationId((attachment: Attachment) => attachment.channel) + channel_id: string; + + @JoinColumn({ name: "message_id" }) + @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, { + onDelete: "CASCADE", + }) + message: import("./Message").Message; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => require("./Channel").Channel, { + onDelete: "CASCADE", + }) + channel: import("./Channel").Channel; + + @BeforeRemove() + onDelete() { + return deleteFile(new URL(this.toJSON().url).pathname); + } + + toJSON() { + const channelId = this.channel_id ?? this.channel?.id ?? this.message?.channel_id; + const messageId = this.message_id ?? this.message?.id; + return { + ...this, + url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`, + proxy_url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`, + }; + } + signUrls(data: NewUrlUserSignatureData): PublicAttachment { + const att = Attachment.prototype.toJSON.apply(this); + return { + ...att, + url: getUrlSignature(new NewUrlSignatureData({ ...data, url: att.url })) + .applyToUrl(att.url) + .toString(), + proxy_url: att.proxy_url + ? getUrlSignature(new NewUrlSignatureData({ ...data, url: att.proxy_url })) + .applyToUrl(att.proxy_url) + .toString() + : att.proxy_url, + }; + } +} diff --git a/src/database/entities/AuditLog.ts b/src/database/entities/AuditLog.ts new file mode 100644 index 00000000..f47f6dc7 --- /dev/null +++ b/src/database/entities/AuditLog.ts @@ -0,0 +1,61 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { AuditLogChange, AuditLogEvents } from "@spacebar/schemas"; + +@Entity({ + name: "audit_logs", +}) +export class AuditLog extends BaseClass { + @JoinColumn({ name: "target_id" }) + @ManyToOne(() => User) + target?: User; + + @Column({ nullable: true }) + @RelationId((auditlog: AuditLog) => auditlog.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, (user: User) => user.id) + user: User; + + @Column({ type: "int" }) + action_type: AuditLogEvents; + + @Column({ type: "jsonb", nullable: true }) + options?: { + delete_member_days?: string; + members_removed?: string; + channel_id?: string; + messaged_id?: string; + count?: string; + id?: string; + type?: string; + role_name?: string; + }; + + @Column() + @Column({ type: "jsonb" }) + changes: AuditLogChange[]; + + @Column({ nullable: true }) + reason?: string; +} diff --git a/src/database/entities/AutomodRule.ts b/src/database/entities/AutomodRule.ts new file mode 100644 index 00000000..97312984 --- /dev/null +++ b/src/database/entities/AutomodRule.ts @@ -0,0 +1,67 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2024 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 . +*/ + +import { BaseClass } from "./BaseClass"; +import { Entity, JoinColumn, ManyToOne, Column } from "typeorm"; +import { User } from "./User"; +import { AutomodAction, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas"; + +@Entity({ + name: "automod_rules", +}) +export class AutomodRule extends BaseClass { + @JoinColumn({ name: "creator_id" }) + @ManyToOne(() => User, { onDelete: "CASCADE" }) + creator: User; + + @Column() + enabled: boolean; + + @Column() + event_type: AutomodRuleEventType; + + @Column({ type: "int8", array: true }) + exempt_channels: string[]; + + @Column({ type: "int8", array: true }) + exempt_roles: string[]; + + @Column() + guild_id: string; + + @Column() + name: string; + + @Column() + position: number; + + @Column() + trigger_type: AutomodRuleTriggerType; + + @Column({ + type: "jsonb", + nullable: true, + }) + trigger_metadata?: // this is null for "Block suspected spam content" + AutomodRuleTriggerMetadata; + + @Column({ + type: "jsonb", + }) + actions: AutomodAction[]; +} diff --git a/src/database/entities/BackupCodes.ts b/src/database/entities/BackupCodes.ts new file mode 100644 index 00000000..88fe932e --- /dev/null +++ b/src/database/entities/BackupCodes.ts @@ -0,0 +1,56 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import crypto from "node:crypto"; +import { Config } from "../../util/util"; + +@Entity({ + name: "backup_codes", +}) +export class BackupCode extends BaseClass { + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { onDelete: "CASCADE" }) + user: User; + + @Column() + code: string; + + @Column() + consumed: boolean; + + @Column() + expired: boolean; +} + +export function generateMfaBackupCodes(user_id: string) { + const backup_codes: BackupCode[] = []; + for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) { + const code = BackupCode.create({ + user: { id: user_id }, + code: crypto.randomBytes(4).toString("hex"), // 8 characters + consumed: false, + expired: false, + }); + backup_codes.push(code); + } + + return backup_codes; +} diff --git a/src/database/entities/Badge.ts b/src/database/entities/Badge.ts new file mode 100644 index 00000000..924ef79e --- /dev/null +++ b/src/database/entities/Badge.ts @@ -0,0 +1,37 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; + +@Entity({ + name: "badges", +}) +export class Badge extends BaseClassWithoutId { + @Column({ primary: true }) + id: string; + + @Column() + description: string; + + @Column() + icon: string; + + @Column({ nullable: true }) + link?: string; +} diff --git a/src/database/entities/Ban.ts b/src/database/entities/Ban.ts new file mode 100644 index 00000000..17121ace --- /dev/null +++ b/src/database/entities/Ban.ts @@ -0,0 +1,61 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { User } from "./User"; + +@Entity({ + name: "bans", +}) +export class Ban extends BaseClass { + @Column({ nullable: true }) + @RelationId((ban: Ban) => ban.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ nullable: true }) + @RelationId((ban: Ban) => ban.guild) + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + @RelationId((ban: Ban) => ban.executor) + executor_id: string; + + @JoinColumn({ name: "executor_id" }) + @ManyToOne(() => User) + executor: User; + + @Column({ nullable: true }) + ip?: string; + + @Column({ nullable: true }) + reason?: string; +} diff --git a/src/database/entities/BaseClass.ts b/src/database/entities/BaseClass.ts new file mode 100644 index 00000000..770c96e8 --- /dev/null +++ b/src/database/entities/BaseClass.ts @@ -0,0 +1,125 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, PrimaryColumn } from "typeorm"; +import { getDatabase } from "../Database"; +import { Snowflake } from "../../util/util/Snowflake"; +import { OrmUtils } from "../../util/imports/OrmUtils"; +import { annotationsKey } from "../../util/util/Decorators"; + +export class BaseClassWithoutId extends BaseEntity { + private get construct() { + return this.constructor; + } + + // stores custom annotations we may stick on the properties + [annotationsKey]: { [p: string]: string[] }; + + // retrieves the custom annotations as its not super straight forward + get_annotations() { + return Object.getPrototypeOf(this)[annotationsKey]; + } + + // Loops through all the keys and compares it to annotations. If the RemoveEmpty is there it sets the value to undefined if null + clean_data() { + const annotations = this.get_annotations(); + if (annotations == undefined || annotations.length > 0) + //prevent errors if there are no annotations on an object + return; + for (const key in this) { + if ( + key in this && // This object has this property, should never fail but better to be safe + key in annotations && // If this property has an annotation + annotations[key].indexOf("JsonRemoveEmpty") > -1 && // if one of the annotations is JsonRemoveEmpty + (this[key] == null || // If this property is null + (typeof this[key] == "object" && Object.keys(this[key]).length == 0)) + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + this[key] = undefined; // set to undefined to remove + } + if ( + key in this && // This object has this property, should never fail but better to be safe + key in annotations && // If this property has an annotation + annotations[key].indexOf("JsonNumber") > -1 && // if one of the annotations is JsonRemoveEmpty + typeof this[key] == "string" // and its a String + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + this[key] = Number(this[key]); // convert string back to number + } + } + return this; + } + + private get metadata() { + return getDatabase()?.getMetadata(this.construct); + } + + assign(props: object) { + OrmUtils.mergeDeep(this, props); + return this; + } + + // TODO: fix eslint + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toJSON(): any { + this.clean_data(); + return Object.fromEntries( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + this.metadata!.columns // @ts-ignore + .map((x) => [x.propertyName, this[x.propertyName]]) + .concat( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.metadata.relations.map((x) => [ + x.propertyName, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this[x.propertyName], + ]), + ), + ); + } + + static increment(conditions: FindOptionsWhere, propertyPath: string, value: number | string) { + const repository = this.getRepository(); + return repository.increment(conditions, propertyPath, value); + } + + static decrement(conditions: FindOptionsWhere, propertyPath: string, value: number | string) { + const repository = this.getRepository(); + return repository.decrement(conditions, propertyPath, value); + } + + public async insert(): Promise { + await getDatabase()!.getRepository(this.construct).insert(this); + return this; + } +} + +export class BaseClass extends BaseClassWithoutId { + @PrimaryColumn({ type: "int8" }) + id: string = Snowflake.generate(); + + @BeforeUpdate() + @BeforeInsert() + _do_validate() { + if (!this.id) this.id = Snowflake.generate(); + } +} diff --git a/src/database/entities/Categories.ts b/src/database/entities/Categories.ts new file mode 100644 index 00000000..8c7ca78c --- /dev/null +++ b/src/database/entities/Categories.ts @@ -0,0 +1,57 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, PrimaryColumn } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; + +// TODO: categories: +// [{ +// "id": 16, +// "default": "Anime & Manga", +// "localizations": { +// "de": "Anime & Manga", +// "fr": "Anim\u00e9s et mangas", +// "ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430" +// } +// }, +// "is_primary": false/true +// }] +// Also populate discord default categories + +@Entity({ + name: "categories", +}) +export class Categories extends BaseClassWithoutId { + // Not using snowflake + + @PrimaryColumn() + id: number; + + @Column({ nullable: true }) + name: string; + + @Column({ type: "jsonb" }) + localizations: string; + + // Whether to show the category prominently (e.g. in a sidebar) instead of only secondary (e.g. in search results) + @Column({ nullable: true }) + is_primary: boolean; + + @Column({ nullable: true }) + icon?: string; +} diff --git a/src/database/entities/Channel.ts b/src/database/entities/Channel.ts new file mode 100644 index 00000000..17a2a628 --- /dev/null +++ b/src/database/entities/Channel.ts @@ -0,0 +1,736 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { HTTPError } from "lambert-server/HTTPError"; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { DmChannelDTO } from "../../util/dtos"; +import { ChannelCreateEvent, ChannelRecipientRemoveEvent, ThreadCreateEvent, ThreadMembersUpdateEvent } from "../../util/interfaces"; +import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, Config, DiscordApiErrors } from "../../util/util"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { Invite } from "./Invite"; +import { Message } from "./Message"; +import { Tag } from "./Tag"; +import { ReadState } from "./ReadState"; +import { Recipient } from "./Recipient"; +import { User } from "./User"; +import { VoiceState } from "./VoiceState"; +import { Webhook } from "./Webhook"; +import { Member } from "./Member"; +import { ChannelPermissionOverwrite, ChannelType, PublicChannel, PublicUserProjection, ThreadMetadata } from "@spacebar/schemas"; +import { OrmUtils } from "../../util/imports"; +import { ThreadMember } from "./ThreadMember"; + +@Entity({ + name: "channels", +}) +export class Channel extends BaseClass { + @Column() + created_at: Date; + + @Column({ nullable: true }) + name?: string; + + @Column({ type: "text", nullable: true }) + icon?: string | null; + + @Column({ type: "int" }) + type: ChannelType; + + @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + recipients?: Recipient[]; + + @OneToMany(() => ThreadMember, (member: ThreadMember) => member.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + thread_members?: ThreadMember[]; + + @Column({ nullable: true }) + last_message_id?: string; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.guild) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.channels, { + onDelete: "CASCADE", + nullable: true, + }) + guild?: Guild; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.parent) + parent_id: string | null; + + @JoinColumn({ name: "parent_id" }) + @ManyToOne(() => Channel) + parent?: Channel; + + // for group DMs and owned custom channel types + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.owner) + owner_id?: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User) + owner: User; + + @Column({ nullable: true, type: "timestamp with time zone" }) + last_pin_timestamp?: Date | null; // ISO8601 + + @Column({ nullable: true }) + default_auto_archive_duration?: number; + + @Column({ type: "jsonb", nullable: true }) + permission_overwrites?: ChannelPermissionOverwrite[]; + + @Column({ nullable: true }) + video_quality_mode?: number; + + @Column({ nullable: true }) + bitrate?: number; + + @Column({ nullable: true }) + user_limit?: number; + + @Column() + nsfw: boolean = false; + + @Column({ nullable: true }) + rate_limit_per_user?: number; + + @Column({ nullable: true }) + topic?: string; + + @OneToMany(() => Invite, (invite: Invite) => invite.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + invites?: Invite[]; + + @Column({ nullable: true }) + retention_policy_id?: string; + + @OneToMany(() => Message, (message: Message) => message.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + messages?: Message[]; + + @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + voice_states?: VoiceState[]; + + @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + read_states?: ReadState[]; + + @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + webhooks?: Webhook[]; + + @Column() + flags: number = 0; + + @Column({ nullable: true }) + default_thread_rate_limit_per_user?: number = 0; + + @Column({ type: "jsonb", nullable: true }) + thread_metadata?: ThreadMetadata; + + @Column({ nullable: true }) + member_count?: number; + + @Column({ nullable: true }) + message_count?: number; + + @Column({ nullable: true }) + total_message_sent?: number; + + @JoinColumn({ name: "available_tags_ids" }) + @OneToMany(() => Tag, (tag: Tag) => tag.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + available_tags?: Tag[]; + + @Column("text", { array: true, nullable: true }) + applied_tags?: string[]; + + @Column("text", { nullable: true }) + status?: string | null; + + /** Must be calculated Channel.calculatePosition */ + position: number; + + // TODO: DM channel + static async createChannel( + channel: Partial, + user_id: string = "0", + opts?: { + keepId?: boolean; + skipExistsCheck?: boolean; + skipPermissionCheck?: boolean; + skipEventEmit?: boolean; + skipNameChecks?: boolean; + }, + ): Promise { + if (!opts?.skipPermissionCheck) { + // Always check if user has permission first + const permissions = await getPermission(user_id, channel.guild_id); + permissions.hasThrow("MANAGE_CHANNELS"); + } + + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + select: { + features: !opts?.skipNameChecks, + channel_ordering: true, + id: true, + }, + }); + + if (!opts?.skipNameChecks) { + if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { + for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403); + + // Categories skip these checks on discord.com + if ( + (channel.type !== ChannelType.GUILD_CATEGORY && channel.type !== ChannelType.GUILD_STAGE_VOICE && channel.type !== ChannelType.GUILD_VOICE) || + guild.features.includes("IRC_LIKE_CATEGORY_NAMES") + ) { + if (channel.name.includes(" ")) throw new HTTPError("Channel name cannot include invalid characters", 403); + + if (channel.name.match(/--+/g)) throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403); + + if (channel.name.charAt(0) === "-" || channel.name.charAt(channel.name.length - 1) === "-") + throw new HTTPError("Channel name cannot start/end with dash.", 403); + } else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com + } + + if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { + if (!channel.name) throw new HTTPError("Channel name cannot be empty.", 403); + } + } + + switch (channel.type) { + // TODO: should threads even be routed through this function instead of createThreadChannel? + case ChannelType.GUILD_PUBLIC_THREAD: + case ChannelType.GUILD_PRIVATE_THREAD: + case ChannelType.GUILD_NEWS_THREAD: + case ChannelType.GUILD_TEXT: + case ChannelType.GUILD_FORUM: + case ChannelType.GUILD_MEDIA: + case ChannelType.GUILD_NEWS: + case ChannelType.GUILD_VOICE: + if (channel.parent_id && !opts?.skipExistsCheck) { + const exists = await Channel.findOneOrFail({ + where: { id: channel.parent_id }, + }); + if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); + if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild"); + } + break; + case ChannelType.GUILD_CATEGORY: + case ChannelType.UNHANDLED: + break; + case ChannelType.DM: + case ChannelType.GROUP_DM: + throw new HTTPError("You can't create a dm channel in a guild"); + case ChannelType.GUILD_STORE: + default: + throw new HTTPError("Not yet supported"); + } + + if (!channel.permission_overwrites) channel.permission_overwrites = []; + // TODO: eagerly auto generate position of all guild channels + + const position = (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0; + + channel = { + ...channel, + ...(!opts?.keepId && { id: Snowflake.generate() }), + created_at: new Date(), + position, + // from #876 (threads): shouldnt these be undefined? + // message_count: 0, + // member_count: 0, + // total_message_sent: 0, + }; + + // TODO: figure out why the generic is required here + const ret = Channel.create(channel); + + await Promise.all([ + ret.save(), + !opts?.skipEventEmit + ? emitEvent({ + event: "CHANNEL_CREATE", + data: ret.toJSON(), + guild_id: channel.guild_id, + } satisfies ChannelCreateEvent) + : Promise.resolve(), + Guild.insertChannelInOrder(guild.id, ret.id, position, guild), + ]); + + return ret; + } + threadOnly() { + return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA; + } + + static async createThreadChannel( + channel: Partial, + metadata: Partial, + user_id: string = "0", + opts?: { + keepId?: boolean; + skipExistsCheck?: boolean; + skipParentExistsCheck?: boolean; + skipPermissionCheck?: boolean; + skipEventEmit?: boolean; + skipNameChecks?: boolean; + }, + ): Promise { + channel = { + // set the default type to private + type: ChannelType.GUILD_PRIVATE_THREAD, + ...channel, + ...(!opts?.keepId && { id: Snowflake.generate() }), + created_at: new Date(), + position: 0, // TODO: + message_count: 0, + member_count: 1, + total_message_sent: 0, + }; + + const exists = await Channel.findOne({ + where: { + id: channel.id, + }, + }); + + const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); + + if (!opts?.skipExistsCheck && !guild.features.includes("ALLOW_EXISTING_THREAD_FOR_MESSAGE") && exists) throw DiscordApiErrors.THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE; + + if (!channel.parent_id) throw new HTTPError("Parent id not set", 400); + const parent = await Channel.findOneOrFail({ where: { id: channel.parent_id } }); + + if (!opts?.skipPermissionCheck) { + // Always check if user has permission first + const permissions = await getPermission(user_id, parent.guild_id); + permissions.hasThrow(channel.type === ChannelType.GUILD_PRIVATE_THREAD ? "CREATE_PRIVATE_THREADS" : "CREATE_PUBLIC_THREADS"); + } + + channel = { + ...channel, + permission_overwrites: parent.permission_overwrites, + nsfw: parent.nsfw, + owner_id: user_id, + guild_id: parent.guild_id, + thread_metadata: { + create_timestamp: new Date().toISOString(), + archive_timestamp: new Date().toISOString(), + archived: false, + auto_archive_duration: 0, + invitable: channel.type === ChannelType.GUILD_NEWS_THREAD || channel.type === ChannelType.GUILD_PUBLIC_THREAD ? Config.get().guild.publicThreadsInvitable : false, + locked: false, + ...metadata, + }, + }; + + if (!opts?.skipParentExistsCheck) { + if (!parent) throw new HTTPError("Parent channel doesn't exist", 400); + if (parent.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild"); + } + + if (!opts?.skipNameChecks) { + const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); + if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { + for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403); + + channel.name = channel.name.trim(); //category names are trimmed client side on discord.com + } + + if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { + if (!channel.name) throw new HTTPError("Channel name cannot be empty.", 403); + } + } + + // TODO: eagerly auto generate position of all guild channels + + const thread = await OrmUtils.mergeDeep(new Channel(), channel).save(); + + const member = { + id: thread.id, + user_id, + join_timestamp: new Date(), + muted: false, + mute_config: null, + flags: 0, + }; + if (channel.member_count) channel.member_count++; + + const threadMember = await OrmUtils.mergeDeep(new ThreadMember(), member).save(); + + if (!opts?.skipEventEmit) { + await Promise.all([ + emitEvent({ + event: "THREAD_CREATE", + data: { + ...thread, + newly_created: true, + }, + guild_id: channel.guild_id, + } satisfies ThreadCreateEvent), + emitEvent({ + event: "THREAD_MEMBERS_UPDATE", + data: { + guild_id: channel.guild_id!, // TODO: is this the right fix? + id: thread.id, + member_count: channel.member_count ?? 0, //TODO: is this the right fix? + added_members: [threadMember], + removed_member_ids: [], + }, + guild_id: channel.guild_id, + } satisfies ThreadMembersUpdateEvent), + ]); + } + + return thread; + } + + static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { + recipients = [...new Set(recipients)].filter((x) => x !== creator_user_id); + // TODO: check config for max number of recipients + /** if you want to disallow note to self channels, uncomment the conditional below + + const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); + if (otherRecipientsUsers.length !== recipients.length) { + throw new HTTPError("Recipient/s not found"); + } + **/ + + const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; + + let channel = null; + let needsTx = true; + + const channelRecipients = [...recipients, creator_user_id]; + + const userRecipients = await Recipient.find({ + where: { user_id: creator_user_id }, + relations: { channel: { recipients: true } }, + }); + + for (const ur of userRecipients) { + if (!ur.channel.recipients) continue; + const re = ur.channel.recipients.map((r) => r.user_id); + if (re.length === channelRecipients.length) { + if (channelRecipients.every((_) => re.includes(_))) { + if (channel == null) { + channel = ur.channel; + if (!ur.closed) needsTx = false; + await ur.assign({ closed: false }).save(); + } + } + } + } + + if (channel == null) { + name = trimSpecial(name); + + channel = await Channel.create({ + name, + type, + owner_id: type === ChannelType.GROUP_DM ? creator_user_id : undefined, + created_at: new Date(), + last_message_id: undefined, + recipients: channelRecipients.map((x) => + Recipient.create({ + user_id: x, + closed: !(type === ChannelType.GROUP_DM || x === creator_user_id), + }), + ), + nsfw: false, + }).save(); + } + + const channel_dto = await DmChannelDTO.from(channel); + + if (!needsTx) { + /*ignored*/ + } else if (type === ChannelType.GROUP_DM && channel.recipients) { + for (const recipient of channel.recipients) { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto.excludedRecipients([recipient.user_id]), + user_id: recipient.user_id, + }); + } + } else { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto, + user_id: creator_user_id, + }); + } + + if (recipients.length === 1) return channel_dto; + else return channel_dto.excludedRecipients([creator_user_id]); + } + + static async removeRecipientFromChannel(channel: Channel, user_id: string) { + await Recipient.delete({ channel_id: channel.id, user_id: user_id }); + channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); + + if (channel.recipients?.length === 0) { + await Channel.deleteChannel(channel); + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + return; + } + + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + + //If the owner leave the server user is the new owner + if (channel.owner_id === user_id) { + channel.owner_id = "1"; // The channel is now owned by the server user + await emitEvent({ + event: "CHANNEL_UPDATE", + data: await DmChannelDTO.from(channel, [user_id]), + channel_id: channel.id, + }); + } + + await channel.save(); + + await emitEvent({ + event: "CHANNEL_RECIPIENT_REMOVE", + data: { + channel_id: channel.id, + user: await User.findOneOrFail({ + where: { id: user_id }, + select: PublicUserProjection, + }), + }, + channel_id: channel.id, + } satisfies ChannelRecipientRemoveEvent); + } + + 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 }); + } + } + + static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) { + if (!guild) + guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: { channel_ordering: true }, + }); + + return guild.channel_ordering.findIndex((id) => channel_id == id); + } + + static async getOrderedChannels(guild_id: string, guild?: Guild) { + if (!guild) + guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: { channel_ordering: true }, + }); + + const channels = await Promise.all(guild.channel_ordering.map((id) => Channel.findOne({ where: { id } }))); + + return channels + .filter((channel) => channel !== null) + .reduce((r, v) => { + v = v as Channel; + + v.position = (guild as Guild).channel_ordering.indexOf(v.id); + r[v.position] = v; + return r; + }, [] as Array); + } + + isDm() { + return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; + } + + isThread() { + return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD || this.type === ChannelType.GUILD_PRIVATE_THREAD; + } + isForum() { + return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA; + } + + isPrivateThread() { + return this.type === ChannelType.GUILD_PRIVATE_THREAD; + } + + isPublicThread() { + return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD; + } + + // Does the channel support sending messages ( eg categories do not ) + isWritable() { + const disallowedChannelTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STAGE_VOICE]; + return disallowedChannelTypes.indexOf(this.type) == -1; + } + + async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise { + if (this.isDm()) return this.owner_id == (opts.user_id ?? opts.user?.id) ? Permissions.ALL : Permissions.DEFAULT_DM_PERMISSIONS; + let guild = opts.guild; + if (!guild) { + if (this.guild) guild = this.guild; + else if (this.guild_id) guild = await Guild.findOneOrFail({ where: { id: this.guild_id } }); + else { + console.error("Channel.getUserPermissions: called without guild for non-DM channel."); + return Permissions.NONE; + } + } + + // check if we can resolve here to short-circuit possibly calling the database unnecessarily + // TODO: do we want to have an instance-wide opt out of this behavior? It would just be an extra if statement here + const ownerId = guild?.owner?.id ?? guild?.owner_id; + if (!!opts.user_id && ownerId === opts.user_id) return Permissions.ALL; + if (!!opts.user?.id && ownerId === opts.user?.id) return Permissions.ALL; + if (!!opts.member?.id && ownerId === opts.member?.id) return Permissions.ALL; + + let member = opts.member; + if (!member) { + if (opts.user) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user.id }, relations: { roles: true } }); + else if (opts.user_id) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user_id }, relations: { roles: true } }); + else { + console.error("Channel.getUserPermissions: called without user or member for non-DM channel."); + return Permissions.NONE; + } + } + + const roles = ( + member.roles || + ( + await Member.findOneOrFail({ + where: { guild_id: guild.id, index: member.index }, + relations: { roles: true }, + select: { + roles: { + id: true, + permissions: true, + position: true, + }, + }, + loadEagerRelations: false, + }) + ).roles + ).sort((a, b) => a.position - b.position); // ascending by position + + return Permissions.finalPermission({ + user: { + ...member, + roles: roles.map((r) => r.id), + flags: member.user?.flags ?? (await User.findOneOrFail({ where: { id: member.id }, select: { flags: true } })).flags, + }, + guild: { id: guild.id, owner_id: guild.owner_id!, roles }, // We don't care about including *all* guild roles, as not all of them are relevant... + channel: this, + }); + } + + // TODO: should we throw for missing args? + async canViewChannel(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise { + if (this.isDm()) return await this.canViewDmChannel(opts.user_id, opts.user); + + const userPerms = await this.getUserPermissions(opts); + return userPerms.has("VIEW_CHANNEL"); + } + + private async canViewDmChannel(user_id?: string, user?: User): Promise { + const userId = user_id ?? user?.id; + if (!userId) { + console.error("Channel.canViewChannel: called without user for DM channel."); + return false; + } + if (!user) return false; + if (this.recipients) return this.recipients.some((r) => r.user_id === user.id && !r.closed); + else { + // we dont have recipients on hand + const recipient = await Recipient.findOne({ where: { channel_id: this.id, user_id: user.id } }); + return recipient == null ? false : !recipient.closed; + } + } + + toJSON(): PublicChannel { + return { + ...this, + last_pin_timestamp: this.last_pin_timestamp?.toISOString(), + guild_id: this.guild_id ?? undefined, + recipients: undefined, //this.recipients?.map(x=>x.user.toPublicUser()), // TODO: fix me + owner: undefined, // TODO: fix me - this is thread owner + + // these fields are not returned depending on the type of channel + bitrate: this.bitrate || undefined, + user_limit: this.user_limit || undefined, + rate_limit_per_user: this.rate_limit_per_user || undefined, + owner_id: this.owner_id || undefined, + ...(this.isThread() && this.thread_members ? { member_ids_preview: this.thread_members.map((_) => _.member.id) } : {}), + default_auto_archive_duration: this.default_auto_archive_duration ?? undefined, + retention_policy_id: undefined, + thread_metadata: this.thread_metadata + ? { + ...this.thread_metadata, + archive_timestamp: new Date(this.thread_metadata.archive_timestamp).toISOString().replace("Z", "+00:00"), + create_timestamp: new Date(this.thread_metadata.create_timestamp).toISOString().replace("Z", "+00:00"), + } + : undefined, + member_count: this.member_count ?? undefined, + message_count: this.message_count ?? undefined, + total_message_sent: this.total_message_sent ?? undefined, + applied_tags: this.applied_tags ?? undefined, + permission_overwrites: this.isThread() ? undefined : this.permission_overwrites, + }; + } +} diff --git a/src/database/entities/ClientRelease.ts b/src/database/entities/ClientRelease.ts new file mode 100644 index 00000000..fc8fe86c --- /dev/null +++ b/src/database/entities/ClientRelease.ts @@ -0,0 +1,43 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "client_release", +}) +export class ClientRelease extends BaseClass { + @Column() + name: string; + + @Column() + pub_date: Date; + + @Column() + url: string; + + @Column() + platform: string; + + @Column() + enabled: boolean; + + @Column({ nullable: true }) + notes?: string; +} diff --git a/src/database/entities/CloudAttachment.ts b/src/database/entities/CloudAttachment.ts new file mode 100644 index 00000000..408df024 --- /dev/null +++ b/src/database/entities/CloudAttachment.ts @@ -0,0 +1,81 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { Channel } from "./Channel"; + +@Entity({ + name: "cloud_attachments", +}) +export class CloudAttachment extends BaseClass { + // Internal tracking metadata + @Column({ name: "user_id", nullable: true }) + @RelationId((att: CloudAttachment) => att.user) + userId: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" }) + user?: User; + + @Column({ name: "channel_id", nullable: true }) + @RelationId((att: CloudAttachment) => att.channel) + channelId?: string; // channel the file is uploaded to + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { nullable: true, onDelete: "SET NULL" }) + channel?: Channel; // channel the file is uploaded to + + @Column({ name: "upload_filename" }) + uploadFilename: string; + + // User-provided info + @Column({ name: "user_attachment_id", nullable: true }) + userAttachmentId?: string; + + @Column({ name: "user_filename" }) + userFilename: string; // name of file attached + + @Column({ name: "user_file_size", nullable: true }) + userFileSize?: number; // size of file in bytes + + @Column({ name: "user_original_content_type", nullable: true }) + userOriginalContentType?: string; + + @Column({ name: "user_is_clip", nullable: true }) + userIsClip?: boolean; // whether the file is a clip + + // Actual file info, initialised after upload + @Column({ nullable: true }) + size?: number; // size of file in bytes + + @Column({ nullable: true }) + height?: number; // height of file (if image) + + @Column({ nullable: true }) + width?: number; // width of file (if image) + + @Column({ name: "content_type", nullable: true }) + contentType?: string; + + // @BeforeRemove() + // onDelete() { + // return deleteFile(new URL(this.url).pathname); + // } +} diff --git a/src/database/entities/Config.ts b/src/database/entities/Config.ts new file mode 100644 index 00000000..7d4e05af --- /dev/null +++ b/src/database/entities/Config.ts @@ -0,0 +1,31 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, PrimaryColumn } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; + +@Entity({ + name: "config", +}) +export class ConfigEntity extends BaseClassWithoutId { + @PrimaryColumn() + key: string; + + @Column({ type: "simple-json", nullable: true }) + value: number | boolean | null | string | undefined; +} diff --git a/src/database/entities/ConnectedAccount.ts b/src/database/entities/ConnectedAccount.ts new file mode 100644 index 00000000..d0ed8097 --- /dev/null +++ b/src/database/entities/ConnectedAccount.ts @@ -0,0 +1,83 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { ConnectedAccountTokenData } from "@spacebar/schemas"; + +@Entity({ + name: "connected_accounts", +}) +export class ConnectedAccount extends BaseClass { + @Column() + external_id: string; + + @Column({ nullable: true }) + @RelationId((account: ConnectedAccount) => account.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ select: false }) + friend_sync?: boolean = false; + + @Column() + name: string; + + @Column({ select: false }) + revoked?: boolean = false; + + @Column({ select: false }) + show_activity?: number = 0; + + @Column() + type: string; + + @Column() + verified?: boolean = true; + + @Column({ select: false }) + visibility?: number = 0; + + @Column({ type: "varchar", array: true }) + integrations?: string[] = []; + + @Column({ type: "jsonb", name: "metadata", nullable: true }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata_?: any; + + @Column() + metadata_visibility?: number = 0; + + @Column() + two_way_link?: boolean = false; + + @Column({ select: false, nullable: true, type: "jsonb" }) + token_data?: ConnectedAccountTokenData | null; + + async revoke() { + this.revoked = true; + this.token_data = null; + await this.save(); + } +} diff --git a/src/database/entities/ConnectionConfigEntity.ts b/src/database/entities/ConnectionConfigEntity.ts new file mode 100644 index 00000000..9ad2af8b --- /dev/null +++ b/src/database/entities/ConnectionConfigEntity.ts @@ -0,0 +1,31 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, PrimaryColumn } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; + +@Entity({ + name: "connection_config", +}) +export class ConnectionConfigEntity extends BaseClassWithoutId { + @PrimaryColumn() + key: string; + + @Column({ type: "simple-json", nullable: true }) + value: number | boolean | null | string | Date | undefined; +} diff --git a/src/database/entities/EmbedCache.ts b/src/database/entities/EmbedCache.ts new file mode 100644 index 00000000..b34be18c --- /dev/null +++ b/src/database/entities/EmbedCache.ts @@ -0,0 +1,38 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { BaseClass } from "./BaseClass"; +import { Entity, Column } from "typeorm"; +import { Embed } from "@spacebar/schemas"; + +@Entity({ + name: "embed_cache", +}) +export class EmbedCache extends BaseClass { + @Column() + url: string; + + @Column({ type: "jsonb", nullable: true }) + embed?: Embed; + + @Column({ type: "jsonb", nullable: true }) + embeds?: Embed[]; + + @Column({ name: "created_at", type: "timestamp with time zone" }) + createdAt: Date; +} diff --git a/src/database/entities/Emoji.ts b/src/database/entities/Emoji.ts new file mode 100644 index 00000000..f75cb03f --- /dev/null +++ b/src/database/entities/Emoji.ts @@ -0,0 +1,65 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { User } from "./index"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; + +@Entity({ + name: "emojis", +}) +export class Emoji extends BaseClass { + @Column() + animated: boolean; + + @Column() + available: boolean; // whether this emoji can be used, may be false due to various reasons + + @Column() + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.emojis, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + @RelationId((emoji: Emoji) => emoji.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User) + user: User; + + @Column() + managed: boolean; + + @Column() + name: string; + + @Column() + require_colons: boolean; + + @Column({ type: "int8", array: true }) + roles: string[]; // roles this emoji is whitelisted to (new discord feature?) + + @Column({ type: "int8", array: true, nullable: true }) + groups: string[]; // user groups this emoji is whitelisted to (Spacebar extension) +} diff --git a/src/database/entities/Encryption.ts b/src/database/entities/Encryption.ts new file mode 100644 index 00000000..9c76701a --- /dev/null +++ b/src/database/entities/Encryption.ts @@ -0,0 +1,43 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "security_settings", +}) +export class SecuritySettings extends BaseClass { + @Column({ nullable: true }) + guild_id: string; + + @Column({ nullable: true }) + channel_id: string; + + @Column() + encryption_permission_mask: number; + + @Column({ type: "varchar", array: true }) + allowed_algorithms: string[]; + + @Column() + current_algorithm: string; + + @Column({ nullable: true }) + used_since_message: string; +} diff --git a/src/database/entities/Guild.ts b/src/database/entities/Guild.ts new file mode 100644 index 00000000..ab56b76a --- /dev/null +++ b/src/database/entities/Guild.ts @@ -0,0 +1,492 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { arrayRemove } from "@spacebar/extensions"; +import { Config, GuildWelcomeScreen, Snowflake, handleFile } from "@spacebar/util"; +import { Ban } from "./Ban"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { Emoji } from "./Emoji"; +import { Invite } from "./Invite"; +import { Member } from "./Member"; +import { Role } from "./Role"; +import { Sticker } from "./Sticker"; +import { Template } from "./Template"; +import { User } from "./User"; +import { VoiceState } from "./VoiceState"; +import { Webhook } from "./Webhook"; +// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0} +// TODO: guild_scheduled_events +// TODO: stage_instances +// TODO: threads +// TODO: +// "keywords": [ +// "Genshin Impact", +// "Paimon", +// "Honkai Impact", +// "ARPG", +// "Open-World", +// "Waifu", +// "Anime", +// "Genshin", +// "miHoYo", +// "Gacha" +// ], + +export const PublicGuildRelations = [ + "channels", + "emojis", + "roles", + "stickers", + "voice_states", + // "members", // TODO: These are public, but all members should not be fetched. + // "members.user", +]; + +@Entity({ + name: "guilds", +}) +export class Guild extends BaseClass { + @Column({ type: String, nullable: true }) + @RelationId((guild: Guild) => guild.afk_channel) + afk_channel_id?: string | null; + + @JoinColumn({ name: "afk_channel_id" }) + @ManyToOne(() => Channel) + afk_channel?: Channel; + + @Column({ nullable: true }) + afk_timeout?: number; + + // * commented out -> use owner instead + // application id of the guild creator if it is bot-created + // @Column({ nullable: true }) + // application?: string; + + @JoinColumn({ name: "ban_ids" }) + @OneToMany(() => Ban, (ban: Ban) => ban.guild, { + cascade: true, + orphanedRowAction: "delete", + }) + bans: Ban[]; + + @Column({ nullable: true }) + banner?: string; + + @Column({ nullable: true }) + default_message_notifications?: number; + + @Column({ nullable: true }) + description?: string; + + @Column({ nullable: true }) + discovery_splash?: string; + + @Column({ nullable: true }) + explicit_content_filter?: number; + + @Column({ type: "varchar", array: true }) + features: string[] = []; //TODO use enum + //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features + + @Column({ nullable: true }) + primary_category_id?: string; // TODO: this was number? + + @Column({ nullable: true }) + icon?: string; + + @Column() + large?: boolean = false; + + @Column({ nullable: true }) + max_members?: number; + + @Column({ nullable: true }) + max_presences?: number; + + @Column({ nullable: true }) + max_video_channel_users?: number; + + @Column({ nullable: true }) + member_count?: number; + + @Column({ nullable: true }) + presence_count?: number; // users online + + @OneToMany(() => Member, (member: Member) => member.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + members: Member[]; + + @JoinColumn({ name: "role_ids" }) + @OneToMany(() => Role, (role: Role) => role.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + roles: Role[]; + + @JoinColumn({ name: "channel_ids" }) + @OneToMany(() => Channel, (channel: Channel) => channel.guild, { + cascade: true, + orphanedRowAction: "delete", + }) + channels: Channel[]; + + @Column({ nullable: true }) + @RelationId((guild: Guild) => guild.template) + template_id?: string; + + @JoinColumn({ name: "template_id", referencedColumnName: "id" }) + @ManyToOne(() => Template) + template: Template; + + @JoinColumn({ name: "emoji_ids" }) + @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + emojis: Emoji[]; + + @JoinColumn({ name: "sticker_ids" }) + @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + stickers: Sticker[]; + + @JoinColumn({ name: "invite_ids" }) + @OneToMany(() => Invite, (invite: Invite) => invite.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + invites: Invite[]; + + @JoinColumn({ name: "voice_state_ids" }) + @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + voice_states: VoiceState[]; + + @JoinColumn({ name: "webhook_ids" }) + @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, { + cascade: true, + orphanedRowAction: "delete", + onDelete: "CASCADE", + }) + webhooks: Webhook[]; + + @Column({ nullable: true }) + mfa_level?: number; + + @Column() + name: string; + + @Column({ nullable: true }) + @RelationId((guild: Guild) => guild.owner) + owner_id?: string; // optional to allow for ownerless guilds + + @JoinColumn({ name: "owner_id", referencedColumnName: "id" }) + @ManyToOne(() => User) + owner?: User; // optional to allow for ownerless guilds + + @Column({ nullable: true }) + preferred_locale?: string; + + @Column({ nullable: true }) + premium_subscription_count?: number; + + @Column() + premium_tier?: number; // crowd premium level + + @Column({ type: String, nullable: true }) + @RelationId((guild: Guild) => guild.public_updates_channel) + public_updates_channel_id: string | null; + + @JoinColumn({ name: "public_updates_channel_id" }) + @ManyToOne(() => Channel) + public_updates_channel?: Channel; + + @Column({ type: String, nullable: true }) + @RelationId((guild: Guild) => guild.rules_channel) + rules_channel_id?: string | null; + + @JoinColumn({ name: "rules_channel_id" }) + @ManyToOne(() => Channel) + rules_channel?: string; + + @Column({ nullable: true }) + region?: string; + + @Column({ nullable: true }) + splash?: string; + + @Column({ type: String, nullable: true }) + @RelationId((guild: Guild) => guild.system_channel) + system_channel_id?: string | null; + + @JoinColumn({ name: "system_channel_id" }) + @ManyToOne(() => Channel) + system_channel?: Channel; + + @Column({ nullable: true }) + system_channel_flags?: number; + + @Column() + unavailable: boolean = false; + + @Column({ nullable: true }) + verification_level?: number; + + /** + * DEPRECATED: Look at the new Guild onboarding screens. + */ + @Column({ type: "jsonb" }) + welcome_screen: GuildWelcomeScreen; + + @Column({ nullable: true, type: "int8" }) + @RelationId((guild: Guild) => guild.widget_channel) + widget_channel_id?: string; + + @JoinColumn({ name: "widget_channel_id" }) + @ManyToOne(() => Channel) + widget_channel?: Channel; + + @Column() + widget_enabled: boolean = true; + + @Column({ nullable: true }) + nsfw_level?: number; + + @Column() + nsfw: boolean = false; + + // TODO: nested guilds + @Column({ nullable: true }) + parent?: string; + + // only for developer portal + permissions?: number; + + //new guild settings, 11/08/2022: + @Column({ nullable: true }) + premium_progress_bar_enabled: boolean = false; + + @Column({ select: false, type: "int8", array: true }) + channel_ordering: string[]; + + @Column() + discovery_weight: number = 0; + + @Column() + discovery_excluded: boolean = false; + + async ToGuildSource() { + if (!this.features.includes("DISCOVERABLE")) { + return null; + } + return { + id: this.id, + name: this.name, + icon: this.icon, + description: this.description, + banner: this.banner, + splash: this.splash, + discovery_splash: this.discovery_splash, + features: this.features, + vanity_url_code: null, + preferred_locale: this.preferred_locale || "en", + premium_subscription_count: this.premium_subscription_count, + approximate_member_count: await Member.countBy({ + guild_id: this.id, + }), + approximate_presence_count: await Member.countBy({ + guild_id: this.id, + user: { + sessions: { + status: "online", + }, + }, + }), + emojis: this.emojis ?? undefined, + emoji_count: this.emojis ? this.emojis.length : undefined, + stickers: this.stickers ?? undefined, + sticker_count: this.stickers ? this.stickers.length : undefined, + auto_removed: false, + primary_category_id: this.primary_category_id, + keywords: [], + is_published: false, + reasons_to_join: [], + }; + } + + static async createGuild(body: { + name?: string; + icon?: string | null; + owner_id?: string; + roles?: Partial[]; + channels?: Partial[]; + source_guild_id: string | null; + }) { + const guild_id = Snowflake.generate(); + + const guild = await Guild.create({ + id: guild_id, + name: body.name || "Spacebar", + icon: await handleFile(`/icons/${guild_id}`, body.icon as string), + owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds + presence_count: 0, + member_count: 0, // will automatically be increased by addMember() + mfa_level: 0, + preferred_locale: "en-US", + premium_subscription_count: 0, + premium_tier: 0, + system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance + nsfw_level: 0, + verification_level: 0, + welcome_screen: { + enabled: false, + description: "", + welcome_channels: [], + }, + channel_ordering: [], + afk_timeout: Config.get().defaults.guild.afkTimeout, + default_message_notifications: Config.get().defaults.guild.defaultMessageNotifications, + explicit_content_filter: Config.get().defaults.guild.explicitContentFilter, + features: Config.get().guild.defaultFeatures, + max_members: Config.get().limits.guild.maxMembers, + max_presences: Config.get().defaults.guild.maxPresences, + max_video_channel_users: Config.get().defaults.guild.maxVideoChannelUsers, + region: Config.get().regions.default, + }).save(); + + // we have to create the role _after_ the guild because else we would get a foreign key error + // TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage + await Role.create({ + id: guild_id, + guild_id: guild_id, + color: 0, + colors: { primary_color: 0 }, + hoist: false, + managed: false, + mentionable: false, + name: "@everyone", + permissions: "2251804225", + position: 0, + icon: undefined, + unicode_emoji: undefined, + flags: 0, // TODO? + }).save(); + + // create custom roles if provided + if (body.roles && body.roles.length) { + await Promise.all( + body.roles?.map( + (role) => + new Promise((resolve) => { + Role.create({ + ...role, + guild_id, + id: + // role.id === body.template_guild_id indicates that this is the @everyone role + role.id === body.source_guild_id || role.id == "0" ? guild_id : Snowflake.generate(), + }) + .save() + .then(resolve); + }), + ), + ); + } + + if (!body.channels || !body.channels.length) { + body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }]; + } + + const ids = new Map(); + + body.channels.forEach((x) => { + if (x.id) { + ids.set(x.id, Snowflake.generate()); + } + }); + + for (const channel of body.channels.sort((a) => (a.parent_id ? 1 : -1))) { + const id = ids.get(channel.id) || Snowflake.generate(); + + const parent_id = ids.get(channel.parent_id); + + const saved = await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, { + keepId: true, + skipExistsCheck: true, + skipPermissionCheck: true, + skipEventEmit: true, + }); + + await Guild.insertChannelInOrder(guild.id, saved.id, parent_id ?? channel.position ?? 0, guild); + } + + return guild; + } + + /** Insert a channel into the guild ordering by parent channel id or position */ + static async insertChannelInOrder(guild_id: string, channel_id: string, position: number, guild?: Guild): Promise; + static async insertChannelInOrder(guild_id: string, channel_id: string, parent_id: string, guild?: Guild): Promise; + static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise; + static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise { + if (!guild) + guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: { channel_ordering: true }, + }); + + guild.channel_ordering ??= []; + + let position; + if (typeof insertPoint == "string") position = guild.channel_ordering.indexOf(insertPoint) + 1; + else position = insertPoint; + + arrayRemove(guild.channel_ordering, channel_id); + + guild.channel_ordering.splice(position, 0, channel_id); + await Guild.update({ id: guild_id }, { channel_ordering: guild.channel_ordering }); + return position; + } + + toJSON(): Guild { + return { + ...this, + unavailable: this.unavailable == false ? undefined : true, + channel_ordering: undefined, + discovery_weight: undefined, + discovery_excluded: undefined, + parent: undefined, + primary_category_id: undefined, + nsfw: undefined, + template_id: undefined, + presence_count: undefined, + }; + } +} diff --git a/src/database/entities/InstanceBan.ts b/src/database/entities/InstanceBan.ts new file mode 100644 index 00000000..03424e5a --- /dev/null +++ b/src/database/entities/InstanceBan.ts @@ -0,0 +1,111 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, CreateDateColumn, Entity, FindOptionsWhere, Index, JoinColumn, OneToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "instance_bans", +}) +export class InstanceBan extends BaseClass { + @Column({ type: "bigint" }) + @CreateDateColumn() + created_at: Date = new Date(); + + @Column() + reason: string; + + @Index() + @Column({ nullable: true }) + user_id?: string; + + @Index() + @Column({ nullable: true }) + fingerprint?: string; + + @Index() + @Column({ nullable: true }) + ip_address?: string; + + // chain of trust type tracking + + @Column({ default: false }) + is_allowlisted: boolean = false; + + @Column({ default: false }) + is_from_other_instance_ban: boolean = false; + + @Column({ nullable: true }) + @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban) + origin_instance_ban_id?: string; + + @JoinColumn({ name: "origin_instance_ban_id" }) + @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" }) + origin_instance_ban?: InstanceBan; + + static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) { + const optionalChecks: FindOptionsWhere[] = [{ user_id: opts.userId }]; + if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress }); + if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint }); + const instanceBans = await InstanceBan.find({ where: optionalChecks }); + + const banReasons = []; + for (const ban of instanceBans) { + if (ban.is_allowlisted) continue; + if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint"); + if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress"); + if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId"); + } + + const banViralityPromises: Promise[] = []; + if (opts.propagateBan && banReasons.length > 0) { + if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + ip_address: opts.ipAddress, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + fingerprint: opts.fingerprint, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId)) + banViralityPromises.push( + InstanceBan.create({ + user_id: opts.userId, + reason: "Propagated from other instance ban", + is_from_other_instance_ban: true, + origin_instance_ban: instanceBans[0], + }).save(), + ); + } + + await Promise.all(banViralityPromises); + return banReasons; + } +} diff --git a/src/database/entities/Invite.ts b/src/database/entities/Invite.ts new file mode 100644 index 00000000..cc5374eb --- /dev/null +++ b/src/database/entities/Invite.ts @@ -0,0 +1,126 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; +import { Channel } from "./Channel"; +import { Guild } from "./Guild"; +import { Member } from "./Member"; +import { User } from "./User"; + +export const PublicInviteRelation = ["inviter", "guild", "channel"]; + +@Entity({ + name: "invites", +}) +export class Invite extends BaseClassWithoutId { + @PrimaryColumn() + code: string; + + @Column() + temporary: boolean; + + @Column() + uses: number; + + @Column() + max_uses: number; + + @Column() + max_age: number; + + @Column() + created_at: Date; + + @Column({ nullable: true }) + expires_at?: Date; + + @Column({ nullable: true }) + @RelationId((invite: Invite) => invite.guild) + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.invites, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + @RelationId((invite: Invite) => invite.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column({ nullable: true }) + @RelationId((invite: Invite) => invite.inviter) + inviter_id?: string; + + @JoinColumn({ name: "inviter_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + inviter: User; + + @Column({ nullable: true }) + @RelationId((invite: Invite) => invite.target_user) + target_user_id: string; + + @JoinColumn({ name: "target_user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + target_user?: string; // could be used for "User specific invites" https://github.com/spacebarchat/server/issues/326 + + @Column({ nullable: true }) + target_user_type?: number; + + @Column({ nullable: true }) + vanity_url?: boolean; + + @Column() + flags: number; + + isExpired() { + if (this.max_age !== 0 && this.expires_at && this.expires_at < new Date()) return true; + if (this.max_uses !== 0 && this.uses >= this.max_uses) return true; + return false; + } + toPublicJSON() { + return { + ...this, + inviter: this.inviter.toPublicUser(), + }; + } + + static async joinGuild(user_id: string, code: string) { + const invite = await Invite.findOneOrFail({ where: { code } }); + if (invite.isExpired()) { + await Invite.delete({ code }); + throw new Error("Invite is expired"); + } + if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code }); + else await invite.save(); + + await Member.addToGuild(user_id, invite.guild_id); + return invite; + } +} diff --git a/src/database/entities/Member.ts b/src/database/entities/Member.ts new file mode 100644 index 00000000..b9fbac3d --- /dev/null +++ b/src/database/entities/Member.ts @@ -0,0 +1,488 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { HTTPError } from "lambert-server/HTTPError"; +import { BeforeInsert, BeforeUpdate, Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, Not, PrimaryGeneratedColumn, RelationId } from "typeorm"; +import { Ban, Channel, PublicGuildRelations } from "./index"; +import { ReadyGuildDTO } from "../../util/dtos"; +import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } from "../../util/interfaces"; +import { Config, emitEvent, DiscordApiErrors, Stopwatch } from "../../util/util"; +import { BaseClassWithoutId } from "./BaseClass"; +import { Guild } from "./Guild"; +import { Message } from "./Message"; +import { Role } from "./Role"; +import { User } from "./User"; +import { AvatarDecorationData, Collectibles, DisplayNameStyle, PublicMember, PublicMemberProjection, UserGuildSettings } from "@spacebar/schemas"; + +export const MemberPrivateProjection: (keyof Member)[] = [ + "id", + "guild", + "guild_id", + "deaf", + "joined_at", + "last_message_id", + "mute", + "nick", + "pending", + "premium_since", + "roles", + "settings", + "user", + "avatar", + "banner", + "bio", + "theme_colors", + "pronouns", + "communication_disabled_until", + "flags", +]; + +@Entity({ + name: "members", +}) +@Index(["id", "guild_id"], { unique: true }) +export class Member extends BaseClassWithoutId { + @PrimaryGeneratedColumn() + index: string; + + @Column() + @RelationId((member: Member) => member.user) + id: string; + + @JoinColumn({ name: "id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column() + @RelationId((member: Member) => member.guild) + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + nick?: string; + + @JoinTable({ + name: "member_roles", + joinColumn: { name: "index", referencedColumnName: "index" }, + inverseJoinColumn: { + name: "role_id", + referencedColumnName: "id", + }, + }) + @ManyToMany(() => Role, { cascade: true }) + roles: Role[]; + + @Column() + joined_at: Date; + + @Column({ type: "bigint", nullable: true }) + premium_since?: number; + + @Column() + deaf: boolean; + + @Column() + mute: boolean; + + @Column() + pending: boolean; + + @Column({ type: "jsonb", select: false }) + settings: UserGuildSettings; + + @Column({ nullable: true }) + last_message_id?: string; + + /** + @JoinColumn({ name: "id" }) + @ManyToOne(() => User, { + onDelete: "DO NOTHING", + // do not auto-kick force-joined members just because their joiners left the server + }) **/ + @Column({ nullable: true }) + joined_by: string; + + @Column({ nullable: true }) + avatar?: string; + + @Column({ nullable: true }) + banner: string; + + @Column() + bio: string; + + @Column({ nullable: true, type: "int4", array: true }) + theme_colors?: number[]; // TODO: Separate `User` and `UserProfile` models + + @Column({ nullable: true }) + pronouns?: string; + + @Column({ nullable: true, type: Date }) + communication_disabled_until: Date | null; + + // TODO: add this when we have proper read receipts + // @Column({ type: "jsonb" }) + // read_state: ReadState; + + @Column({ type: "jsonb", nullable: true }) + avatar_decoration_data?: AvatarDecorationData; + + @Column({ type: "jsonb", nullable: true }) + display_name_styles?: DisplayNameStyle; + + @Column({ type: "jsonb", nullable: true }) + collectibles?: Collectibles; + + @Column({ type: "int", default: 0 }) + flags: number = 0; + + @BeforeUpdate() + @BeforeInsert() + validate() { + if (this.nick) { + this.nick = this.nick.split("\n").join(""); + this.nick = this.nick.split("\t").join(""); + } + if (this.nick === "") this.nick = undefined; + if (this.pronouns === "") this.pronouns = undefined; + } + + static async IsInGuildOrFail(user_id: string, guild_id: string) { + if ( + await Member.count({ + where: { id: user_id, guild_id }, + }) + ) + return; + throw new HTTPError("You are not member of this guild", 403); + } + + static async removeFromGuild(user_id: string, guild_id: string) { + const guild = await Guild.findOneOrFail({ + select: { owner_id: true }, + where: { id: guild_id }, + }); + if (guild.owner_id === user_id) throw new Error("The owner cannot be removed from the guild"); + const member = await Member.findOneOrFail({ + where: { id: user_id, guild_id }, + relations: { user: true }, + }); + + // use promise all to execute all promises at the same time -> save time + return Promise.all([ + Member.delete({ + id: user_id, + guild_id, + }), + Guild.decrement({ id: guild_id }, "member_count", 1), + + emitEvent({ + event: "GUILD_DELETE", + data: { + id: guild_id, + }, + user_id: user_id, + } satisfies GuildDeleteEvent), + emitEvent({ + event: "GUILD_MEMBER_REMOVE", + data: { guild_id, user: member.user.toPublicUser() }, + guild_id, + } satisfies GuildMemberRemoveEvent), + ]); + } + + static async addRole(user_id: string, guild_id: string, role_id: string) { + const [member] = await Promise.all([ + Member.findOneOrFail({ + where: { id: user_id, guild_id }, + relations: { user: true, roles: true }, // we don't want to load the role objects just the ids + select: { + index: true, + roles: { + id: true, + }, + }, + }), + Role.findOneOrFail({ + where: { id: role_id, guild_id }, + select: { id: true }, + }), + ]); + member.roles.push(Role.create({ id: role_id })); + + await Promise.all([ + member.save(), + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + data: { + guild_id, + user: member.user, + roles: member.roles.map((x) => x.id), + }, + guild_id, + } satisfies GuildMemberUpdateEvent), + ]); + } + + static async removeRole(user_id: string, guild_id: string, role_id: string) { + const [member] = await Promise.all([ + Member.findOneOrFail({ + where: { id: user_id, guild_id }, + relations: { user: true, roles: true }, // we don't want to load the role objects just the ids + select: { + index: true, + roles: { + id: true, + }, + }, + }), + Role.findOneOrFail({ where: { id: role_id, guild_id } }), + ]); + member.roles = member.roles.filter((x) => x.id !== role_id); + + await Promise.all([ + member.save(), + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + data: { + guild_id, + user: member.user, + roles: member.roles.map((x) => x.id), + }, + guild_id, + } satisfies GuildMemberUpdateEvent), + ]); + } + + static async changeNickname(user_id: string, guild_id: string, nickname: string) { + const member = await Member.findOneOrFail({ + where: { + id: user_id, + guild_id, + }, + relations: { user: true, roles: true }, + }); + + // @ts-expect-error Member nickname is nullable + member.nick = nickname || null; + + await Promise.all([ + member.save(), + + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + data: { + guild_id, + user: member.user, + nick: nickname || undefined, + roles: member.roles.map((x) => x.id), + }, + guild_id, + } satisfies GuildMemberUpdateEvent), + ]); + } + + static async addToGuild(user_id: string, guild_id: string, isRegistration: boolean = false) { + const totalSw = Stopwatch.startNew(); + const incSw = Stopwatch.startNew(); + const logTrace = (...data: unknown[]) => { + if (process.env.LOG_VERBOSE_TRACES !== "true") return; + console.log("[Member.addToGuild]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); + }; + + if (!isRegistration) { + const isBanned = Ban.exists({ where: { guild_id, user_id } }); + const isMember = Member.exists({ where: { id: user_id, guild_id } }); + + if (await isBanned) throw DiscordApiErrors.USER_BANNED; + logTrace("Check bans"); + + if (await isMember) throw new HTTPError("You are already a member of this guild", 400); + logTrace("Check existing membership"); + + const { maxGuilds } = Config.get().limits.user; + const guild_count = await Member.count({ where: { id: user_id } }); + if (guild_count >= maxGuilds) { + throw new HTTPError(`You are at the ${maxGuilds} guild limit.`, 403); + } + logTrace("Enforce max guilds"); + } + + const guild = await Guild.findOneOrFail({ + where: { + id: guild_id, + }, + relations: PublicGuildRelations, + relationLoadStrategy: "query", + }); + const channelPositionsGuild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: { channel_ordering: true }, + }); + logTrace("Find guild"); + + const newMember = Member.create({ + id: user_id, + guild_id, + nick: undefined, + joined_at: new Date(), + deaf: false, + mute: false, + pending: false, + bio: "", + roles: [Role.create({ id: guild_id })], // @everyone role + // read_state: {}, + settings: { + guild_id: null, + mute_config: null, + mute_scheduled_events: false, + flags: 0, + hide_muted_channels: false, + notify_highlights: 0, + channel_overrides: {}, + message_notifications: guild.default_message_notifications, + mobile_push: true, + muted: false, + suppress_everyone: false, + suppress_roles: false, + version: 0, + }, + // Member.save is needed because else the roles relations wouldn't be updated + }); + + let memberCount = 0; + let memberPreview: PublicMember[] = []; + if (!isRegistration) { + for await (const channel of guild.channels) { + channel.position = await Channel.calculatePosition(channel.id, guild_id, channelPositionsGuild); + } + + logTrace("Reorder channels"); + + memberCount = isRegistration ? 0 : await Member.count({ where: { guild_id } }); + logTrace("Get member count"); + + memberPreview = ( + await Member.find({ + where: { + guild_id, + user: { + id: Not(user_id), + sessions: { + status: Not("invisible" as const), // lol typescript? + }, + }, + }, + relations: { user: true, roles: true }, + take: 10, + }) + ).map((member) => member.toPublicMember()); + logTrace("Calculate member preview"); + } + + const user = await User.getPublicUser(user_id); + logTrace("Get user"); + + await Promise.all([ + newMember.save(), // TODO: can we somehow insert the roles manually? We have no entity for this... Would skip a few select's + Guild.increment({ id: guild_id }, "member_count", 1), + emitEvent({ + event: "GUILD_MEMBER_ADD", + data: { + ...newMember.toPublicMember(), + user: user, + guild_id, + }, + guild_id, + origin: "util/entities/Member.ts:377/addToGuild(user_id, guild_id)", + } satisfies GuildMemberAddEvent), + isRegistration + ? null + : emitEvent({ + event: "GUILD_CREATE", + data: { + ...new ReadyGuildDTO(guild).toJSON(), + members: [...memberPreview, { ...newMember.toPublicMember(), user }], + member_count: memberCount + 1, + guild_hashes: {}, + guild_scheduled_events: [], + joined_at: newMember.joined_at, + presences: [], + stage_instances: [], + threads: [], + embedded_activities: [], + voice_states: guild.voice_states.map((x) => x.toPublicVoiceState()), + }, + user_id, + } satisfies GuildCreateEvent), + ]); + logTrace("Save member info"); + + const welcomeChannelId = guild.system_channel_id; + if (welcomeChannelId && (await Channel.exists({ where: { id: welcomeChannelId } }))) { + // Send a welcome message + const message = Message.create({ + type: 7, + guild_id: guild.id, + channel_id: welcomeChannelId, + author: user, + timestamp: new Date(), + reactions: [], + attachments: [], + embeds: [], + sticker_items: [], + edited_timestamp: undefined, + mentions: [], + mention_channels: [], + mention_roles: [], + mention_everyone: false, + }); + + await Promise.all([ + message.insert(), + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: message.channel_id, + data: message.toJSON(), + } satisfies MessageCreateEvent), + Channel.update({ id: welcomeChannelId }, { last_message_id: message.id }), + ]); + logTrace("Send welcome message"); + } + } + + toPublicMember() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const member: any = {}; + PublicMemberProjection.forEach((x) => { + member[x] = this[x]; + }); + + if (this.roles) member.roles = this.roles.map((x: Role) => x.id); + if (this.user) member.user = this.user.toPublicUser(); + + return member as PublicMember; + } +} diff --git a/src/database/entities/Message.ts b/src/database/entities/Message.ts new file mode 100644 index 00000000..31e6a0e4 --- /dev/null +++ b/src/database/entities/Message.ts @@ -0,0 +1,486 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { User } from "./User"; +import { Member } from "./Member"; +import { Role } from "./Role"; +import { Channel } from "./Channel"; +import { Application } from "./Application"; +import { Column, CreateDateColumn, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId, FindOneOptions, Raw, Not, BaseEntity, In } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { Webhook } from "./Webhook"; +import { Sticker } from "./Sticker"; +import { Attachment } from "./Attachment"; +import { NewUrlUserSignatureData } from "../../util/Signing"; +import { + ApplicationCommandType, + BaseMessageComponents, + Embed, + MessageComponentType, + MessageSnapshot, + MessageType, + PartialMessage, + Poll, + PublicMessage, + Reaction, + UnfurledMediaItem, + PartialUser, + InteractionType, +} from "@spacebar/schemas"; +import { MessageFlags } from "@spacebar/util"; +import { JsonRemoveEmpty } from "../../util/util/Decorators"; + +@Entity({ + name: "messages", +}) +@Index(["channel_id", "id"], { unique: true }) +export class Message extends BaseClass { + @Column({ nullable: true }) + @RelationId((message: Message) => message.channel) + @Index() + channel_id?: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.thread) + @JsonRemoveEmpty + thread_id?: string; + + @JoinColumn({ name: "thread_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + @JsonRemoveEmpty + thread?: Channel; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.guild) + @JsonRemoveEmpty + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild?: Guild; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.author) + @Index() + author_id?: string; + + @JoinColumn({ name: "author_id", referencedColumnName: "id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + author?: User; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.member) + member_id?: string; + + @JoinColumn({ name: "member_id", referencedColumnName: "id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + member?: Member; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.webhook) + @JsonRemoveEmpty + webhook_id?: string; + + @JoinColumn({ name: "webhook_id" }) + @ManyToOne(() => Webhook) + webhook?: Webhook; + + @Column({ nullable: true }) + @RelationId((message: Message) => message.application) + application_id?: string; + + @JoinColumn({ name: "application_id" }) + @ManyToOne(() => Application) + application?: Application; + + @Column({ nullable: true }) + content?: string; + + @Column() + @CreateDateColumn() + timestamp: Date; + + @Column({ nullable: true }) + edited_timestamp?: Date; + + @Column({ nullable: true }) + tts?: boolean; + + @Column({ nullable: true }) + mention_everyone?: boolean; + + @JoinTable({ name: "message_user_mentions" }) + @ManyToMany(() => User) + mentions: User[]; + + @JoinTable({ name: "message_role_mentions" }) + @ManyToMany(() => Role) + mention_roles: Role[]; + + @JoinTable({ name: "message_channel_mentions" }) + @ManyToMany(() => Channel) + @JsonRemoveEmpty + mention_channels: Channel[]; + + @JoinTable({ name: "message_stickers" }) + @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) + @JsonRemoveEmpty + sticker_items?: Sticker[]; + + @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { + cascade: true, + orphanedRowAction: "delete", + }) + attachments?: Attachment[]; + + @Column({ type: "jsonb" }) + embeds: Embed[]; + + @Column({ type: "jsonb" }) + @JsonRemoveEmpty + reactions: Reaction[]; + + @Column({ type: "text", nullable: true }) + @JsonRemoveEmpty + nonce?: string; + + @Column({ nullable: true, type: Date }) + pinned_at?: Date | null; + + get pinned(): boolean { + return this.pinned_at != null; + } + + @Column({ type: "int" }) + type: MessageType; + + @Column({ type: "jsonb", nullable: true }) + @JsonRemoveEmpty + activity?: { + type: number; + party_id: string; + }; + + @Column({ default: 0 }) + flags: number; + + @Column({ type: "jsonb", nullable: true }) + @JsonRemoveEmpty + message_reference?: { + message_id?: string; + channel_id?: string; + guild_id?: string; + type?: number; // 0 = DEFAULT, 1 = FORWARD + }; + + @JoinColumn({ name: "message_reference_id" }) + @ManyToOne(() => Message, { onDelete: "SET NULL" }) + referenced_message?: Message | null; + + @Column({ type: "jsonb", nullable: true }) + @JsonRemoveEmpty + interaction?: { + id: string; + type: InteractionType; + name: string; + }; + + @Column({ type: "jsonb", nullable: true }) + @JsonRemoveEmpty + interaction_metadata?: { + id: string; + type: InteractionType; + user_id: string; + authorizing_integration_owners: object; + name: string; + command_type: ApplicationCommandType; + }; + + @Column({ type: "jsonb", nullable: true }) + components?: BaseMessageComponents[]; + + @Column({ type: "jsonb", nullable: true }) + @JsonRemoveEmpty + poll?: Poll; + + @Column({ nullable: true }) + username?: string; + + @Column({ nullable: true }) + avatar?: string; + + @Column({ default: "[]", type: "jsonb" }) + message_snapshots: MessageSnapshot[]; + + get isWebhook() { + return this.webhook_id != null && this.webhook != null; + } + + static async fillReplies(messages: Message[]) { + const ms = messages + .filter((msg) => msg.message_reference && !msg.referenced_message?.id && msg.message_reference.message_id) + .filter((msg) => [MessageType.REPLY, MessageType.THREAD_STARTER_MESSAGE, MessageType.CONTEXT_MENU_COMMAND].includes(msg.type)); + if (!ms.length) return; + const curMs = new Map(messages.map((m) => [m.id, m] as const)); + const neededIds = new Set(ms.map((m) => m.message_reference!.message_id as string)).difference(curMs); + if (neededIds.size) { + const newMessages = await Message.find({ + where: { + id: In([...neededIds]), + }, + relations: { author: true, mentions: true, mention_roles: true, mention_channels: true }, + }); + newMessages.forEach((msg) => curMs.set(msg.id, msg)); + } + for (const message of ms) { + message.referenced_message = curMs.get(message.message_reference!.message_id as string) || null; + } + } + + toJSON(shallow = false): PublicMessage { + // this.clean_data(); + return { + ...this, + channel_id: this.channel_id ?? this.channel.id, + channel: undefined, + + timestamp: this.timestamp.toISOString(), + edited_timestamp: this.edited_timestamp ? this.edited_timestamp.toISOString() : null, + + author_id: undefined, + member_id: undefined, + webhook_id: this.webhook_id ?? undefined, + application_id: undefined, + mentions: this.mentions?.map((user) => { + if (user && !user.toPublicUser) console.trace("toPublic user missing!!!"); + return (user?.toPublicUser?.() ?? user ?? undefined) as unknown as PartialUser; + }), + + mention_roles: this.mention_roles?.map((role) => role.id) ?? [], + mention_channels: this.mention_channels?.map((ch) => ch.toJSON()) ?? [], + attachments: this.attachments?.map((att) => att.toJSON()) ?? [], + + nonce: this.nonce ?? undefined, + tts: this.tts ?? false, + guild: this.guild ?? undefined, + webhook: this.webhook ?? undefined, + interaction: this.interaction ?? undefined, + interaction_metadata: this.interaction_metadata ?? undefined, + reactions: this.reactions ?? undefined, + sticker_items: this.sticker_items ?? undefined, + message_reference: this.message_reference ?? undefined, + mention_everyone: this.mention_everyone ?? false, + author: { + ...(this.author?.toPublicUser() ?? undefined), + // Webhooks + username: this.username ?? this.author?.username ?? null, + avatar: this.avatar ?? this.author?.avatar ?? null, + }, + activity: this.activity ?? undefined, + application: this.application ?? undefined, + components: this.components ?? [], + poll: this.poll ?? undefined, + content: this.content ?? "", + pinned: this.pinned, + thread: this.thread ? this.thread.toJSON() : this.thread, + referenced_message: this.referenced_message && !shallow ? this.referenced_message.toJSON(true) : undefined, + }; + } + + toPartialMessage(): PartialMessage { + return { + id: this.id, + // lobby_id: this.lobby_id, + channel_id: this.channel_id!, + type: this.type, + content: this.content!, + author: { ...this.author!, avatar: this.author?.avatar ?? null }, + flags: this.flags, + application_id: this.application_id, + //channel: this.channel, // TODO: ephemeral DM channels + // recipient_id: this.recipient_id, // TODO: ephemeral DM channels + }; + } + + toSnapshot(): MessageSnapshot { + return { + message: { + attachments: this.attachments?.map((x) => x.toJSON()), + components: this.components, + content: this.content!, + edited_timestamp: this.edited_timestamp, + embeds: this.embeds, + flags: this.flags, + mention_roles: this.mention_roles?.map((x) => x.id), + mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this + timestamp: this.timestamp, + type: this.type, + }, + }; + } + + withSignedAttachments(data: NewUrlUserSignatureData) { + function signMedia(media: UnfurledMediaItem) { + Object.assign(media, Attachment.prototype.signUrls.call(media, data)); + } + return { + ...this, + attachments: this.attachments?.map((attachment: Attachment) => Attachment.prototype.signUrls.call(attachment, data)), + components: this.components + ? this.components.map((comp) => { + comp = structuredClone(comp); + if (comp.type === MessageComponentType.Section) { + const accessory = comp.accessory; + if (accessory.type === MessageComponentType.Thumbnail) { + signMedia(accessory.media); + } + } else if (comp.type === MessageComponentType.MediaGallery) { + comp.items.forEach(({ media }) => signMedia(media)); + } else if (comp.type === MessageComponentType.File) { + signMedia(comp.file); + } else if (comp.type === MessageComponentType.Container) { + for (const elm of comp.components) { + switch (elm.type) { + case MessageComponentType.Separator: + case MessageComponentType.TextDisplay: + case MessageComponentType.ActionRow: + break; + case MessageComponentType.Section: { + const accessory = elm.accessory; + if (accessory.type === MessageComponentType.Thumbnail) { + signMedia(accessory.media); + } + break; + } + case MessageComponentType.MediaGallery: + elm.items.forEach(({ media }) => signMedia(media)); + break; + case MessageComponentType.File: { + signMedia(elm.file); + break; + } + + default: + elm satisfies never; + } + } + } + return comp; + }) + : this.components, + }; + } + + static async createWithDefaults(opts: Partial): Promise { + const message = Message.create(); + + if (!opts.author) { + if (!opts.author_id) throw new Error("Either author or author_id must be provided to create a Message"); + opts.author = await User.findOneOrFail({ where: { id: opts.author_id! } }); + } + + if (!opts.channel) { + if (!opts.channel_id) throw new Error("Either channel or channel_id must be provided to create a Message"); + opts.channel = await Channel.findOneOrFail({ where: { id: opts.channel_id! } }); + opts.guild_id ??= opts.channel.guild_id; + } + + if (!opts.member_id) opts.member_id = message.author_id; + if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.member_id! } }); + + if (!opts.guild) { + if (opts.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.guild_id! } }); + else if (opts.channel?.guild?.id) opts.guild = opts.channel.guild; + else if (opts.channel?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.channel.guild_id! } }); + else if (opts.member?.guild?.id) opts.guild = opts.member.guild; + else if (opts.member?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.member.guild_id! } }); + else throw new Error("Either guild, guild_id, channel.guild, channel.guild_id, member.guild or member.guild_id must be provided to create a Message"); + } + + // try 2 now that we have a guild + if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.author!.id, guild_id: opts.guild!.id } }); + + // set reply type if a message if referenced + if (opts.message_reference && !opts.type) message.type = MessageType.REPLY; + + // backpropagate ids + opts.channel_id = opts.channel.id; + opts.guild_id = opts.guild.id; + opts.author_id = opts.author.id; + opts.member_id = opts.member.id; + opts.webhook_id = opts.webhook?.id; + opts.application_id = opts.application?.id; + + delete opts.member; + + Object.assign(message, { + tts: false, + embeds: [], + reactions: [], + flags: 0, + type: 0, + timestamp: new Date(), + ...opts, + }); + return message; + } + static addDefault(options: FindOneOptions) { + if (options.where) { + const arr = options.where instanceof Array ? options.where : [options.where]; + for (const thing of arr) { + if (!("flags" in thing)) { + thing.flags = Not(Raw((alias) => `${alias} & ${MessageFlags.FLAGS.EPHEMERAL} = ${MessageFlags.FLAGS.EPHEMERAL}`)); + } + } + } + } +} + +//@ts-expect-error It works but TS types hate it +Message.findOneOrFail = function (this: Message, options: FindOneOptions): Promise { + Message.addDefault(options as FindOneOptions); + //@ts-expect-error how to use generics on call, who knows! + return BaseEntity.findOneOrFail.call(Message, options); +}; +//@ts-expect-error It works but TS types hate it +Message.findOne = function (this: Message, options: FindOneOptions): Promise { + Message.addDefault(options as FindOneOptions); + //@ts-expect-error how to use generics on call, who knows! + return BaseEntity.findOne.call(Message, options); +}; +//@ts-expect-error It works but TS types hate it +Message.find = function (this: Message, options: FindOneOptions): Promise { + Message.addDefault(options as FindOneOptions); + //@ts-expect-error how to use generics on call, who knows! + return BaseEntity.find.call(Message, options); +}; diff --git a/src/database/entities/Migration.ts b/src/database/entities/Migration.ts new file mode 100644 index 00000000..b4a5ff78 --- /dev/null +++ b/src/database/entities/Migration.ts @@ -0,0 +1,33 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, PrimaryGeneratedColumn, BaseEntity } from "typeorm"; + +@Entity({ + name: "migrations", +}) +export class Migration extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: "bigint" }) + timestamp: number; + + @Column() + name: string; +} diff --git a/src/database/entities/Note.ts b/src/database/entities/Note.ts new file mode 100644 index 00000000..e9f173ff --- /dev/null +++ b/src/database/entities/Note.ts @@ -0,0 +1,38 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; + +@Entity({ + name: "notes", +}) +@Unique(["owner", "target"]) +export class Note extends BaseClass { + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User, { onDelete: "CASCADE" }) + owner: User; + + @JoinColumn({ name: "target_id" }) + @ManyToOne(() => User, { onDelete: "CASCADE" }) + target: User; + + @Column() + content: string; +} diff --git a/src/database/entities/RateLimit.ts b/src/database/entities/RateLimit.ts new file mode 100644 index 00000000..ab85285c --- /dev/null +++ b/src/database/entities/RateLimit.ts @@ -0,0 +1,37 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "rate_limits", +}) +export class RateLimit extends BaseClass { + @Column() // no relation as it also + executor_id: string; + + @Column() + hits: number; + + @Column() + blocked: boolean; + + @Column() + expires_at: Date; +} diff --git a/src/database/entities/ReadState.ts b/src/database/entities/ReadState.ts new file mode 100644 index 00000000..4d15c6cc --- /dev/null +++ b/src/database/entities/ReadState.ts @@ -0,0 +1,93 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { User } from "./User"; +import { ReadStateFlags, ReadStateType } from "@spacebar/schemas"; + +// for read receipts +// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable +// public read receipt ≥ notification cursor ≥ private fully read marker + +@Entity({ + name: "read_states", +}) +@Index(["channel_id", "user_id"], { unique: true }) +export class ReadState extends BaseClass { + @Column() + @RelationId((read_state: ReadState) => read_state.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column() + @RelationId((read_state: ReadState) => read_state.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ nullable: true }) + last_message_id?: string; + + @Column({ nullable: true }) + last_acked_id?: string; + + @Column({ nullable: true }) + notifications_cursor: string; + + @Column({ default: 0 }) + mention_count: number; + + @Column({ default: 0 }) + badge_count: number; + + @Column({ nullable: true }) + last_pin_timestamp?: Date; + + @Column({ default: ReadStateType.CHANNEL }) + read_state_type: ReadStateType; + + @Column({ default: 0 }) + flags: ReadStateFlags; + + // toJSON() { + // const res = { ...this } as Partial; + // if (this.read_state_type === ReadStateType.CHANNEL) { + // delete res.badge_count; + // delete res.last_acked_id; + // } else { + // delete res.mention_count; // mutually exclusive with badge_count + // delete res.last_message_id; // mutually exclusive with last_acked_id + // // these only apply to channels: + // delete res.last_pin_timestamp; + // delete res.flags; + // // delete res.last_viewed; // TODO + // } + // return res; + // } +} diff --git a/src/database/entities/Recipient.ts b/src/database/entities/Recipient.ts new file mode 100644 index 00000000..2dcff244 --- /dev/null +++ b/src/database/entities/Recipient.ts @@ -0,0 +1,50 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "recipients", +}) +export class Recipient extends BaseClass { + @Column() + @RelationId((recipient: Recipient) => recipient.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => require("./Channel").Channel, { + onDelete: "CASCADE", + }) + channel: import("./Channel").Channel; + + @Column() + @RelationId((recipient: Recipient) => recipient.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => require("./User").User, { + onDelete: "CASCADE", + }) + user: import("./User").User; + + @Column({ default: false }) + closed: boolean; + + // TODO: settings/mute/nick/added at/encryption keys/read_state +} diff --git a/src/database/entities/Relationship.ts b/src/database/entities/Relationship.ts new file mode 100644 index 00000000..c2256ed3 --- /dev/null +++ b/src/database/entities/Relationship.ts @@ -0,0 +1,63 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { RelationshipType } from "@spacebar/schemas"; + +@Entity({ + name: "relationships", +}) +@Index(["from_id", "to_id"], { unique: true }) +export class Relationship extends BaseClass { + @Column({}) + @RelationId((relationship: Relationship) => relationship.from) + from_id: string; + + @JoinColumn({ name: "from_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + from: User; + + @Column({}) + @RelationId((relationship: Relationship) => relationship.to) + to_id: string; + + @JoinColumn({ name: "to_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + to: User; + + @Column({ nullable: true }) + nickname?: string; + + @Column({ type: "int" }) + type: RelationshipType; + + toPublicRelationship() { + return { + id: this.to?.id || this.to_id, + type: this.type, + nickname: this.nickname, + user: this.to?.toPublicUser(), + }; + } +} diff --git a/src/database/entities/ReportMenu.ts b/src/database/entities/ReportMenu.ts new file mode 100644 index 00000000..a37ae159 --- /dev/null +++ b/src/database/entities/ReportMenu.ts @@ -0,0 +1,41 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2024 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 . +*/ + +import { BaseClass } from "./BaseClass"; +import { Entity, Column } from "typeorm"; +import { ReportMenuType } from "../../schemas/api/reports/ReportMenu"; + +@Entity({ + name: "report_menus", +}) +export class ReportMenu extends BaseClass { + @Column() + type: ReportMenuType; + + @Column() + variant: string; + + @Column() + isCurrent: boolean; + + @Column({ nullable: true }) + inherits?: string; + + @Column({ type: "jsonb" }) + content: unknown; +} diff --git a/src/database/entities/Role.ts b/src/database/entities/Role.ts new file mode 100644 index 00000000..e6b2e459 --- /dev/null +++ b/src/database/entities/Role.ts @@ -0,0 +1,85 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; + +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { RoleColors } from "@spacebar/schemas"; + +@Entity({ + name: "roles", +}) +export class Role extends BaseClass { + @Column() + @RelationId((role: Role) => role.guild) + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.roles, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column() + color: number; + + @Column() + hoist: boolean; + + @Column({ default: false }) + managed: boolean; + + @Column() + mentionable: boolean; + + @Column() + name: string; + + @Column() + permissions: string; + + @Column() + position: number; + + @Column({ nullable: true }) + icon?: string; + + @Column({ nullable: true }) + unicode_emoji?: string; + + @Column({ type: "jsonb", nullable: true }) + tags?: { + bot_id?: string; + integration_id?: string; + premium_subscriber?: boolean; + }; + + @Column({ default: 0 }) + flags: number; + + @Column({ nullable: false, type: "jsonb" }) + colors: RoleColors; + + toJSON(): Role { + return { + ...this, + tags: this.tags ?? undefined, + }; + } +} diff --git a/src/database/entities/SecurityKey.ts b/src/database/entities/SecurityKey.ts new file mode 100644 index 00000000..cbf80d3d --- /dev/null +++ b/src/database/entities/SecurityKey.ts @@ -0,0 +1,48 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; + +@Entity({ + name: "security_keys", +}) +export class SecurityKey extends BaseClass { + @Column({ nullable: true }) + @RelationId((key: SecurityKey) => key.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column() + key_id: string; + + @Column() + public_key: string; + + @Column() + counter: number; + + @Column() + name: string; +} diff --git a/src/database/entities/Session.ts b/src/database/entities/Session.ts new file mode 100644 index 00000000..e0e622db --- /dev/null +++ b/src/database/entities/Session.ts @@ -0,0 +1,181 @@ +/* + 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 . +*/ + +import crypto from "node:crypto"; +import { User } from "./User"; +import { BaseClassWithoutId } from "./BaseClass"; +import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; +import { Activity, ClientStatus, GatewaySession, GatewaySessionClientInfo, PrivateStatus } from "../../util/interfaces"; +import { randomUpperString } from "@spacebar/api"; +import { DateBuilder, IpDataClient, TimeSpan } from "../../util/util"; + +@Entity({ + name: "sessions", +}) +export class Session extends BaseClassWithoutId { + @PrimaryColumn({ nullable: false }) + session_id: string = randomUpperString(); + + @Column() + @RelationId((session: Session) => session.user) + @Index({}) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ type: "jsonb", default: "[]" }) + activities: Activity[]; + + @Column({ type: "jsonb" }) + client_info: { + platform?: string; + os?: string; + version?: number; + location?: string; + }; + + @Column({ type: "jsonb" }) + client_status: ClientStatus; + + @Column({ nullable: false, type: String }) + status: PrivateStatus; + + @Column({ default: false }) + is_admin_session: boolean; + + @CreateDateColumn({ type: Date }) + created_at: Date; + + @Column({ nullable: true, type: Date }) + last_seen?: Date; + + @Column({ nullable: true, type: String }) + last_seen_ip?: string; + + @Column({ nullable: true, type: String }) + last_seen_location?: string; + + @Column({ nullable: true, type: "jsonb" }) + last_seen_location_info?: ExtendedLocationInfo; + + @Column({ nullable: true, type: String }) + session_nickname?: string; + + getPublicStatus() { + return this.status === "invisible" ? "offline" : this.status; + } + + getDiscordDeviceInfo() { + return { + id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), + approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(), + client_info: { + os: this.client_info?.os, + platform: + this.client_info?.platform + (this.client_info?.version ? ` ${this.client_info?.version}` : "") + (this.session_nickname ? ` (${this.session_nickname})` : ""), + location: this.last_seen_location, + }, + }; + } + + getExtendedDeviceInfo() { + return { + id: this.session_id, + id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), + status: this.status, + activities: this.activities, + client_status: this.client_status, + approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(), + client_info: { + ...(this.client_info ?? {}), + location: this.last_seen_location, + }, + last_seen: this.last_seen, + last_seen_ip: this.last_seen_ip, + last_seen_location: this.last_seen_location, + last_seen_location_info: this.last_seen_location_info, + }; + } + + toPrivateGatewayDeviceInfo(): GatewaySession { + // TODO: ... or has `show_current_game` privacy setting enabled - except spotify (always visible) + const hasPrivateActivities = this.status == "offline" || this.status == "invisible"; + const inactiveTreshold = new DateBuilder(new Date(0)).addMinutes(5).buildTimestamp(); + + return { + session_id: this.session_id, + client_info: { + client: this.client_info?.platform ?? "", + os: this.client_info?.os ?? "", + version: this.client_info?.version ?? 0, + } as GatewaySessionClientInfo, + status: this.status, + activities: hasPrivateActivities ? [] : this.activities, + hidden_activities: hasPrivateActivities ? this.activities : [], + active: TimeSpan.fromDates(this.last_seen?.getTime() ?? 0, new Date().getTime()).totalMillis < inactiveTreshold, + }; + } + + async updateIpInfo() { + const ipInfo = await IpDataClient.getIpInfo(this.last_seen_ip!); + if (ipInfo?.ip) { + this.last_seen_location = `${ipInfo.emoji_flag} ${ipInfo.postal} ${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country_name}`; + this.last_seen_location_info = { + is_eu: ipInfo.is_eu, + city: ipInfo.city, + region: ipInfo.region, + region_code: ipInfo.region_code, + country_name: ipInfo.country_name, + country_code: ipInfo.country_code, + continent_name: ipInfo.continent_name, + continent_code: ipInfo.continent_code, + latitude: ipInfo.latitude, + longitude: ipInfo.longitude, + postal: ipInfo.postal, + calling_code: ipInfo.calling_code, + flag: ipInfo.flag, + emoji_flag: ipInfo.emoji_flag, + emoji_unicode: ipInfo.emoji_unicode, + }; + } + } +} + +export interface ExtendedLocationInfo { + is_eu: boolean; + city: string; + region: string; + region_code: string; + country_name: string; + country_code: string; + continent_name: string; + continent_code: string; + latitude: number; + longitude: number; + postal: string; + calling_code: string; + flag: string; + emoji_flag: string; + emoji_unicode: string; +} + +export const PrivateSessionProjection: (keyof Session)[] = ["user_id", "session_id", "activities", "client_info", "status"]; diff --git a/src/database/entities/Sticker.ts b/src/database/entities/Sticker.ts new file mode 100644 index 00000000..c88e0c29 --- /dev/null +++ b/src/database/entities/Sticker.ts @@ -0,0 +1,75 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { User } from "./User"; +import { StickerFormatType, StickerType } from "@spacebar/schemas"; + +@Entity({ + name: "stickers", +}) +export class Sticker extends BaseClass { + @Column() + name: string; + + @Column({ nullable: true }) + description?: string; + + @Column({ nullable: true }) + available?: boolean; + + @Column({ nullable: true }) + tags?: string; + + @Column({ nullable: true }) + @RelationId((sticker: Sticker) => sticker.pack) + pack_id?: string; + + @JoinColumn({ name: "pack_id" }) + @ManyToOne(() => require("./StickerPack").StickerPack, { + onDelete: "CASCADE", + nullable: true, + }) + pack: import("./StickerPack").StickerPack; + + @Column({ nullable: true }) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.stickers, { + onDelete: "CASCADE", + }) + guild?: Guild; + + @Column({ nullable: true }) + user_id?: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user?: User; + + @Column({ type: "int" }) + type: StickerType; + + @Column({ type: "int" }) + format_type: StickerFormatType; +} diff --git a/src/database/entities/StickerPack.ts b/src/database/entities/StickerPack.ts new file mode 100644 index 00000000..87112441 --- /dev/null +++ b/src/database/entities/StickerPack.ts @@ -0,0 +1,51 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { Sticker } from "./index"; +import { BaseClass } from "./BaseClass"; + +@Entity({ + name: "sticker_packs", +}) +export class StickerPack extends BaseClass { + @Column() + name: string; + + @Column({ nullable: true }) + description?: string; + + @Column({ nullable: true }) + banner_asset_id?: string; + + @OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, { + cascade: true, + orphanedRowAction: "delete", + }) + stickers: Sticker[]; + + // sku_id: string + + @Column({ nullable: true }) + @RelationId((pack: StickerPack) => pack.cover_sticker) + cover_sticker_id?: string; + + @ManyToOne(() => Sticker, { nullable: true }) + @JoinColumn() + cover_sticker?: Sticker; +} diff --git a/src/database/entities/Stream.ts b/src/database/entities/Stream.ts new file mode 100644 index 00000000..bf2f3a62 --- /dev/null +++ b/src/database/entities/Stream.ts @@ -0,0 +1,32 @@ +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { Channel } from "./Channel"; + +@Entity({ + name: "streams", +}) +export class Stream extends BaseClass { + @Column() + @RelationId((stream: Stream) => stream.owner) + owner_id: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + owner: User; + + @Column() + @RelationId((stream: Stream) => stream.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column() + endpoint: string; +} diff --git a/src/database/entities/StreamSession.ts b/src/database/entities/StreamSession.ts new file mode 100644 index 00000000..1dca2424 --- /dev/null +++ b/src/database/entities/StreamSession.ts @@ -0,0 +1,39 @@ +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { Stream } from "./Stream"; + +@Entity({ + name: "stream_sessions", +}) +export class StreamSession extends BaseClass { + @Column() + @RelationId((session: StreamSession) => session.stream) + stream_id: string; + + @JoinColumn({ name: "stream_id" }) + @ManyToOne(() => Stream, { + onDelete: "CASCADE", + }) + stream: Stream; + + @Column() + @RelationId((session: StreamSession) => session.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ nullable: true }) + token: string; + + // this is for gateway session + @Column() + session_id: string; + + @Column({ default: false }) + used: boolean; +} diff --git a/src/database/entities/Tag.ts b/src/database/entities/Tag.ts new file mode 100644 index 00000000..63ad4b86 --- /dev/null +++ b/src/database/entities/Tag.ts @@ -0,0 +1,57 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; + +@Entity({ + name: "tags", +}) +export class Tag extends BaseClass { + @Column() + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, (channel) => channel.available_tags, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column() + name: string; + + @Column() + moderated: boolean = false; + + @Column({ nullable: true }) + emoji_id?: string; + + @Column({ nullable: true }) + emoji_name?: string; + + toJSON() { + return { + name: this.name, + id: this.id, + moderated: this.moderated, + emoji_id: this.emoji_id, + emoji_name: this.emoji_name, + }; + } +} diff --git a/src/database/entities/Team.ts b/src/database/entities/Team.ts new file mode 100644 index 00000000..e440e965 --- /dev/null +++ b/src/database/entities/Team.ts @@ -0,0 +1,47 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { TeamMember } from "./TeamMember"; +import { User } from "./User"; + +@Entity({ + name: "teams", +}) +export class Team extends BaseClass { + @Column({ nullable: true }) + icon?: string; + + @JoinColumn({ name: "member_ids" }) + @OneToMany(() => TeamMember, (member: TeamMember) => member.team, { + orphanedRowAction: "delete", + }) + members: TeamMember[]; + + @Column() + name: string; + + @Column({ nullable: true }) + @RelationId((team: Team) => team.owner_user) + owner_user_id: string; + + @JoinColumn({ name: "owner_user_id" }) + @ManyToOne(() => User) + owner_user: User; +} diff --git a/src/database/entities/TeamMember.ts b/src/database/entities/TeamMember.ts new file mode 100644 index 00000000..72e23b76 --- /dev/null +++ b/src/database/entities/TeamMember.ts @@ -0,0 +1,56 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { User } from "./User"; +import { TeamMemberRole, TeamMemberState } from "@spacebar/schemas"; + +@Entity({ + name: "team_members", +}) +export class TeamMember extends BaseClass { + @Column({ type: "int" }) + membership_state: TeamMemberState; + + @Column({ type: "varchar", array: true }) + permissions: string[]; + + @Column() + role: TeamMemberRole; + + @Column({ nullable: true }) + @RelationId((member: TeamMember) => member.team) + team_id: string; + + @JoinColumn({ name: "team_id" }) + @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, { + onDelete: "CASCADE", + }) + team: import("./Team").Team; + + @Column({ nullable: true }) + @RelationId((member: TeamMember) => member.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; +} diff --git a/src/database/entities/Template.ts b/src/database/entities/Template.ts new file mode 100644 index 00000000..79804eed --- /dev/null +++ b/src/database/entities/Template.ts @@ -0,0 +1,64 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { User } from "./User"; + +@Entity({ + name: "templates", +}) +export class Template extends BaseClass { + @Column({ unique: true }) + code: string; + + @Column() + name: string; + + @Column({ nullable: true }) + description?: string; + + @Column({ nullable: true }) + usage_count?: number; + + @Column({ nullable: true }) + @RelationId((template: Template) => template.creator) + creator_id: string; + + @JoinColumn({ name: "creator_id" }) + @ManyToOne(() => User) + creator: User; + + @Column() + created_at: Date; + + @Column() + updated_at: Date; + + @Column({ nullable: true }) + @RelationId((template: Template) => template.source_guild) + source_guild_id: string; + + @JoinColumn({ name: "source_guild_id" }) + @ManyToOne(() => Guild, { onDelete: "CASCADE" }) + source_guild: Guild; + + @Column({ type: "jsonb" }) + serialized_source_guild: Guild; +} diff --git a/src/database/entities/ThreadMember.ts b/src/database/entities/ThreadMember.ts new file mode 100644 index 00000000..fdda4eea --- /dev/null +++ b/src/database/entities/ThreadMember.ts @@ -0,0 +1,195 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm"; +import { ThreadMembersUpdateEvent } from "../../util/interfaces"; +import { emitEvent } from "../../util/util"; +import { BaseClassWithoutId } from "./BaseClass"; +import { Channel } from "./Channel"; +import { HTTPError } from "lambert-server/HTTPError"; +import { Member } from "./Member"; + +// TODO: move +interface ThreadMemberMuteConfig { + end_time?: Date; + selected_time_window?: number; +} + +// TODO: move +export enum ThreadMemberFlags { + NONE = 0, + HAS_INTERACTED = 1 << 0, + ALL_MESSAGES = 1 << 1, + ONLY_MENTIONS = 1 << 2, + NO_MESSAGES = 1 << 3, +} + +@Entity("thread_members") +@Index(["id", "member_idx"], { unique: true }) +export class ThreadMember extends BaseClassWithoutId { + @PrimaryGeneratedColumn() + index: string; + + @Column() + @RelationId((member: ThreadMember) => member.channel) + id: string; + + @JoinColumn({ name: "id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column() + @RelationId((member: ThreadMember) => member.member) + member_idx: string; + + @JoinColumn({ name: "member_idx" }) + @ManyToOne(() => Member, { + onDelete: "CASCADE", + }) + member: Member; + + @Column() + join_timestamp: Date; + + @Column() + muted: boolean; + + @Column({ nullable: true, type: "jsonb" }) + mute_config?: ThreadMemberMuteConfig; + + @Column() + flags: ThreadMemberFlags; + + static async IsInThreadOrFail(member_id: string, thread_id: string) { + if (await ThreadMember.count({ where: { id: thread_id, member_idx: member_id } })) return true; + throw new HTTPError("You are not member of this thread", 403); + } + + static async removeFromThread(member_id: string, thread_id: string) { + const channel = await Channel.findOneOrFail({ where: { id: thread_id } }); + if ( + !(await ThreadMember.count({ + where: { + id: thread_id, + member_idx: member_id, + }, + })) + ) + throw new HTTPError("You are not member of this thread", 403); + // // use promise all to execute all promises at the same time -> save time + // TODO: check for bugs + if (channel.member_count) channel.member_count--; + return Promise.all([ + ThreadMember.delete({ + id: thread_id, + member_idx: member_id, + }), + // //Guild.decrement({ id: guild_id }, "member_count", -1), + + emitEvent({ + event: "THREAD_MEMBERS_UPDATE", + data: { + guild_id: channel.guild_id!, // TODO: is this the right fix? + id: channel.id, + member_count: channel.member_count ?? 0, + removed_member_ids: [member_id], + }, + channel_id: thread_id, + } satisfies ThreadMembersUpdateEvent), + ]); + } + + // static async addRole(user_id: string, guild_id: string, role_id: string) { + // const [member, role] = await Promise.all([ + // // @ts-ignore + // Member.findOneOrFail({ + // where: { id: user_id, guild_id }, + // relations: ["user", "roles"], // we don't want to load the role objects just the ids + // select: ["index"] + // }), + // Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }) + // ]); + // member.roles.push(OrmUtils.mergeDeep(new Role(), { id: role_id })); + + // await Promise.all([ + // member.save(), + // emitEvent({ + // event: "GUILD_MEMBER_UPDATE", + // data: { + // guild_id, + // user: member.user, + // roles: member.roles.map((x) => x.id) + // }, + // guild_id + // } satisfies GuildMemberUpdateEvent) + // ]); + // } + + // static async removeRole(user_id: string, guild_id: string, role_id: string) { + // const [member] = await Promise.all([ + // // @ts-ignore + // Member.findOneOrFail({ + // where: { id: user_id, guild_id }, + // relations: ["user", "roles"], // we don't want to load the role objects just the ids + // select: ["index"] + // }), + // await Role.findOneOrFail({ where: { id: role_id, guild_id } }) + // ]); + // member.roles = member.roles.filter((x) => x.id == role_id); + + // await Promise.all([ + // member.save(), + // emitEvent({ + // event: "GUILD_MEMBER_UPDATE", + // data: { + // guild_id, + // user: member.user, + // roles: member.roles.map((x) => x.id) + // }, + // guild_id + // } satisfies GuildMemberUpdateEvent) + // ]); + // } + + // static async changeNickname(user_id: string, guild_id: string, nickname: string) { + // const member = await Member.findOneOrFail({ + // where: { + // id: user_id, + // guild_id + // }, + // relations: ["user"] + // }); + // member.nick = nickname; + + // await Promise.all([ + // member.save(), + + // emitEvent({ + // event: "GUILD_MEMBER_UPDATE", + // data: { + // guild_id, + // user: member.user, + // nick: nickname + // }, + // guild_id + // } satisfies GuildMemberUpdateEvent) + // ]); + // } +} diff --git a/src/database/entities/User.ts b/src/database/entities/User.ts new file mode 100644 index 00000000..5f40adb3 --- /dev/null +++ b/src/database/entities/User.ts @@ -0,0 +1,409 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Request } from "express"; +import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm"; +import { Config, Email, FieldErrors, Snowflake, Stopwatch, trimSpecial } from "@spacebar/util"; +import { Random } from "@spacebar/util/util/Random"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { ConnectedAccount } from "./ConnectedAccount"; +import { Member } from "./Member"; +import { Relationship } from "./Relationship"; +import { SecurityKey } from "./SecurityKey"; +import { Session } from "./Session"; +import { UserSettings } from "./UserSettings"; +import { + AvatarDecorationData, + ChannelType, + Collectibles, + DisplayNameStyle, + PrimaryGuild, + PrivateUserProjection, + PublicUser, + PublicUserProjection, + UserPrivate, +} from "@spacebar/schemas"; +import { JsonNumber } from "../../util/util/Decorators"; + +@Entity({ + name: "users", +}) +export class User extends BaseClass { + @Column() + username: string; // username max length 32, min 2 (should be configurable) + + @Column() + discriminator: string; // opaque string: 4 digits on discord.com + + @Column({ nullable: true }) + avatar?: string; // hash of the user avatar + + @Column({ nullable: true }) + accent_color?: number; // banner color of user + + @Column({ nullable: true }) + banner?: string; // hash of the user banner + + // TODO: Separate `User` and `UserProfile` models + // puyo: changed from [number, number] because it breaks openapi + @Column({ nullable: true, type: "int4", array: true }) + theme_colors?: number[]; + + @Column({ nullable: true }) + pronouns?: string; + + @Column({ nullable: true, select: false }) + phone?: string; // phone number of the user + + @Column({ select: false }) + desktop: boolean = false; // if the user has desktop app installed + + @Column({ select: false }) + mobile: boolean = false; // if the user has mobile app installed + + @Column() + premium: boolean; // if user bought individual premium + + @Column() + premium_type: number; // individual premium level + + @Column() + bot: boolean = false; // if user is bot + + @Column() + bio: string = ""; // short description of the user + + @Column() + system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author + + @Column({ select: false }) + nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age + + @Column({ select: false }) + mfa_enabled: boolean = false; // if multi factor authentication is enabled + + @Column({ select: false, default: false }) + webauthn_enabled: boolean = false; // if webauthn multi factor authentication is enabled + + @Column({ select: false, nullable: true }) + totp_secret?: string = ""; + + @Column({ nullable: true, select: false }) + totp_last_ticket?: string = ""; + + @Column() + created_at: Date; // registration date + + @Column({ nullable: true }) + premium_since: Date; // premium date + + @Column({ select: false }) + verified: boolean; // email is verified + + @Column() + disabled: boolean = false; // if the account is disabled + + @Column() + deleted: boolean = false; // if the user was deleted + + @Column({ nullable: true, select: false }) + email?: string; // email of the user + + @Column({ type: "bigint" }) + @JsonNumber + flags: number = 0; // UserFlags // TODO: generate + + @Column({ type: "bigint" }) + @JsonNumber + public_flags: number = 0; + + @Column({ type: "bigint" }) + @JsonNumber + purchased_flags: number = 0; + + @Column() + premium_usage_flags: number = 0; + + @Column({ type: "bigint" }) + @JsonNumber + rights: string; + + @OneToMany(() => Session, (session: Session) => session.user) + sessions: Session[]; + + @JoinColumn({ name: "relationship_ids" }) + @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { + cascade: true, + orphanedRowAction: "delete", + }) + relationships: Relationship[]; + + @JoinColumn({ name: "connected_account_ids" }) + @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { + cascade: true, + orphanedRowAction: "delete", + }) + connected_accounts: ConnectedAccount[]; + + @Column({ type: "jsonb", select: false }) + data: { + valid_tokens_since: Date; // all tokens with a previous issue date are invalid + hash?: string; // hash of the password, salt is saved in password (bcrypt) + }; + + @Column({ type: "varchar", array: true, select: false }) + fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts + + @OneToOne(() => UserSettings, { + cascade: true, + orphanedRowAction: "delete", + nullable: true, + }) + @JoinColumn() + settings?: UserSettings; + + @OneToMany(() => SecurityKey, (key: SecurityKey) => key.user) + security_keys: SecurityKey[]; + + @Column({ type: "int8", array: true, nullable: true }) + badge_ids?: string[]; + + @Column({ type: "jsonb", nullable: true }) + avatar_decoration_data?: AvatarDecorationData; + + @Column({ type: "jsonb", nullable: true }) + display_name_styles?: DisplayNameStyle; + + @Column({ type: "jsonb", nullable: true }) + collectibles?: Collectibles; + + @Column({ type: "jsonb", nullable: true }) + primary_guild?: PrimaryGuild; + + // TODO: I don't like this method? + validate() { + if (this.discriminator) { + const discrim = Number(this.discriminator); + if (isNaN(discrim) || !Number.isInteger(discrim) || discrim <= 0 || discrim >= 10000) + throw FieldErrors({ + discriminator: { + message: "Discriminator must be a number.", + code: "DISCRIMINATOR_INVALID", + }, + }); + + this.discriminator = discrim.toString().padStart(4, "0"); + } + } + + toPublicUser() { + this.clean_data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user: any = {}; + PublicUserProjection.forEach((x) => { + user[x] = this[x]; + }); + return user as PublicUser; + } + + toPrivateUser(extraFields: (keyof User)[] = []) { + this.clean_data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user: any = {}; + [...PrivateUserProjection, ...extraFields].forEach((x) => { + user[x] = this[x]; + }); + return user as UserPrivate; + } + + static async getPublicUser(user_id: string): Promise { + const user = await User.findOneOrFail({ + where: { id: user_id }, + select: PublicUserProjection, + }); + return user.toPublicUser(); + } + + public static async generateDiscriminator(username: string): Promise { + if (Config.get().register.incrementingDiscriminators) { + // discriminator will be incrementally generated + + // First we need to figure out the currently highest discrimnator for the given username and then increment it + const users = await User.find({ + where: { username }, + select: { discriminator: true }, + }); + const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator))); + + const discriminator = highestDiscriminator + 1; + if (discriminator >= 10000) { + return undefined; + } + + return discriminator.toString().padStart(4, "0"); + } else { + // discriminator will be randomly generated + + // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists + // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? + const takenDiscriminators = (await User.find({ where: { username }, select: { discriminator: true } })).map((x) => x.discriminator); + if (takenDiscriminators.length >= 9999) return undefined; + + for (let tries = 0; tries < 15; tries++) { + const discriminator = Random.nextInt(1, 9999).toString().padStart(4, "0"); + if (!takenDiscriminators.includes(discriminator)) return discriminator; + } + + return undefined; + } + } + + public get tag(): string { + //const { uniqueUsernames } = Config.get().general; + const uniqueUsernames = false; + + return uniqueUsernames ? this.username : `${this.username}#${this.discriminator}`; + } + + static async register({ + email, + username, + password, + id, + req, + bot, + }: { + username: string; + password?: string; + email?: string; + date_of_birth?: Date; // "2000-04-03" + id?: string; + req?: Request; + bot?: boolean; + }) { + const totalSw = Stopwatch.startNew(); + const incSw = Stopwatch.startNew(); + const logTrace = (...data: unknown[]) => { + if (process.env.LOG_VERBOSE_TRACES !== "true") return; + console.log("[User.register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); + }; + + // trim special utf8 control characters -> Backspace, Newline, ... + username = trimSpecial(username); + + const discriminator = await User.generateDiscriminator(username); + if (!discriminator) { + // We've failed to generate a valid and unused discriminator + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req?.t("auth:register.USERNAME_TOO_MANY_USERS") || "", + }, + }); + } + logTrace("Generate discriminator"); + + // TODO: save date_of_birth + // apparently discord doesn't save the date of birth and just calculate if nsfw is allowed + // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false + const language = req?.language === "en" ? "en-US" : req?.language || "en-US"; + + const settings = UserSettings.create({ + locale: language, + }); + + const user = User.create({ + username: username, + discriminator, + id: id || Snowflake.generate(), + email: email, + data: { + hash: password, + valid_tokens_since: new Date(), + }, + settings: settings, + + premium_since: Config.get().defaults.user.premium ? new Date() : undefined, + rights: Config.get().register.defaultRights, + premium: Config.get().defaults.user.premium ?? false, + premium_type: Config.get().defaults.user.premiumType ?? 0, + verified: Config.get().defaults.user.verified ?? true, + created_at: new Date(), + bot: !!bot, + }); + + user.validate(); + logTrace("Generate/validate user"); + + await Promise.all([user.save(), settings.save()]); + logTrace("Save user"); + + // send verification email if users aren't verified by default and we have an email + if (!Config.get().defaults.user.verified && email) { + await Email.sendVerifyEmail(user, email).catch((e) => { + console.error(`Failed to send verification email to ${user.tag}: ${e}`); + }); + logTrace("Send verify email"); + } + + const { autoJoin } = Config.get().guild; + if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) { + await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild, true).catch((e) => console.error("[Autojoin]", e)))); + logTrace("Autojoin", autoJoin.guilds.length, "guilds"); + } + + return user; + } + + async getDmChannelWith(user_id: string) { + const qry = await Channel.getRepository() + .createQueryBuilder() + .leftJoinAndSelect("Channel.recipients", "rcp") + .where("Channel.type = :type", { type: ChannelType.DM }) + .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] }) + .groupBy("Channel.id") + .having("COUNT(rcp.user_id) = 2") + .getMany(); + + // Emma [it/its]@Rory&: is this technically a bug, or am I being too over-cautious? + if (qry.length > 1) { + console.warn(`[WARN] User(${this.id})#getDmChannel(${user_id}) returned multiple channels:`); + for (const channel of qry) { + console.warn(JSON.stringify(channel)); + } + throw new Error("Array contains more than one matching element"); + } + + return qry[0]; + } + + async getDmChannels() { + const qry = await Channel.getRepository() + .createQueryBuilder("channel") + .leftJoinAndSelect("channel.recipients", "rcp") + .where("channel.type = :type", { type: ChannelType.DM }) + .andWhere("rcp.user_id = :user_id", { user_id: this.id }) + .groupBy("channel.id") + .addGroupBy("rcp.id") + .having("COUNT(rcp.id) = 2") + .getMany(); + + return qry; + } +} diff --git a/src/database/entities/UserSettings.ts b/src/database/entities/UserSettings.ts new file mode 100644 index 00000000..7ccb8e1e --- /dev/null +++ b/src/database/entities/UserSettings.ts @@ -0,0 +1,142 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; +import { CustomStatus, FriendSourceFlags, GuildFolder } from "@spacebar/schemas"; + +@Entity({ + name: "user_settings", +}) +export class UserSettings extends BaseClassWithoutId { + @PrimaryGeneratedColumn() + index: string; + + @Column({ nullable: true }) + afk_timeout: number = 3600; + + @Column({ nullable: true }) + allow_accessibility_detection: boolean = true; + + @Column({ nullable: true }) + animate_emoji: boolean = true; + + @Column({ nullable: true }) + animate_stickers: number = 0; + + @Column({ nullable: true }) + contact_sync_enabled: boolean = false; + + @Column({ nullable: true }) + convert_emoticons: boolean = false; + + @Column({ nullable: true, type: "jsonb" }) + custom_status: CustomStatus | null = null; + + @Column({ nullable: true }) + default_guilds_restricted: boolean = false; + + @Column({ nullable: true }) + detect_platform_accounts: boolean = false; + + @Column({ nullable: true }) + developer_mode: boolean = true; + + @Column({ nullable: true }) + disable_games_tab: boolean = true; + + @Column({ nullable: true }) + enable_tts_command: boolean = false; + + @Column({ nullable: true }) + explicit_content_filter: number = 0; + + @Column({ nullable: true }) + friend_discovery_flags: number = 0; + + @Column({ nullable: true, type: "jsonb" }) + friend_source_flags: FriendSourceFlags = { all: true }; + + @Column({ nullable: true }) + gateway_connected: boolean = false; + + @Column({ nullable: true }) + gif_auto_play: boolean = false; + + @Column({ nullable: true, type: "jsonb" }) + guild_folders: GuildFolder[] = []; // every top guild is displayed as a "folder" + + @Column({ nullable: true, type: "jsonb" }) + guild_positions: string[] = []; // guild ids ordered by position + + @Column({ nullable: true }) + inline_attachment_media: boolean = true; + + @Column({ nullable: true }) + inline_embed_media: boolean = true; + + @Column({ nullable: true }) + locale: string = "en-US"; // en_US + + @Column({ nullable: true }) + message_display_compact: boolean = false; + + @Column({ nullable: true }) + native_phone_integration_enabled: boolean = true; + + @Column({ nullable: true }) + render_embeds: boolean = true; + + @Column({ nullable: true }) + render_reactions: boolean = true; + + @Column({ nullable: true, type: "jsonb" }) + restricted_guilds: string[] = []; + + @Column({ nullable: true }) + show_current_game: boolean = true; + + @Column({ nullable: true }) + status: "online" | "offline" | "dnd" | "idle" | "invisible" = "online"; + + @Column({ nullable: true }) + stream_notifications_enabled: boolean = false; + + @Column({ nullable: true }) + theme: "dark" | "light" = "dark"; // dark + + @Column({ nullable: true }) + timezone_offset: number = 0; // e.g -60 + + @Column({ nullable: true }) + view_nsfw_guilds: boolean = true; + + public static async getOrDefault(userId: string) { + // raw sql query + const userSettingsIndex = (await this.getRepository().query('SELECT "settingsIndex" FROM users WHERE id = $1', [userId]))[0]?.settingsIndex as string | null; + + console.log(`[INFO/UserSettings] Fetched settings index for user ${userId}:`, userSettingsIndex); + + if (!userSettingsIndex) return new UserSettings(); + + const settings = await UserSettings.findOne({ where: { index: userSettingsIndex } }); + if (!settings) return new UserSettings(); + + return settings; + } +} diff --git a/src/database/entities/UserSettingsProtos.ts b/src/database/entities/UserSettingsProtos.ts new file mode 100644 index 00000000..c8ee9d12 --- /dev/null +++ b/src/database/entities/UserSettingsProtos.ts @@ -0,0 +1,145 @@ +/* + 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 . +*/ + +import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; +import { BaseClassWithoutId } from "./BaseClass"; +import { User } from "./User"; +import { FrecencyUserSettings, PreloadedUserSettings } from "discord-protos"; + +@Entity({ + name: "user_settings_protos", +}) +export class UserSettingsProtos extends BaseClassWithoutId { + @OneToOne(() => User, { + cascade: true, + orphanedRowAction: "delete", + eager: false, + }) + @JoinColumn({ name: "user_id" }) + user: User; + + @PrimaryColumn({ type: "text" }) + user_id: string; + + @Column({ nullable: true, type: String, name: "userSettings" }) + _userSettings: string | undefined; + + @Column({ nullable: true, type: String, name: "frecencySettings" }) + _frecencySettings: string | undefined; + + // @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)); + } + + set userSettings(value: PreloadedUserSettings | undefined) { + if (value) { + // this._userSettings = JSON.stringify(value, this.bigintReplacer); + this._userSettings = PreloadedUserSettings.toJsonString(value); + } else { + this._userSettings = undefined; + } + } + + get frecencySettings(): FrecencyUserSettings | undefined { + if (!this._frecencySettings) return undefined; + return FrecencyUserSettings.fromJson(JSON.parse(this._frecencySettings, this.bigintReviver)); + } + + set frecencySettings(value: FrecencyUserSettings | undefined) { + if (value) { + this._frecencySettings = JSON.stringify(value, this.bigintReplacer); + } else { + this._frecencySettings = undefined; + } + } + + static async getOrDefault(user_id: string, save: boolean = false): Promise { + await User.findOneOrFail({ + where: { id: user_id }, + select: { settings: true }, + }); + + let userSettings = await UserSettingsProtos.findOne({ + where: { user_id }, + }); + + let modified = false; + if (!userSettings) { + userSettings = UserSettingsProtos.create({ + user_id, + }); + modified = true; + } + + if (!userSettings.userSettings) { + userSettings.userSettings = PreloadedUserSettings.create({ + versions: { + dataVersion: 0, + clientVersion: 0, + serverVersion: 0, + }, + }); + modified = true; + } + + if (!userSettings.frecencySettings) { + userSettings.frecencySettings = FrecencyUserSettings.create({ + versions: { + dataVersion: 0, + clientVersion: 0, + serverVersion: 0, + }, + }); + modified = true; + } + + if (modified && save) userSettings = await userSettings.save(); + + return userSettings; + } +} diff --git a/src/database/entities/ValidRegistrationTokens.ts b/src/database/entities/ValidRegistrationTokens.ts new file mode 100644 index 00000000..f8cb35a4 --- /dev/null +++ b/src/database/entities/ValidRegistrationTokens.ts @@ -0,0 +1,33 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity({ + name: "valid_registration_tokens", +}) +export class ValidRegistrationToken extends BaseEntity { + @PrimaryColumn() + token: string; + + @Column() + created_at: Date = new Date(); + + @Column() + expires_at: Date; +} diff --git a/src/database/entities/VoiceState.ts b/src/database/entities/VoiceState.ts new file mode 100644 index 00000000..5abdb7da --- /dev/null +++ b/src/database/entities/VoiceState.ts @@ -0,0 +1,107 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { Guild } from "./Guild"; +import { Member } from "./Member"; +import { User } from "./User"; +import { PublicVoiceState, PublicVoiceStateProjection } from "@spacebar/schemas"; + +//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex +@Entity({ + name: "voice_states", +}) +export class VoiceState extends BaseClass { + @Column({ nullable: true }) + @RelationId((voice_state: VoiceState) => voice_state.guild) + guild_id: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, (guild) => guild.voice_states, { + onDelete: "CASCADE", + }) + guild?: Guild; + + @Column({ nullable: true }) + @RelationId((voice_state: VoiceState) => voice_state.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column({ nullable: true }) + @RelationId((voice_state: VoiceState) => voice_state.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + // @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }]) + // @ManyToOne(() => Member, { + // onDelete: "CASCADE", + // }) + //TODO find a way to make it work without breaking Guild.voice_states + member: Member; + + @Column() + session_id: string; + + @Column({ nullable: true }) + token: string; + + @Column() + deaf: boolean; + + @Column() + mute: boolean; + + @Column() + self_deaf: boolean; + + @Column() + self_mute: boolean; + + @Column({ nullable: true }) + self_stream?: boolean; + + @Column() + self_video: boolean; + + @Column() + suppress: boolean; // whether this user is muted by the current user + + @Column({ nullable: true, default: null }) + request_to_speak_timestamp?: Date; + + toPublicVoiceState() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const voiceState: any = {}; + PublicVoiceStateProjection.forEach((x) => { + voiceState[x] = this[x]; + }); + return voiceState as PublicVoiceState; + } +} diff --git a/src/database/entities/Webhook.ts b/src/database/entities/Webhook.ts new file mode 100644 index 00000000..f631c393 --- /dev/null +++ b/src/database/entities/Webhook.ts @@ -0,0 +1,104 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { Application } from "./Application"; +import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { Guild } from "./Guild"; +import { User } from "./User"; +import { WebhookType } from "@spacebar/schemas"; + +@Entity({ + name: "webhooks", +}) +export class Webhook extends BaseClass { + @Column({ type: "int" }) + type: WebhookType; + + @Column({ nullable: true }) + name: string; + + @Column({ nullable: true }) + avatar: string; + + @Column({ nullable: true }) + token?: string; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.guild) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild?: Guild; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + channel: Channel; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.application) + application_id: string; + + @JoinColumn({ name: "application_id" }) + @ManyToOne(() => Application, { + onDelete: "CASCADE", + }) + application: Application; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.user) + user_id: string; + + @JoinColumn({ name: "user_id" }) + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + user: User; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.guild) + source_guild_id?: string; + + @JoinColumn({ name: "source_guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + source_guild?: Guild; + + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.channel) + source_channel_id: string; + + @JoinColumn({ name: "source_channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + source_channel: Channel; + + url: string; +} diff --git a/src/database/entities/index.ts b/src/database/entities/index.ts new file mode 100644 index 00000000..79fceecc --- /dev/null +++ b/src/database/entities/index.ts @@ -0,0 +1,66 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2024 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 . +*/ + +export * from "./Application"; +export * from "./ApplicationCommand"; +export * from "./Attachment"; +export * from "./AuditLog"; +export * from "./AutomodRule"; +export * from "./BackupCodes"; +export * from "./Badge"; +export * from "./Ban"; +export * from "./BaseClass"; +export * from "./Categories"; +export * from "./Channel"; +export * from "./ClientRelease"; +export * from "./CloudAttachment"; +export * from "./Config"; +export * from "./ConnectedAccount"; +export * from "./ConnectionConfigEntity"; +export * from "./EmbedCache"; +export * from "./Emoji"; +export * from "./Encryption"; +export * from "./Guild"; +export * from "./InstanceBan"; +export * from "./Invite"; +export * from "./Member"; +export * from "./Message"; +export * from "./Migration"; +export * from "./Note"; +export * from "./RateLimit"; +export * from "./ReadState"; +export * from "./Recipient"; +export * from "./Relationship"; +export * from "./Role"; +export * from "./SecurityKey"; +export * from "./Session"; +export * from "./Sticker"; +export * from "./StickerPack"; +export * from "./Stream"; +export * from "./StreamSession"; +export * from "./Team"; +export * from "./TeamMember"; +export * from "./Template"; +export * from "./ThreadMember"; +export * from "./User"; +export * from "./UserSettings"; +export * from "./UserSettingsProtos"; +export * from "./ValidRegistrationTokens"; +export * from "./VoiceState"; +export * from "./Webhook"; +export * from "./Tag"; diff --git a/src/database/index.ts b/src/database/index.ts index 0ceebea8..3ec764b3 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -16,4 +16,7 @@ along with this program. If not, see . */ +import "reflect-metadata"; + export * from "./Database"; +export * from "./entities/index"; diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts index 6a3c6cb3..c227c33d 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ -import { WebSocket } from "@spacebar/gateway"; -import { emitEvent, Member, PresenceUpdateEvent, Session, SessionsReplace, User, VoiceState, VoiceStateUpdateEvent, distributePresenceUpdate } from "@spacebar/util"; import { randomString } from "@spacebar/api"; +import { Member, Session, User, VoiceState } from "@spacebar/database"; +import { WebSocket } from "@spacebar/gateway"; +import { emitEvent, PresenceUpdateEvent, SessionsReplace, VoiceStateUpdateEvent, distributePresenceUpdate } from "@spacebar/util"; import { ProcessLifecycle } from "../../util/util/ProcessLifecycle"; export async function Close(this: WebSocket, code: number, reason: Buffer) { diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 70571e5f..226859a8 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -16,26 +16,13 @@ along with this program. If not, see . */ -import { - Ban, - EVENTEnum, - EventOpts, - getPermission, - listenEvent, - ListenEventOpts, - Member, - Message, - NewUrlUserSignatureData, - Permissions, - RabbitMQ, - Recipient, - Relationship, -} from "@spacebar/util"; -import { CLOSECODES, OPCODES, Send } from "../util"; -import { WebSocket } from "@spacebar/gateway"; import { Channel as AMQChannel } from "amqplib"; -import { PublicMember, RelationshipType } from "@spacebar/schemas"; import { bgRedBright } from "picocolors"; +import { Ban, Member, Message, Recipient, Relationship } from "@spacebar/database"; +import { EVENTEnum, EventOpts, getPermission, listenEvent, ListenEventOpts, NewUrlUserSignatureData, Permissions, RabbitMQ } from "@spacebar/util"; +import { WebSocket } from "@spacebar/gateway"; +import { PublicMember, RelationshipType } from "@spacebar/schemas"; +import { CLOSECODES, OPCODES, Send } from "../util"; // TODO: close connection on Invalidated Token // TODO: check intent diff --git a/src/gateway/opcodes/GuildSync.ts b/src/gateway/opcodes/GuildSync.ts index 04d3de56..f075028b 100644 --- a/src/gateway/opcodes/GuildSync.ts +++ b/src/gateway/opcodes/GuildSync.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ -import { Member, Session, Presence, timePromise, Stopwatch, Config, getMostRelevantSession } from "@spacebar/util"; +import { In } from "typeorm"; +import { Member, Session } from "@spacebar/database"; import { WebSocket, Payload, OPCODES, Send, handleOffloadedGatewayRequest } from "@spacebar/gateway"; import { PublicMember } from "@spacebar/schemas"; -import { In } from "typeorm"; +import { Presence, timePromise, Stopwatch, Config, getMostRelevantSession } from "@spacebar/util"; // TODO: only show roles/members that have access to this channel // TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online diff --git a/src/gateway/opcodes/Heartbeat.ts b/src/gateway/opcodes/Heartbeat.ts index 5e60a65a..a02e3b67 100644 --- a/src/gateway/opcodes/Heartbeat.ts +++ b/src/gateway/opcodes/Heartbeat.ts @@ -16,11 +16,11 @@ along with this program. If not, see . */ +import { FindOptionsWhere } from "typeorm"; import { OPCODES, Payload, WebSocket } from "@spacebar/gateway"; +import { Session } from "@spacebar/database"; import { setHeartbeat } from "../util/Heartbeat"; import { Send } from "../util/Send"; -import { Session } from "@spacebar/util"; -import { FindOptionsWhere } from "typeorm"; interface QoSData { seq: number | null; diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index b4b127e5..9c87aec9 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -20,46 +20,48 @@ import { In, Not } from "typeorm"; import { PreloadedUserSettings } from "discord-protos"; import { Capabilities, CLOSECODES, OPCODES, Payload, Send, setupListener, WebSocket } from "@spacebar/gateway"; import { arrayGroupBy } from "@spacebar/extensions"; -import { getDatabase } from "@spacebar/database"; import { + getDatabase, Application, Channel, + Emoji, + Guild, + Member, + MemberPrivateProjection, + ReadState, + Recipient, + Relationship, + Role, + Session, + Sticker, + ThreadMember, + UserSettings, + UserSettingsProtos, + VoiceState, +} from "@spacebar/database"; +import { checkToken, Config, CurrentTokenFormatVersion, ElapsedTime, emitEvent, - Emoji, EVENTEnum, generateToken, - Guild, GuildOrUnavailable, Intents, - Member, - MemberPrivateProjection, OPCodes, PresenceUpdateEvent, - ReadState, ReadyEventData, ReadyGuildDTO, ReadyUserGuildSettingsEntries, - Recipient, - Relationship, - Role, - Session, SessionsReplace, - Sticker, Stopwatch, - ThreadMember, timeFunction, timePromise, TraceNode, TraceRoot, - UserSettings, - UserSettingsProtos, - VoiceState, } from "@spacebar/util"; -import { ChannelType, DefaultUserGuildSettings, DMChannel, IdentifySchema, PrivateUserProjection, PublicUser, PublicUserProjection, RelationshipType } from "@spacebar/schemas"; +import { ChannelType, DefaultUserGuildSettings, DMChannel, IdentifySchema, PrivateUserProjection, PublicUser, PublicUserProjection } from "@spacebar/schemas"; import { randomString } from "@spacebar/api"; import { check } from "./instanceOf"; diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index d8900d9e..d8f4cb5a 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -17,11 +17,11 @@ */ import murmur from "murmurhash-js/murmurhash3_gc"; -import { getDatabase } from "@spacebar/database"; +import { getDatabase, Member, Role, Session, User, Channel } from "@spacebar/database"; import { arrayPartition } from "@spacebar/extensions"; import { WebSocket, Payload, handlePresenceUpdate, OPCODES, Send } from "@spacebar/gateway"; import { LazyRequestSchema } from "@spacebar/schemas"; -import { getPermission, listenEvent, Member, Role, Session, User, Presence, Channel, Permissions, getMostRelevantSession } from "@spacebar/util"; +import { getPermission, listenEvent, Presence, Permissions, getMostRelevantSession } from "@spacebar/util"; import { check } from "./instanceOf"; // TODO: only show roles/members that have access to this channel diff --git a/src/gateway/opcodes/PresenceUpdate.ts b/src/gateway/opcodes/PresenceUpdate.ts index 47f84fa9..a0be5f65 100644 --- a/src/gateway/opcodes/PresenceUpdate.ts +++ b/src/gateway/opcodes/PresenceUpdate.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ +import { Session, User } from "@spacebar/database"; import { WebSocket, Payload } from "@spacebar/gateway"; -import { emitEvent, InternalStatusOrder, PresenceUpdateEvent, PrivateStatus, PublicStatus, PublicStatusOrder, Session, User } from "@spacebar/util"; -import { check } from "./instanceOf"; +import { emitEvent, InternalStatusOrder, PresenceUpdateEvent, PrivateStatus } from "@spacebar/util"; import { ActivitySchema } from "@spacebar/schemas"; +import { check } from "./instanceOf"; export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { const startTime = Date.now(); diff --git a/src/gateway/opcodes/RequestChannelInfo.ts b/src/gateway/opcodes/RequestChannelInfo.ts index efa3e728..8cb9098d 100644 --- a/src/gateway/opcodes/RequestChannelInfo.ts +++ b/src/gateway/opcodes/RequestChannelInfo.ts @@ -16,9 +16,10 @@ along with this program. If not, see . */ +import { Channel } from "@spacebar/database"; import { WebSocket, Payload, OPCODES, Send, handleOffloadedGatewayRequest } from "@spacebar/gateway"; import { ChannelType } from "@spacebar/schemas"; -import { Channel, Config } from "@spacebar/util"; +import { Config } from "@spacebar/util"; export async function onRequestChannelInfo(this: WebSocket, { d }: Payload) { // Schema validation can only accept either string or array, so transforming it here to support both diff --git a/src/gateway/opcodes/RequestGuildMembers.ts b/src/gateway/opcodes/RequestGuildMembers.ts index 88b24d6c..ec8e17cf 100644 --- a/src/gateway/opcodes/RequestGuildMembers.ts +++ b/src/gateway/opcodes/RequestGuildMembers.ts @@ -17,10 +17,10 @@ */ import { FindManyOptions, ILike, In, MoreThan } from "typeorm"; -import { getDatabase } from "@spacebar/database"; +import { getDatabase, Member, Session } from "@spacebar/database"; import { WebSocket, Payload, OPCODES, Send, handleOffloadedGatewayRequest } from "@spacebar/gateway"; import { RequestGuildMembersSchema } from "@spacebar/schemas"; -import { Config, DateBuilder, getPermission, GuildMembersChunkEvent, Member, Presence, Session } from "@spacebar/util"; +import { Config, DateBuilder, getPermission, GuildMembersChunkEvent, Presence } from "@spacebar/util"; import { check } from "./instanceOf"; export async function onRequestGuildMembers(this: WebSocket, { d }: Payload) { diff --git a/src/gateway/opcodes/StreamCreate.ts b/src/gateway/opcodes/StreamCreate.ts index d1faffb1..6fc222ab 100644 --- a/src/gateway/opcodes/StreamCreate.ts +++ b/src/gateway/opcodes/StreamCreate.ts @@ -1,17 +1,24 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 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 . +*/ + +import { Channel, Member, Stream, StreamSession, VoiceState } from "@spacebar/database"; import { genVoiceToken, Payload, WebSocket, generateStreamKey } from "@spacebar/gateway"; -import { - Channel, - Config, - emitEvent, - Member, - Snowflake, - Stream, - StreamCreateEvent, - StreamServerUpdateEvent, - StreamSession, - VoiceState, - VoiceStateUpdateEvent, -} from "@spacebar/util"; +import { Config, emitEvent, Snowflake, StreamCreateEvent, StreamServerUpdateEvent, VoiceStateUpdateEvent } from "@spacebar/util"; import { check } from "./instanceOf"; import { StreamCreateSchema } from "@spacebar/schemas"; diff --git a/src/gateway/opcodes/StreamDelete.ts b/src/gateway/opcodes/StreamDelete.ts index 1ccc5ca6..82d09538 100644 --- a/src/gateway/opcodes/StreamDelete.ts +++ b/src/gateway/opcodes/StreamDelete.ts @@ -1,5 +1,24 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + +import { Member, Stream, VoiceState } from "@spacebar/database"; import { parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; -import { emitEvent, Member, Stream, StreamDeleteEvent, VoiceState, VoiceStateUpdateEvent } from "@spacebar/util"; +import { emitEvent, StreamDeleteEvent, VoiceStateUpdateEvent } from "@spacebar/util"; import { check } from "./instanceOf"; import { StreamDeleteSchema } from "@spacebar/schemas"; diff --git a/src/gateway/opcodes/StreamWatch.ts b/src/gateway/opcodes/StreamWatch.ts index 040fb954..f63e4a15 100644 --- a/src/gateway/opcodes/StreamWatch.ts +++ b/src/gateway/opcodes/StreamWatch.ts @@ -1,8 +1,27 @@ -import { genVoiceToken, parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; -import { Config, emitEvent, Stream, StreamCreateEvent, StreamServerUpdateEvent, StreamSession } from "@spacebar/util"; -import { check } from "./instanceOf"; +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + import { Not } from "typeorm"; +import { Stream, StreamSession } from "@spacebar/database"; +import { genVoiceToken, parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; +import { Config, emitEvent, StreamCreateEvent, StreamServerUpdateEvent } from "@spacebar/util"; import { StreamWatchSchema } from "@spacebar/schemas"; +import { check } from "./instanceOf"; export async function onStreamWatch(this: WebSocket, data: Payload) { const startTime = Date.now(); diff --git a/src/gateway/opcodes/VoiceStateUpdate.ts b/src/gateway/opcodes/VoiceStateUpdate.ts index 0bc09852..e6975358 100644 --- a/src/gateway/opcodes/VoiceStateUpdate.ts +++ b/src/gateway/opcodes/VoiceStateUpdate.ts @@ -16,10 +16,11 @@ along with this program. If not, see . */ +import { Guild, Member, VoiceState } from "@spacebar/database"; import { Payload, WebSocket, genVoiceToken } from "@spacebar/gateway"; -import { Config, emitEvent, Guild, Member, VoiceServerUpdateEvent, VoiceState, VoiceStateUpdateEvent } from "@spacebar/util"; -import { check } from "./instanceOf"; +import { Config, emitEvent, VoiceServerUpdateEvent, VoiceStateUpdateEvent } from "@spacebar/util"; import { Region, VoiceStateUpdateSchema } from "@spacebar/schemas"; +import { check } from "./instanceOf"; // TODO: check if a voice server is setup // Notice: Bot users respect the voice channel's user limit, if set. diff --git a/src/gateway/util/Utils.ts b/src/gateway/util/Utils.ts index 20880622..23e9569b 100644 --- a/src/gateway/util/Utils.ts +++ b/src/gateway/util/Utils.ts @@ -16,8 +16,8 @@ along with this program. If not, see . */ -import { sleep } from "@spacebar/extensions"; -import { Event, Session, TimeSpan, VoiceState } from "@spacebar/util"; +import { Session, VoiceState } from "@spacebar/database"; +import { Event, TimeSpan } from "@spacebar/util"; import { WebSocket } from "./WebSocket"; import { OPCODES } from "./Constants"; import { Send } from "./Send"; diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 41dc93c1..50c7bbb1 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -16,12 +16,13 @@ along with this program. If not, see . */ -import { Intents, ListenEventOpts, Permissions, Session } from "@spacebar/util"; import WS from "ws"; import { Deflate, Inflate } from "fast-zlib"; -import { Capabilities } from "./Capabilities"; import { Decoder, Encoder } from "@toondepauw/node-zstd"; +import { Session } from "@spacebar/database"; +import { Intents, ListenEventOpts, Permissions } from "@spacebar/util"; import { QoSPayload } from "../opcodes/Heartbeat"; +import { Capabilities } from "./Capabilities"; export interface WebSocket extends WS { recentTransactions: string[]; diff --git a/src/schemas/api/bots/InteractionCreateSchema.ts b/src/schemas/api/bots/InteractionCreateSchema.ts index 5a6d1c3f..ff7cfaf8 100644 --- a/src/schemas/api/bots/InteractionCreateSchema.ts +++ b/src/schemas/api/bots/InteractionCreateSchema.ts @@ -18,7 +18,7 @@ import { PublicMember, PublicUser, Snowflake, InteractionType } from "@spacebar/schemas"; // TODO: remove entity imports -import { Channel, Message } from "@spacebar/util"; +import { Channel, Message } from "@spacebar/database"; export interface InteractionCreateSchema { version: number; // TODO: types? diff --git a/src/schemas/api/channels/Channel.ts b/src/schemas/api/channels/Channel.ts index e3902626..33514b82 100644 --- a/src/schemas/api/channels/Channel.ts +++ b/src/schemas/api/channels/Channel.ts @@ -17,7 +17,7 @@ */ // TODO: remove entity imports -import { Channel, Recipient, Tag, ThreadMember } from "@spacebar/util"; +import { Channel, Recipient, Tag, ThreadMember } from "@spacebar/database"; import { HTTPError } from "lambert-server/HTTPError"; import { Snowflake } from "../../Identifiers"; import { PartialUser, PublicMember } from "../users"; diff --git a/src/schemas/api/guilds/VoiceState.ts b/src/schemas/api/guilds/VoiceState.ts index 278e9678..1c2f7d00 100644 --- a/src/schemas/api/guilds/VoiceState.ts +++ b/src/schemas/api/guilds/VoiceState.ts @@ -17,7 +17,7 @@ */ //TODO: remove entity import -import { VoiceState } from "@spacebar/util"; +import { VoiceState } from "@spacebar/database"; export enum PublicVoiceStateEnum { user_id, diff --git a/src/schemas/api/messages/Message.ts b/src/schemas/api/messages/Message.ts index 73d0e79b..a6d5f162 100644 --- a/src/schemas/api/messages/Message.ts +++ b/src/schemas/api/messages/Message.ts @@ -17,7 +17,7 @@ */ // TODO: remove entity import -import { Sticker } from "@spacebar/util"; +import { Sticker } from "@spacebar/database"; import { Embed, MessageActivity, MessageComponent, PartialUser, Poll, PublicChannel, Snowflake } from "@spacebar/schemas"; import { PublicAttachment } from "./Attachments"; diff --git a/src/schemas/api/users/ConnectedAccount.ts b/src/schemas/api/users/ConnectedAccount.ts index 281fc58f..0779a769 100644 --- a/src/schemas/api/users/ConnectedAccount.ts +++ b/src/schemas/api/users/ConnectedAccount.ts @@ -17,6 +17,6 @@ */ //TODO: remove entity import -import { ConnectedAccount } from "@spacebar/util"; +import { ConnectedAccount } from "@spacebar/database"; export type PublicConnectedAccount = Pick; diff --git a/src/schemas/api/users/Member.ts b/src/schemas/api/users/Member.ts index 049190e0..0cfaa0c9 100644 --- a/src/schemas/api/users/Member.ts +++ b/src/schemas/api/users/Member.ts @@ -16,9 +16,9 @@ along with this program. If not, see . */ -import { PublicUser } from "@spacebar/schemas"; // TODO: remove entity import -import { Member } from "@spacebar/util"; +import { Member } from "@spacebar/database"; +import { PublicUser } from "@spacebar/schemas"; export interface ChannelOverride { message_notifications: number; diff --git a/src/schemas/api/users/User.ts b/src/schemas/api/users/User.ts index 0360eb03..8a3449ba 100644 --- a/src/schemas/api/users/User.ts +++ b/src/schemas/api/users/User.ts @@ -20,7 +20,7 @@ import { ConnectedAccountSchema, Snowflake, UserSettingsSchema } from "@spacebar // TODO: maybe move the one thing this depends on to import { BitField } from "@spacebar/util/util"; // TODO: remove entity import -import { Relationship, Session } from "@spacebar/util/entities"; +import { Relationship, Session } from "@spacebar/database/entities"; interface UserEntityPleaseRewriteThankYou { id: Snowflake; diff --git a/src/schemas/api/users/index.ts b/src/schemas/api/users/index.ts index 85c92662..5c9b2008 100644 --- a/src/schemas/api/users/index.ts +++ b/src/schemas/api/users/index.ts @@ -21,3 +21,4 @@ export * from "./InstanceUserDeleteSchema"; export * from "./Member"; export * from "./User"; export * from "./UserSettings"; +export * from "./SessionsSchemas"; diff --git a/src/schemas/responses/DiscoverableGuildsResponse.ts b/src/schemas/responses/DiscoverableGuildsResponse.ts index 02c46f1c..711c10cd 100644 --- a/src/schemas/responses/DiscoverableGuildsResponse.ts +++ b/src/schemas/responses/DiscoverableGuildsResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; export interface DiscoverableGuildsResponse { total: number; diff --git a/src/schemas/responses/EmailDomainLookupVerifyCodeResponse.ts b/src/schemas/responses/EmailDomainLookupVerifyCodeResponse.ts index b2400e95..d868d897 100644 --- a/src/schemas/responses/EmailDomainLookupVerifyCodeResponse.ts +++ b/src/schemas/responses/EmailDomainLookupVerifyCodeResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; export interface EmailDomainLookupVerifyCodeResponse { guild: Guild; diff --git a/src/schemas/responses/EmojiSourceResponse.ts b/src/schemas/responses/EmojiSourceResponse.ts index f01848a8..511ba380 100644 --- a/src/schemas/responses/EmojiSourceResponse.ts +++ b/src/schemas/responses/EmojiSourceResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove entity import -import { Emoji } from "@spacebar/util"; +import { Emoji } from "@spacebar/database"; export interface EmojiSourceResponse { type: "GUILD" | "APPLICATION"; diff --git a/src/schemas/responses/GuildMessagesSearchResponse.ts b/src/schemas/responses/GuildMessagesSearchResponse.ts index e498f8bc..3cc35ce2 100644 --- a/src/schemas/responses/GuildMessagesSearchResponse.ts +++ b/src/schemas/responses/GuildMessagesSearchResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { Role } from "@spacebar/util"; +import { Role } from "@spacebar/database"; import { BaseMessageComponents, Embed, MessageType, Poll, PublicUser } from "@spacebar/schemas"; import { PublicAttachment } from "../api/messages/Attachments"; diff --git a/src/schemas/responses/GuildRecommendationsResponse.ts b/src/schemas/responses/GuildRecommendationsResponse.ts index c8fed072..f0149d09 100644 --- a/src/schemas/responses/GuildRecommendationsResponse.ts +++ b/src/schemas/responses/GuildRecommendationsResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; export interface GuildRecommendationsResponse { recommended_guilds: Guild[]; diff --git a/src/schemas/responses/HubDirectoryEntriesResponse.ts b/src/schemas/responses/HubDirectoryEntriesResponse.ts index c19b6a14..fc710aa2 100644 --- a/src/schemas/responses/HubDirectoryEntriesResponse.ts +++ b/src/schemas/responses/HubDirectoryEntriesResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { Guild } from "@spacebar/util"; +import { Guild } from "@spacebar/database"; export interface HubDirectoryEntry { author_id: string; diff --git a/src/schemas/responses/MemberJoinGuildResponse.ts b/src/schemas/responses/MemberJoinGuildResponse.ts index bbb370cc..d37ed865 100644 --- a/src/schemas/responses/MemberJoinGuildResponse.ts +++ b/src/schemas/responses/MemberJoinGuildResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove entity imports -import { Emoji, Role, Sticker } from "@spacebar/util"; +import { Emoji, Role, Sticker } from "@spacebar/database"; import { GuildCreateResponse } from "@spacebar/schemas"; export interface MemberJoinGuildResponse { diff --git a/src/schemas/responses/PreloadMessagesResponseSchema.ts b/src/schemas/responses/PreloadMessagesResponseSchema.ts index 0a767888..ea3d38d9 100644 --- a/src/schemas/responses/PreloadMessagesResponseSchema.ts +++ b/src/schemas/responses/PreloadMessagesResponseSchema.ts @@ -17,6 +17,6 @@ */ // TODO: remove entity import -import { Message } from "@spacebar/util"; +import { Message } from "@spacebar/database"; export type PreloadMessagesResponseSchema = Message[]; diff --git a/src/schemas/responses/TeamListResponse.ts b/src/schemas/responses/TeamListResponse.ts index d8bf8564..fc4d93e2 100644 --- a/src/schemas/responses/TeamListResponse.ts +++ b/src/schemas/responses/TeamListResponse.ts @@ -17,6 +17,6 @@ */ // TODO: remove entity import -import { Team } from "@spacebar/util"; +import { Team } from "@spacebar/database"; export type TeamListResponse = Team[]; diff --git a/src/schemas/responses/TokenResponse.ts b/src/schemas/responses/TokenResponse.ts index 4a0c0834..88ac5e20 100644 --- a/src/schemas/responses/TokenResponse.ts +++ b/src/schemas/responses/TokenResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { BackupCode, UserSettings } from "@spacebar/util"; +import { BackupCode, UserSettings } from "@spacebar/database"; export interface TokenResponse { token: string; diff --git a/src/schemas/responses/TypedResponses.ts b/src/schemas/responses/TypedResponses.ts index 9fa6cd0c..9b7d64ee 100644 --- a/src/schemas/responses/TypedResponses.ts +++ b/src/schemas/responses/TypedResponses.ts @@ -20,7 +20,7 @@ import { GeneralConfiguration, LimitsConfiguration } from "../../util/config/types"; import { DmChannelDTO } from "../../util/dtos"; // TODO: remove entity imports -import { Application, BackupCode, Categories, Channel, Guild, Invite, Member, Message, Role, Template, Webhook } from "@spacebar/util"; +import { Application, BackupCode, Categories, Channel, Guild, Invite, Member, Message, Role, Template, Webhook } from "@spacebar/database"; import { GuildVoiceRegion } from "./GuildVoiceRegionsResponse"; import { GuildCreateResponse, PrivateUser, PublicMember, PublicUser } from "@spacebar/schemas"; diff --git a/src/schemas/responses/UserProfileResponse.ts b/src/schemas/responses/UserProfileResponse.ts index 64459bd1..daf0c460 100644 --- a/src/schemas/responses/UserProfileResponse.ts +++ b/src/schemas/responses/UserProfileResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove entity imports -import { Badge, Member, User } from "@spacebar/util"; +import { Badge, Member, User } from "@spacebar/database"; import { PublicConnectedAccount, PublicMember, PublicUser } from "@spacebar/schemas"; export type MutualGuild = { diff --git a/src/schemas/responses/WebhookCreateResponse.ts b/src/schemas/responses/WebhookCreateResponse.ts index 08d997fb..2f5e92f0 100644 --- a/src/schemas/responses/WebhookCreateResponse.ts +++ b/src/schemas/responses/WebhookCreateResponse.ts @@ -17,7 +17,7 @@ */ // TODO: remove dependency on entities -import { User, Webhook } from "@spacebar/util"; +import { User, Webhook } from "@spacebar/database"; export interface WebhookCreateResponse { user: User; diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index 56b2b899..e0ec56ba 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -17,7 +17,7 @@ */ import crypto from "node:crypto"; -import { ConnectedAccount } from "../entities"; +import { ConnectedAccount } from "../../database/entities"; import { ConnectedAccountSchema, ConnectionCallbackSchema } from "@spacebar/schemas"; import { Config, DiscordApiErrors } from "../util"; diff --git a/src/util/connections/ConnectionConfig.ts b/src/util/connections/ConnectionConfig.ts index c5da3878..338b97be 100644 --- a/src/util/connections/ConnectionConfig.ts +++ b/src/util/connections/ConnectionConfig.ts @@ -17,7 +17,7 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ConnectionConfigEntity } from "../entities"; +import { ConnectionConfigEntity } from "../../database/entities"; import { OrmUtils } from "../imports"; let config: any; diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts index 3d7761f7..8418b085 100644 --- a/src/util/connections/RefreshableConnection.ts +++ b/src/util/connections/RefreshableConnection.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { ConnectedAccount } from "../entities"; +import { ConnectedAccount } from "../../database/entities"; import { Connection } from "./Connection"; import { ConnectedAccountCommonOAuthTokenResponse } from "@spacebar/schemas"; diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts index a137d732..dfa3db01 100644 --- a/src/util/dtos/ConnectedAccountDTO.ts +++ b/src/util/dtos/ConnectedAccountDTO.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { ConnectedAccount } from "../entities"; +import { ConnectedAccount } from "../../database/entities"; export class ConnectedAccountDTO { id: string; diff --git a/src/util/dtos/DmChannelDTO.ts b/src/util/dtos/DmChannelDTO.ts index f7c42d96..9e6f05da 100644 --- a/src/util/dtos/DmChannelDTO.ts +++ b/src/util/dtos/DmChannelDTO.ts @@ -17,7 +17,7 @@ */ import { MinimalPublicUserDTO } from "./UserDTO"; -import { Channel, User } from "../entities"; +import { Channel, User } from "../../database/entities"; import { PublicUserProjection } from "@spacebar/schemas"; export class DmChannelDTO { diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts index 1b9677e0..b2475847 100644 --- a/src/util/dtos/ReadyGuildDTO.ts +++ b/src/util/dtos/ReadyGuildDTO.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { Channel, Emoji, Guild, Role, Sticker } from "../entities"; +import { Channel, Emoji, Guild, Role, Sticker } from "../../database/entities"; import { ChannelOverride, ChannelType, PublicMember, PublicUser, UserGuildSettings } from "@spacebar/schemas"; // TODO: this is not the best place for this type diff --git a/src/util/dtos/UserDTO.ts b/src/util/dtos/UserDTO.ts index 2a79f8f1..63f1c3ad 100644 --- a/src/util/dtos/UserDTO.ts +++ b/src/util/dtos/UserDTO.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { User } from "../entities"; +import { User } from "../../database/entities"; export class MinimalPublicUserDTO { avatar?: string | null; diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts deleted file mode 100644 index c4f40343..00000000 --- a/src/util/entities/Application.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Team } from "./Team"; -import { User } from "./User"; -import { Guild } from "./Guild"; - -@Entity({ - name: "applications", -}) -export class Application extends BaseClass { - @Column() - name: string; - - @Column({ nullable: true }) - icon?: string; - - @Column({ nullable: true }) - description: string; - - @Column({ nullable: true }) - summary: string = ""; - - @Column({ type: "jsonb", nullable: true }) - type?: object; // TODO: this type is bad - - @Column() - hook: boolean = true; - - @Column() - bot_public?: boolean = true; - - @Column() - bot_require_code_grant?: boolean = false; - - @Column() - verify_key: string; - - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User, { onDelete: "CASCADE" }) - owner: User; - - // TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags - @Column() - flags: number = 0; - - @Column({ type: "varchar", nullable: true }) - redirect_uris: string[] = []; - - @Column({ nullable: true }) - rpc_application_state: number = 0; - - @Column({ nullable: true }) - store_application_state: number = 1; - - @Column({ nullable: true }) - verification_state: number = 1; - - @Column({ nullable: true }) - interactions_endpoint_url?: string; - - @Column({ nullable: true }) - integration_public: boolean = true; - - @Column({ nullable: true }) - integration_require_code_grant: boolean = false; - - @Column({ nullable: true }) - discoverability_state: number = 1; - - @Column({ nullable: true }) - discovery_eligibility_flags: number = 2240; - - @JoinColumn({ name: "bot_user_id" }) - @OneToOne(() => User, { onDelete: "CASCADE" }) - bot?: User; - - @Column({ type: "varchar", array: true, nullable: true }) - tags?: string[]; - - @Column({ nullable: true }) - cover_image?: string; // the application's default rich presence invite cover image hash - - @Column({ type: "jsonb", nullable: true }) - install_params?: { scopes: string[]; permissions: string }; - - @Column({ nullable: true }) - terms_of_service_url?: string; - - @Column({ nullable: true }) - privacy_policy_url?: string; - - @Column({ nullable: true }) - @RelationId((application: Application) => application.guild) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild) - guild?: Guild; // guild to which the app is linked, e.g. a developer support server - - @Column({ nullable: true }) - custom_install_url?: string; - - //just for us - - //@Column({ type: "varchar", array: true, nullable: true }) - //rpc_origins?: string[]; - - //@Column({ nullable: true }) - //primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created, - - //@Column({ nullable: true }) - //slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page - - @JoinColumn({ name: "team_id" }) - @ManyToOne(() => Team, { - onDelete: "CASCADE", - nullable: true, - }) - team?: Team; -} diff --git a/src/util/entities/ApplicationCommand.ts b/src/util/entities/ApplicationCommand.ts deleted file mode 100644 index 892bb833..00000000 --- a/src/util/entities/ApplicationCommand.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { - ApplicationCommandHandlerType, - ApplicationCommandOption, - ApplicationCommandIndexPermissions, - ApplicationCommandType, - Snowflake, - ApplicationIntegrationType, - InteractionContextType, -} from "@spacebar/schemas"; - -@Entity({ - name: "application_commands", -}) -export class ApplicationCommand extends BaseClass { - @Column({ default: ApplicationCommandType.CHAT_INPUT }) - type?: ApplicationCommandType; - - @Column() - application_id: Snowflake; - - @Column({ nullable: true }) - guild_id?: Snowflake; - - @Column() - name: string; - - @Column({ nullable: true, type: "jsonb" }) - name_localizations?: Record; - - @Column() - description: string; - - @Column({ nullable: true, type: "jsonb" }) - description_localizations?: Record; - - @Column({ type: "jsonb", default: [] }) - options: ApplicationCommandOption[]; - - @Column({ nullable: true, type: String }) - default_member_permissions: string | null; - - /* - * @deprecated - */ - @Column({ default: true }) - dm_permission?: boolean; - - @Column({ nullable: true, type: "jsonb" }) - permissions?: ApplicationCommandIndexPermissions; - - @Column({ default: false }) - nsfw?: boolean; - - @Column({ nullable: true, type: "jsonb" }) - integration_types?: ApplicationIntegrationType[]; - - @Column({ default: 0 }) - global_popularity_rank?: number; - - @Column({ nullable: true, type: "jsonb" }) - contexts?: InteractionContextType[]; - - @Column({ default: 0 }) - version: Snowflake; - - @Column({ default: 0 }) - handler?: ApplicationCommandHandlerType; -} diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts deleted file mode 100644 index ff9643fc..00000000 --- a/src/util/entities/Attachment.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { Config, deleteFile } from "../util"; -import { BaseClass } from "./BaseClass"; -import { getUrlSignature, NewUrlUserSignatureData, NewUrlSignatureData } from "../Signing"; -import { PublicAttachment } from "../../schemas/api/messages/Attachments"; - -@Entity({ - name: "attachments", -}) -export class Attachment extends BaseClass { - @Column() - filename: string; // name of file attached - - @Column() - size: number; // size of file in bytes - - @Column({ nullable: true }) - height?: number; // height of file (if image) - - @Column({ nullable: true }) - width?: number; // width of file (if image) - - @Column({ nullable: true }) - content_type?: string; - - @Column({ nullable: true }) - @RelationId((attachment: Attachment) => attachment.message) - message_id: string; - - @Column({ nullable: true }) - @RelationId((attachment: Attachment) => attachment.channel) - channel_id: string; - - @JoinColumn({ name: "message_id" }) - @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, { - onDelete: "CASCADE", - }) - message: import("./Message").Message; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => require("./Channel").Channel, { - onDelete: "CASCADE", - }) - channel: import("./Channel").Channel; - - @BeforeRemove() - onDelete() { - return deleteFile(new URL(this.toJSON().url).pathname); - } - - toJSON() { - const channelId = this.channel_id ?? this.channel?.id ?? this.message?.channel_id; - const messageId = this.message_id ?? this.message?.id; - return { - ...this, - url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`, - proxy_url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`, - }; - } - signUrls(data: NewUrlUserSignatureData): PublicAttachment { - const att = Attachment.prototype.toJSON.apply(this); - return { - ...att, - url: getUrlSignature(new NewUrlSignatureData({ ...data, url: att.url })) - .applyToUrl(att.url) - .toString(), - proxy_url: att.proxy_url - ? getUrlSignature(new NewUrlSignatureData({ ...data, url: att.proxy_url })) - .applyToUrl(att.proxy_url) - .toString() - : att.proxy_url, - }; - } -} diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts deleted file mode 100644 index f47f6dc7..00000000 --- a/src/util/entities/AuditLog.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { AuditLogChange, AuditLogEvents } from "@spacebar/schemas"; - -@Entity({ - name: "audit_logs", -}) -export class AuditLog extends BaseClass { - @JoinColumn({ name: "target_id" }) - @ManyToOne(() => User) - target?: User; - - @Column({ nullable: true }) - @RelationId((auditlog: AuditLog) => auditlog.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) - user: User; - - @Column({ type: "int" }) - action_type: AuditLogEvents; - - @Column({ type: "jsonb", nullable: true }) - options?: { - delete_member_days?: string; - members_removed?: string; - channel_id?: string; - messaged_id?: string; - count?: string; - id?: string; - type?: string; - role_name?: string; - }; - - @Column() - @Column({ type: "jsonb" }) - changes: AuditLogChange[]; - - @Column({ nullable: true }) - reason?: string; -} diff --git a/src/util/entities/AutomodRule.ts b/src/util/entities/AutomodRule.ts deleted file mode 100644 index 97312984..00000000 --- a/src/util/entities/AutomodRule.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2024 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 . -*/ - -import { BaseClass } from "./BaseClass"; -import { Entity, JoinColumn, ManyToOne, Column } from "typeorm"; -import { User } from "./User"; -import { AutomodAction, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas"; - -@Entity({ - name: "automod_rules", -}) -export class AutomodRule extends BaseClass { - @JoinColumn({ name: "creator_id" }) - @ManyToOne(() => User, { onDelete: "CASCADE" }) - creator: User; - - @Column() - enabled: boolean; - - @Column() - event_type: AutomodRuleEventType; - - @Column({ type: "int8", array: true }) - exempt_channels: string[]; - - @Column({ type: "int8", array: true }) - exempt_roles: string[]; - - @Column() - guild_id: string; - - @Column() - name: string; - - @Column() - position: number; - - @Column() - trigger_type: AutomodRuleTriggerType; - - @Column({ - type: "jsonb", - nullable: true, - }) - trigger_metadata?: // this is null for "Block suspected spam content" - AutomodRuleTriggerMetadata; - - @Column({ - type: "jsonb", - }) - actions: AutomodAction[]; -} diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts deleted file mode 100644 index 6759a9a3..00000000 --- a/src/util/entities/BackupCodes.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import crypto from "node:crypto"; -import { Config } from "../util"; - -@Entity({ - name: "backup_codes", -}) -export class BackupCode extends BaseClass { - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; - - @Column() - code: string; - - @Column() - consumed: boolean; - - @Column() - expired: boolean; -} - -export function generateMfaBackupCodes(user_id: string) { - const backup_codes: BackupCode[] = []; - for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) { - const code = BackupCode.create({ - user: { id: user_id }, - code: crypto.randomBytes(4).toString("hex"), // 8 characters - consumed: false, - expired: false, - }); - backup_codes.push(code); - } - - return backup_codes; -} diff --git a/src/util/entities/Badge.ts b/src/util/entities/Badge.ts deleted file mode 100644 index 924ef79e..00000000 --- a/src/util/entities/Badge.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; - -@Entity({ - name: "badges", -}) -export class Badge extends BaseClassWithoutId { - @Column({ primary: true }) - id: string; - - @Column() - description: string; - - @Column() - icon: string; - - @Column({ nullable: true }) - link?: string; -} diff --git a/src/util/entities/Ban.ts b/src/util/entities/Ban.ts deleted file mode 100644 index 17121ace..00000000 --- a/src/util/entities/Ban.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { User } from "./User"; - -@Entity({ - name: "bans", -}) -export class Ban extends BaseClass { - @Column({ nullable: true }) - @RelationId((ban: Ban) => ban.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ nullable: true }) - @RelationId((ban: Ban) => ban.guild) - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - @RelationId((ban: Ban) => ban.executor) - executor_id: string; - - @JoinColumn({ name: "executor_id" }) - @ManyToOne(() => User) - executor: User; - - @Column({ nullable: true }) - ip?: string; - - @Column({ nullable: true }) - reason?: string; -} diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts deleted file mode 100644 index a64138ad..00000000 --- a/src/util/entities/BaseClass.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, PrimaryColumn } from "typeorm"; -import { getDatabase } from "@spacebar/database"; -import { Snowflake } from "../util"; -import { OrmUtils } from "../imports"; -import { annotationsKey } from "../util/Decorators"; - -export class BaseClassWithoutId extends BaseEntity { - private get construct() { - return this.constructor; - } - - // stores custom annotations we may stick on the properties - [annotationsKey]: { [p: string]: string[] }; - - // retrieves the custom annotations as its not super straight forward - get_annotations() { - return Object.getPrototypeOf(this)[annotationsKey]; - } - - // Loops through all the keys and compares it to annotations. If the RemoveEmpty is there it sets the value to undefined if null - clean_data() { - const annotations = this.get_annotations(); - if (annotations == undefined || annotations.length > 0) - //prevent errors if there are no annotations on an object - return; - for (const key in this) { - if ( - key in this && // This object has this property, should never fail but better to be safe - key in annotations && // If this property has an annotation - annotations[key].indexOf("JsonRemoveEmpty") > -1 && // if one of the annotations is JsonRemoveEmpty - (this[key] == null || // If this property is null - (typeof this[key] == "object" && Object.keys(this[key]).length == 0)) - ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - this[key] = undefined; // set to undefined to remove - } - if ( - key in this && // This object has this property, should never fail but better to be safe - key in annotations && // If this property has an annotation - annotations[key].indexOf("JsonNumber") > -1 && // if one of the annotations is JsonRemoveEmpty - typeof this[key] == "string" // and its a String - ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - this[key] = Number(this[key]); // convert string back to number - } - } - return this; - } - - private get metadata() { - return getDatabase()?.getMetadata(this.construct); - } - - assign(props: object) { - OrmUtils.mergeDeep(this, props); - return this; - } - - // TODO: fix eslint - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { - this.clean_data(); - return Object.fromEntries( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - this.metadata!.columns // @ts-ignore - .map((x) => [x.propertyName, this[x.propertyName]]) - .concat( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.metadata.relations.map((x) => [ - x.propertyName, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this[x.propertyName], - ]), - ), - ); - } - - static increment(conditions: FindOptionsWhere, propertyPath: string, value: number | string) { - const repository = this.getRepository(); - return repository.increment(conditions, propertyPath, value); - } - - static decrement(conditions: FindOptionsWhere, propertyPath: string, value: number | string) { - const repository = this.getRepository(); - return repository.decrement(conditions, propertyPath, value); - } - - public async insert(): Promise { - await getDatabase()!.getRepository(this.construct).insert(this); - return this; - } -} - -export class BaseClass extends BaseClassWithoutId { - @PrimaryColumn({ type: "int8" }) - id: string = Snowflake.generate(); - - @BeforeUpdate() - @BeforeInsert() - _do_validate() { - if (!this.id) this.id = Snowflake.generate(); - } -} diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts deleted file mode 100644 index 8c7ca78c..00000000 --- a/src/util/entities/Categories.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, PrimaryColumn } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; - -// TODO: categories: -// [{ -// "id": 16, -// "default": "Anime & Manga", -// "localizations": { -// "de": "Anime & Manga", -// "fr": "Anim\u00e9s et mangas", -// "ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430" -// } -// }, -// "is_primary": false/true -// }] -// Also populate discord default categories - -@Entity({ - name: "categories", -}) -export class Categories extends BaseClassWithoutId { - // Not using snowflake - - @PrimaryColumn() - id: number; - - @Column({ nullable: true }) - name: string; - - @Column({ type: "jsonb" }) - localizations: string; - - // Whether to show the category prominently (e.g. in a sidebar) instead of only secondary (e.g. in search results) - @Column({ nullable: true }) - is_primary: boolean; - - @Column({ nullable: true }) - icon?: string; -} diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts deleted file mode 100644 index e3c1c979..00000000 --- a/src/util/entities/Channel.ts +++ /dev/null @@ -1,736 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { HTTPError } from "lambert-server/HTTPError"; -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { DmChannelDTO } from "../dtos"; -import { ChannelCreateEvent, ChannelRecipientRemoveEvent, ThreadCreateEvent, ThreadMembersUpdateEvent } from "../interfaces"; -import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, Config, DiscordApiErrors } from "../util"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { Invite } from "./Invite"; -import { Message } from "./Message"; -import { Tag } from "./Tag"; -import { ReadState } from "./ReadState"; -import { Recipient } from "./Recipient"; -import { User } from "./User"; -import { VoiceState } from "./VoiceState"; -import { Webhook } from "./Webhook"; -import { Member } from "./Member"; -import { ChannelPermissionOverwrite, ChannelType, PublicChannel, PublicUserProjection, ThreadMetadata } from "@spacebar/schemas"; -import { OrmUtils } from "../imports"; -import { ThreadMember } from "./ThreadMember"; - -@Entity({ - name: "channels", -}) -export class Channel extends BaseClass { - @Column() - created_at: Date; - - @Column({ nullable: true }) - name?: string; - - @Column({ type: "text", nullable: true }) - icon?: string | null; - - @Column({ type: "int" }) - type: ChannelType; - - @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - recipients?: Recipient[]; - - @OneToMany(() => ThreadMember, (member: ThreadMember) => member.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - thread_members?: ThreadMember[]; - - @Column({ nullable: true }) - last_message_id?: string; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.guild) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.channels, { - onDelete: "CASCADE", - nullable: true, - }) - guild?: Guild; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.parent) - parent_id: string | null; - - @JoinColumn({ name: "parent_id" }) - @ManyToOne(() => Channel) - parent?: Channel; - - // for group DMs and owned custom channel types - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.owner) - owner_id?: string; - - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User) - owner: User; - - @Column({ nullable: true, type: "timestamp with time zone" }) - last_pin_timestamp?: Date | null; // ISO8601 - - @Column({ nullable: true }) - default_auto_archive_duration?: number; - - @Column({ type: "jsonb", nullable: true }) - permission_overwrites?: ChannelPermissionOverwrite[]; - - @Column({ nullable: true }) - video_quality_mode?: number; - - @Column({ nullable: true }) - bitrate?: number; - - @Column({ nullable: true }) - user_limit?: number; - - @Column() - nsfw: boolean = false; - - @Column({ nullable: true }) - rate_limit_per_user?: number; - - @Column({ nullable: true }) - topic?: string; - - @OneToMany(() => Invite, (invite: Invite) => invite.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - invites?: Invite[]; - - @Column({ nullable: true }) - retention_policy_id?: string; - - @OneToMany(() => Message, (message: Message) => message.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - messages?: Message[]; - - @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - voice_states?: VoiceState[]; - - @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - read_states?: ReadState[]; - - @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - webhooks?: Webhook[]; - - @Column() - flags: number = 0; - - @Column({ nullable: true }) - default_thread_rate_limit_per_user?: number = 0; - - @Column({ type: "jsonb", nullable: true }) - thread_metadata?: ThreadMetadata; - - @Column({ nullable: true }) - member_count?: number; - - @Column({ nullable: true }) - message_count?: number; - - @Column({ nullable: true }) - total_message_sent?: number; - - @JoinColumn({ name: "available_tags_ids" }) - @OneToMany(() => Tag, (tag: Tag) => tag.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - available_tags?: Tag[]; - - @Column("text", { array: true, nullable: true }) - applied_tags?: string[]; - - @Column("text", { nullable: true }) - status?: string | null; - - /** Must be calculated Channel.calculatePosition */ - position: number; - - // TODO: DM channel - static async createChannel( - channel: Partial, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - skipPermissionCheck?: boolean; - skipEventEmit?: boolean; - skipNameChecks?: boolean; - }, - ): Promise { - if (!opts?.skipPermissionCheck) { - // Always check if user has permission first - const permissions = await getPermission(user_id, channel.guild_id); - permissions.hasThrow("MANAGE_CHANNELS"); - } - - const guild = await Guild.findOneOrFail({ - where: { id: channel.guild_id }, - select: { - features: !opts?.skipNameChecks, - channel_ordering: true, - id: true, - }, - }); - - if (!opts?.skipNameChecks) { - if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { - for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403); - - // Categories skip these checks on discord.com - if ( - (channel.type !== ChannelType.GUILD_CATEGORY && channel.type !== ChannelType.GUILD_STAGE_VOICE && channel.type !== ChannelType.GUILD_VOICE) || - guild.features.includes("IRC_LIKE_CATEGORY_NAMES") - ) { - if (channel.name.includes(" ")) throw new HTTPError("Channel name cannot include invalid characters", 403); - - if (channel.name.match(/--+/g)) throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403); - - if (channel.name.charAt(0) === "-" || channel.name.charAt(channel.name.length - 1) === "-") - throw new HTTPError("Channel name cannot start/end with dash.", 403); - } else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com - } - - if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { - if (!channel.name) throw new HTTPError("Channel name cannot be empty.", 403); - } - } - - switch (channel.type) { - // TODO: should threads even be routed through this function instead of createThreadChannel? - case ChannelType.GUILD_PUBLIC_THREAD: - case ChannelType.GUILD_PRIVATE_THREAD: - case ChannelType.GUILD_NEWS_THREAD: - case ChannelType.GUILD_TEXT: - case ChannelType.GUILD_FORUM: - case ChannelType.GUILD_MEDIA: - case ChannelType.GUILD_NEWS: - case ChannelType.GUILD_VOICE: - if (channel.parent_id && !opts?.skipExistsCheck) { - const exists = await Channel.findOneOrFail({ - where: { id: channel.parent_id }, - }); - if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); - if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild"); - } - break; - case ChannelType.GUILD_CATEGORY: - case ChannelType.UNHANDLED: - break; - case ChannelType.DM: - case ChannelType.GROUP_DM: - throw new HTTPError("You can't create a dm channel in a guild"); - case ChannelType.GUILD_STORE: - default: - throw new HTTPError("Not yet supported"); - } - - if (!channel.permission_overwrites) channel.permission_overwrites = []; - // TODO: eagerly auto generate position of all guild channels - - const position = (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0; - - channel = { - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - position, - // from #876 (threads): shouldnt these be undefined? - // message_count: 0, - // member_count: 0, - // total_message_sent: 0, - }; - - // TODO: figure out why the generic is required here - const ret = Channel.create(channel); - - await Promise.all([ - ret.save(), - !opts?.skipEventEmit - ? emitEvent({ - event: "CHANNEL_CREATE", - data: ret.toJSON(), - guild_id: channel.guild_id, - } satisfies ChannelCreateEvent) - : Promise.resolve(), - Guild.insertChannelInOrder(guild.id, ret.id, position, guild), - ]); - - return ret; - } - threadOnly() { - return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA; - } - - static async createThreadChannel( - channel: Partial, - metadata: Partial, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - skipParentExistsCheck?: boolean; - skipPermissionCheck?: boolean; - skipEventEmit?: boolean; - skipNameChecks?: boolean; - }, - ): Promise { - channel = { - // set the default type to private - type: ChannelType.GUILD_PRIVATE_THREAD, - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - position: 0, // TODO: - message_count: 0, - member_count: 1, - total_message_sent: 0, - }; - - const exists = await Channel.findOne({ - where: { - id: channel.id, - }, - }); - - const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); - - if (!opts?.skipExistsCheck && !guild.features.includes("ALLOW_EXISTING_THREAD_FOR_MESSAGE") && exists) throw DiscordApiErrors.THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE; - - if (!channel.parent_id) throw new HTTPError("Parent id not set", 400); - const parent = await Channel.findOneOrFail({ where: { id: channel.parent_id } }); - - if (!opts?.skipPermissionCheck) { - // Always check if user has permission first - const permissions = await getPermission(user_id, parent.guild_id); - permissions.hasThrow(channel.type === ChannelType.GUILD_PRIVATE_THREAD ? "CREATE_PRIVATE_THREADS" : "CREATE_PUBLIC_THREADS"); - } - - channel = { - ...channel, - permission_overwrites: parent.permission_overwrites, - nsfw: parent.nsfw, - owner_id: user_id, - guild_id: parent.guild_id, - thread_metadata: { - create_timestamp: new Date().toISOString(), - archive_timestamp: new Date().toISOString(), - archived: false, - auto_archive_duration: 0, - invitable: channel.type === ChannelType.GUILD_NEWS_THREAD || channel.type === ChannelType.GUILD_PUBLIC_THREAD ? Config.get().guild.publicThreadsInvitable : false, - locked: false, - ...metadata, - }, - }; - - if (!opts?.skipParentExistsCheck) { - if (!parent) throw new HTTPError("Parent channel doesn't exist", 400); - if (parent.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild"); - } - - if (!opts?.skipNameChecks) { - const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); - if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { - for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403); - - channel.name = channel.name.trim(); //category names are trimmed client side on discord.com - } - - if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { - if (!channel.name) throw new HTTPError("Channel name cannot be empty.", 403); - } - } - - // TODO: eagerly auto generate position of all guild channels - - const thread = await OrmUtils.mergeDeep(new Channel(), channel).save(); - - const member = { - id: thread.id, - user_id, - join_timestamp: new Date(), - muted: false, - mute_config: null, - flags: 0, - }; - if (channel.member_count) channel.member_count++; - - const threadMember = await OrmUtils.mergeDeep(new ThreadMember(), member).save(); - - if (!opts?.skipEventEmit) { - await Promise.all([ - emitEvent({ - event: "THREAD_CREATE", - data: { - ...thread, - newly_created: true, - }, - guild_id: channel.guild_id, - } satisfies ThreadCreateEvent), - emitEvent({ - event: "THREAD_MEMBERS_UPDATE", - data: { - guild_id: channel.guild_id!, // TODO: is this the right fix? - id: thread.id, - member_count: channel.member_count ?? 0, //TODO: is this the right fix? - added_members: [threadMember], - removed_member_ids: [], - }, - guild_id: channel.guild_id, - } satisfies ThreadMembersUpdateEvent), - ]); - } - - return thread; - } - - static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { - recipients = [...new Set(recipients)].filter((x) => x !== creator_user_id); - // TODO: check config for max number of recipients - /** if you want to disallow note to self channels, uncomment the conditional below - - const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); - if (otherRecipientsUsers.length !== recipients.length) { - throw new HTTPError("Recipient/s not found"); - } - **/ - - const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; - - let channel = null; - let needsTx = true; - - const channelRecipients = [...recipients, creator_user_id]; - - const userRecipients = await Recipient.find({ - where: { user_id: creator_user_id }, - relations: { channel: { recipients: true } }, - }); - - for (const ur of userRecipients) { - if (!ur.channel.recipients) continue; - const re = ur.channel.recipients.map((r) => r.user_id); - if (re.length === channelRecipients.length) { - if (channelRecipients.every((_) => re.includes(_))) { - if (channel == null) { - channel = ur.channel; - if (!ur.closed) needsTx = false; - await ur.assign({ closed: false }).save(); - } - } - } - } - - if (channel == null) { - name = trimSpecial(name); - - channel = await Channel.create({ - name, - type, - owner_id: type === ChannelType.GROUP_DM ? creator_user_id : undefined, - created_at: new Date(), - last_message_id: undefined, - recipients: channelRecipients.map((x) => - Recipient.create({ - user_id: x, - closed: !(type === ChannelType.GROUP_DM || x === creator_user_id), - }), - ), - nsfw: false, - }).save(); - } - - const channel_dto = await DmChannelDTO.from(channel); - - if (!needsTx) { - /*ignored*/ - } else if (type === ChannelType.GROUP_DM && channel.recipients) { - for (const recipient of channel.recipients) { - await emitEvent({ - event: "CHANNEL_CREATE", - data: channel_dto.excludedRecipients([recipient.user_id]), - user_id: recipient.user_id, - }); - } - } else { - await emitEvent({ - event: "CHANNEL_CREATE", - data: channel_dto, - user_id: creator_user_id, - }); - } - - if (recipients.length === 1) return channel_dto; - else return channel_dto.excludedRecipients([creator_user_id]); - } - - static async removeRecipientFromChannel(channel: Channel, user_id: string) { - await Recipient.delete({ channel_id: channel.id, user_id: user_id }); - channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); - - if (channel.recipients?.length === 0) { - await Channel.deleteChannel(channel); - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - return; - } - - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - - //If the owner leave the server user is the new owner - if (channel.owner_id === user_id) { - channel.owner_id = "1"; // The channel is now owned by the server user - await emitEvent({ - event: "CHANNEL_UPDATE", - data: await DmChannelDTO.from(channel, [user_id]), - channel_id: channel.id, - }); - } - - await channel.save(); - - await emitEvent({ - event: "CHANNEL_RECIPIENT_REMOVE", - data: { - channel_id: channel.id, - user: await User.findOneOrFail({ - where: { id: user_id }, - select: PublicUserProjection, - }), - }, - channel_id: channel.id, - } satisfies ChannelRecipientRemoveEvent); - } - - 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 }); - } - } - - static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) { - if (!guild) - guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: { channel_ordering: true }, - }); - - return guild.channel_ordering.findIndex((id) => channel_id == id); - } - - static async getOrderedChannels(guild_id: string, guild?: Guild) { - if (!guild) - guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: { channel_ordering: true }, - }); - - const channels = await Promise.all(guild.channel_ordering.map((id) => Channel.findOne({ where: { id } }))); - - return channels - .filter((channel) => channel !== null) - .reduce((r, v) => { - v = v as Channel; - - v.position = (guild as Guild).channel_ordering.indexOf(v.id); - r[v.position] = v; - return r; - }, [] as Array); - } - - isDm() { - return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; - } - - isThread() { - return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD || this.type === ChannelType.GUILD_PRIVATE_THREAD; - } - isForum() { - return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA; - } - - isPrivateThread() { - return this.type === ChannelType.GUILD_PRIVATE_THREAD; - } - - isPublicThread() { - return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD; - } - - // Does the channel support sending messages ( eg categories do not ) - isWritable() { - const disallowedChannelTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STAGE_VOICE]; - return disallowedChannelTypes.indexOf(this.type) == -1; - } - - async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise { - if (this.isDm()) return this.owner_id == (opts.user_id ?? opts.user?.id) ? Permissions.ALL : Permissions.DEFAULT_DM_PERMISSIONS; - let guild = opts.guild; - if (!guild) { - if (this.guild) guild = this.guild; - else if (this.guild_id) guild = await Guild.findOneOrFail({ where: { id: this.guild_id } }); - else { - console.error("Channel.getUserPermissions: called without guild for non-DM channel."); - return Permissions.NONE; - } - } - - // check if we can resolve here to short-circuit possibly calling the database unnecessarily - // TODO: do we want to have an instance-wide opt out of this behavior? It would just be an extra if statement here - const ownerId = guild?.owner?.id ?? guild?.owner_id; - if (!!opts.user_id && ownerId === opts.user_id) return Permissions.ALL; - if (!!opts.user?.id && ownerId === opts.user?.id) return Permissions.ALL; - if (!!opts.member?.id && ownerId === opts.member?.id) return Permissions.ALL; - - let member = opts.member; - if (!member) { - if (opts.user) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user.id }, relations: { roles: true } }); - else if (opts.user_id) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user_id }, relations: { roles: true } }); - else { - console.error("Channel.getUserPermissions: called without user or member for non-DM channel."); - return Permissions.NONE; - } - } - - const roles = ( - member.roles || - ( - await Member.findOneOrFail({ - where: { guild_id: guild.id, index: member.index }, - relations: { roles: true }, - select: { - roles: { - id: true, - permissions: true, - position: true, - }, - }, - loadEagerRelations: false, - }) - ).roles - ).sort((a, b) => a.position - b.position); // ascending by position - - return Permissions.finalPermission({ - user: { - ...member, - roles: roles.map((r) => r.id), - flags: member.user?.flags ?? (await User.findOneOrFail({ where: { id: member.id }, select: { flags: true } })).flags, - }, - guild: { id: guild.id, owner_id: guild.owner_id!, roles }, // We don't care about including *all* guild roles, as not all of them are relevant... - channel: this, - }); - } - - // TODO: should we throw for missing args? - async canViewChannel(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise { - if (this.isDm()) return await this.canViewDmChannel(opts.user_id, opts.user); - - const userPerms = await this.getUserPermissions(opts); - return userPerms.has("VIEW_CHANNEL"); - } - - private async canViewDmChannel(user_id?: string, user?: User): Promise { - const userId = user_id ?? user?.id; - if (!userId) { - console.error("Channel.canViewChannel: called without user for DM channel."); - return false; - } - if (!user) return false; - if (this.recipients) return this.recipients.some((r) => r.user_id === user.id && !r.closed); - else { - // we dont have recipients on hand - const recipient = await Recipient.findOne({ where: { channel_id: this.id, user_id: user.id } }); - return recipient == null ? false : !recipient.closed; - } - } - - toJSON(): PublicChannel { - return { - ...this, - last_pin_timestamp: this.last_pin_timestamp?.toISOString(), - guild_id: this.guild_id ?? undefined, - recipients: undefined, //this.recipients?.map(x=>x.user.toPublicUser()), // TODO: fix me - owner: undefined, // TODO: fix me - this is thread owner - - // these fields are not returned depending on the type of channel - bitrate: this.bitrate || undefined, - user_limit: this.user_limit || undefined, - rate_limit_per_user: this.rate_limit_per_user || undefined, - owner_id: this.owner_id || undefined, - ...(this.isThread() && this.thread_members ? { member_ids_preview: this.thread_members.map((_) => _.member.id) } : {}), - default_auto_archive_duration: this.default_auto_archive_duration ?? undefined, - retention_policy_id: undefined, - thread_metadata: this.thread_metadata - ? { - ...this.thread_metadata, - archive_timestamp: new Date(this.thread_metadata.archive_timestamp).toISOString().replace("Z", "+00:00"), - create_timestamp: new Date(this.thread_metadata.create_timestamp).toISOString().replace("Z", "+00:00"), - } - : undefined, - member_count: this.member_count ?? undefined, - message_count: this.message_count ?? undefined, - total_message_sent: this.total_message_sent ?? undefined, - applied_tags: this.applied_tags ?? undefined, - permission_overwrites: this.isThread() ? undefined : this.permission_overwrites, - }; - } -} diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts deleted file mode 100644 index fc8fe86c..00000000 --- a/src/util/entities/ClientRelease.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity } from "typeorm"; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "client_release", -}) -export class ClientRelease extends BaseClass { - @Column() - name: string; - - @Column() - pub_date: Date; - - @Column() - url: string; - - @Column() - platform: string; - - @Column() - enabled: boolean; - - @Column({ nullable: true }) - notes?: string; -} diff --git a/src/util/entities/CloudAttachment.ts b/src/util/entities/CloudAttachment.ts deleted file mode 100644 index 408df024..00000000 --- a/src/util/entities/CloudAttachment.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { Channel } from "./Channel"; - -@Entity({ - name: "cloud_attachments", -}) -export class CloudAttachment extends BaseClass { - // Internal tracking metadata - @Column({ name: "user_id", nullable: true }) - @RelationId((att: CloudAttachment) => att.user) - userId: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" }) - user?: User; - - @Column({ name: "channel_id", nullable: true }) - @RelationId((att: CloudAttachment) => att.channel) - channelId?: string; // channel the file is uploaded to - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { nullable: true, onDelete: "SET NULL" }) - channel?: Channel; // channel the file is uploaded to - - @Column({ name: "upload_filename" }) - uploadFilename: string; - - // User-provided info - @Column({ name: "user_attachment_id", nullable: true }) - userAttachmentId?: string; - - @Column({ name: "user_filename" }) - userFilename: string; // name of file attached - - @Column({ name: "user_file_size", nullable: true }) - userFileSize?: number; // size of file in bytes - - @Column({ name: "user_original_content_type", nullable: true }) - userOriginalContentType?: string; - - @Column({ name: "user_is_clip", nullable: true }) - userIsClip?: boolean; // whether the file is a clip - - // Actual file info, initialised after upload - @Column({ nullable: true }) - size?: number; // size of file in bytes - - @Column({ nullable: true }) - height?: number; // height of file (if image) - - @Column({ nullable: true }) - width?: number; // width of file (if image) - - @Column({ name: "content_type", nullable: true }) - contentType?: string; - - // @BeforeRemove() - // onDelete() { - // return deleteFile(new URL(this.url).pathname); - // } -} diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts deleted file mode 100644 index 7d4e05af..00000000 --- a/src/util/entities/Config.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, PrimaryColumn } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; - -@Entity({ - name: "config", -}) -export class ConfigEntity extends BaseClassWithoutId { - @PrimaryColumn() - key: string; - - @Column({ type: "simple-json", nullable: true }) - value: number | boolean | null | string | undefined; -} diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts deleted file mode 100644 index d0ed8097..00000000 --- a/src/util/entities/ConnectedAccount.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { ConnectedAccountTokenData } from "@spacebar/schemas"; - -@Entity({ - name: "connected_accounts", -}) -export class ConnectedAccount extends BaseClass { - @Column() - external_id: string; - - @Column({ nullable: true }) - @RelationId((account: ConnectedAccount) => account.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ select: false }) - friend_sync?: boolean = false; - - @Column() - name: string; - - @Column({ select: false }) - revoked?: boolean = false; - - @Column({ select: false }) - show_activity?: number = 0; - - @Column() - type: string; - - @Column() - verified?: boolean = true; - - @Column({ select: false }) - visibility?: number = 0; - - @Column({ type: "varchar", array: true }) - integrations?: string[] = []; - - @Column({ type: "jsonb", name: "metadata", nullable: true }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - metadata_?: any; - - @Column() - metadata_visibility?: number = 0; - - @Column() - two_way_link?: boolean = false; - - @Column({ select: false, nullable: true, type: "jsonb" }) - token_data?: ConnectedAccountTokenData | null; - - async revoke() { - this.revoked = true; - this.token_data = null; - await this.save(); - } -} diff --git a/src/util/entities/ConnectionConfigEntity.ts b/src/util/entities/ConnectionConfigEntity.ts deleted file mode 100644 index 9ad2af8b..00000000 --- a/src/util/entities/ConnectionConfigEntity.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, PrimaryColumn } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; - -@Entity({ - name: "connection_config", -}) -export class ConnectionConfigEntity extends BaseClassWithoutId { - @PrimaryColumn() - key: string; - - @Column({ type: "simple-json", nullable: true }) - value: number | boolean | null | string | Date | undefined; -} diff --git a/src/util/entities/EmbedCache.ts b/src/util/entities/EmbedCache.ts deleted file mode 100644 index b34be18c..00000000 --- a/src/util/entities/EmbedCache.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { BaseClass } from "./BaseClass"; -import { Entity, Column } from "typeorm"; -import { Embed } from "@spacebar/schemas"; - -@Entity({ - name: "embed_cache", -}) -export class EmbedCache extends BaseClass { - @Column() - url: string; - - @Column({ type: "jsonb", nullable: true }) - embed?: Embed; - - @Column({ type: "jsonb", nullable: true }) - embeds?: Embed[]; - - @Column({ name: "created_at", type: "timestamp with time zone" }) - createdAt: Date; -} diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts deleted file mode 100644 index 586550f9..00000000 --- a/src/util/entities/Emoji.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { User } from "."; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; - -@Entity({ - name: "emojis", -}) -export class Emoji extends BaseClass { - @Column() - animated: boolean; - - @Column() - available: boolean; // whether this emoji can be used, may be false due to various reasons - - @Column() - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.emojis, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - @RelationId((emoji: Emoji) => emoji.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User) - user: User; - - @Column() - managed: boolean; - - @Column() - name: string; - - @Column() - require_colons: boolean; - - @Column({ type: "int8", array: true }) - roles: string[]; // roles this emoji is whitelisted to (new discord feature?) - - @Column({ type: "int8", array: true, nullable: true }) - groups: string[]; // user groups this emoji is whitelisted to (Spacebar extension) -} diff --git a/src/util/entities/Encryption.ts b/src/util/entities/Encryption.ts deleted file mode 100644 index 9c76701a..00000000 --- a/src/util/entities/Encryption.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity } from "typeorm"; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "security_settings", -}) -export class SecuritySettings extends BaseClass { - @Column({ nullable: true }) - guild_id: string; - - @Column({ nullable: true }) - channel_id: string; - - @Column() - encryption_permission_mask: number; - - @Column({ type: "varchar", array: true }) - allowed_algorithms: string[]; - - @Column() - current_algorithm: string; - - @Column({ nullable: true }) - used_since_message: string; -} diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts deleted file mode 100644 index 0cea637e..00000000 --- a/src/util/entities/Guild.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { arrayRemove } from "@spacebar/extensions"; -import { Config, GuildWelcomeScreen, Snowflake, handleFile } from ".."; -import { Ban } from "./Ban"; -import { BaseClass } from "./BaseClass"; -import { Channel } from "./Channel"; -import { Emoji } from "./Emoji"; -import { Invite } from "./Invite"; -import { Member } from "./Member"; -import { Role } from "./Role"; -import { Sticker } from "./Sticker"; -import { Template } from "./Template"; -import { User } from "./User"; -import { VoiceState } from "./VoiceState"; -import { Webhook } from "./Webhook"; -// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0} -// TODO: guild_scheduled_events -// TODO: stage_instances -// TODO: threads -// TODO: -// "keywords": [ -// "Genshin Impact", -// "Paimon", -// "Honkai Impact", -// "ARPG", -// "Open-World", -// "Waifu", -// "Anime", -// "Genshin", -// "miHoYo", -// "Gacha" -// ], - -export const PublicGuildRelations = [ - "channels", - "emojis", - "roles", - "stickers", - "voice_states", - // "members", // TODO: These are public, but all members should not be fetched. - // "members.user", -]; - -@Entity({ - name: "guilds", -}) -export class Guild extends BaseClass { - @Column({ type: String, nullable: true }) - @RelationId((guild: Guild) => guild.afk_channel) - afk_channel_id?: string | null; - - @JoinColumn({ name: "afk_channel_id" }) - @ManyToOne(() => Channel) - afk_channel?: Channel; - - @Column({ nullable: true }) - afk_timeout?: number; - - // * commented out -> use owner instead - // application id of the guild creator if it is bot-created - // @Column({ nullable: true }) - // application?: string; - - @JoinColumn({ name: "ban_ids" }) - @OneToMany(() => Ban, (ban: Ban) => ban.guild, { - cascade: true, - orphanedRowAction: "delete", - }) - bans: Ban[]; - - @Column({ nullable: true }) - banner?: string; - - @Column({ nullable: true }) - default_message_notifications?: number; - - @Column({ nullable: true }) - description?: string; - - @Column({ nullable: true }) - discovery_splash?: string; - - @Column({ nullable: true }) - explicit_content_filter?: number; - - @Column({ type: "varchar", array: true }) - features: string[] = []; //TODO use enum - //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features - - @Column({ nullable: true }) - primary_category_id?: string; // TODO: this was number? - - @Column({ nullable: true }) - icon?: string; - - @Column() - large?: boolean = false; - - @Column({ nullable: true }) - max_members?: number; - - @Column({ nullable: true }) - max_presences?: number; - - @Column({ nullable: true }) - max_video_channel_users?: number; - - @Column({ nullable: true }) - member_count?: number; - - @Column({ nullable: true }) - presence_count?: number; // users online - - @OneToMany(() => Member, (member: Member) => member.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - members: Member[]; - - @JoinColumn({ name: "role_ids" }) - @OneToMany(() => Role, (role: Role) => role.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - roles: Role[]; - - @JoinColumn({ name: "channel_ids" }) - @OneToMany(() => Channel, (channel: Channel) => channel.guild, { - cascade: true, - orphanedRowAction: "delete", - }) - channels: Channel[]; - - @Column({ nullable: true }) - @RelationId((guild: Guild) => guild.template) - template_id?: string; - - @JoinColumn({ name: "template_id", referencedColumnName: "id" }) - @ManyToOne(() => Template) - template: Template; - - @JoinColumn({ name: "emoji_ids" }) - @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - emojis: Emoji[]; - - @JoinColumn({ name: "sticker_ids" }) - @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - stickers: Sticker[]; - - @JoinColumn({ name: "invite_ids" }) - @OneToMany(() => Invite, (invite: Invite) => invite.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - invites: Invite[]; - - @JoinColumn({ name: "voice_state_ids" }) - @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - voice_states: VoiceState[]; - - @JoinColumn({ name: "webhook_ids" }) - @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, { - cascade: true, - orphanedRowAction: "delete", - onDelete: "CASCADE", - }) - webhooks: Webhook[]; - - @Column({ nullable: true }) - mfa_level?: number; - - @Column() - name: string; - - @Column({ nullable: true }) - @RelationId((guild: Guild) => guild.owner) - owner_id?: string; // optional to allow for ownerless guilds - - @JoinColumn({ name: "owner_id", referencedColumnName: "id" }) - @ManyToOne(() => User) - owner?: User; // optional to allow for ownerless guilds - - @Column({ nullable: true }) - preferred_locale?: string; - - @Column({ nullable: true }) - premium_subscription_count?: number; - - @Column() - premium_tier?: number; // crowd premium level - - @Column({ type: String, nullable: true }) - @RelationId((guild: Guild) => guild.public_updates_channel) - public_updates_channel_id: string | null; - - @JoinColumn({ name: "public_updates_channel_id" }) - @ManyToOne(() => Channel) - public_updates_channel?: Channel; - - @Column({ type: String, nullable: true }) - @RelationId((guild: Guild) => guild.rules_channel) - rules_channel_id?: string | null; - - @JoinColumn({ name: "rules_channel_id" }) - @ManyToOne(() => Channel) - rules_channel?: string; - - @Column({ nullable: true }) - region?: string; - - @Column({ nullable: true }) - splash?: string; - - @Column({ type: String, nullable: true }) - @RelationId((guild: Guild) => guild.system_channel) - system_channel_id?: string | null; - - @JoinColumn({ name: "system_channel_id" }) - @ManyToOne(() => Channel) - system_channel?: Channel; - - @Column({ nullable: true }) - system_channel_flags?: number; - - @Column() - unavailable: boolean = false; - - @Column({ nullable: true }) - verification_level?: number; - - /** - * DEPRECATED: Look at the new Guild onboarding screens. - */ - @Column({ type: "jsonb" }) - welcome_screen: GuildWelcomeScreen; - - @Column({ nullable: true, type: "int8" }) - @RelationId((guild: Guild) => guild.widget_channel) - widget_channel_id?: string; - - @JoinColumn({ name: "widget_channel_id" }) - @ManyToOne(() => Channel) - widget_channel?: Channel; - - @Column() - widget_enabled: boolean = true; - - @Column({ nullable: true }) - nsfw_level?: number; - - @Column() - nsfw: boolean = false; - - // TODO: nested guilds - @Column({ nullable: true }) - parent?: string; - - // only for developer portal - permissions?: number; - - //new guild settings, 11/08/2022: - @Column({ nullable: true }) - premium_progress_bar_enabled: boolean = false; - - @Column({ select: false, type: "int8", array: true }) - channel_ordering: string[]; - - @Column() - discovery_weight: number = 0; - - @Column() - discovery_excluded: boolean = false; - - async ToGuildSource() { - if (!this.features.includes("DISCOVERABLE")) { - return null; - } - return { - id: this.id, - name: this.name, - icon: this.icon, - description: this.description, - banner: this.banner, - splash: this.splash, - discovery_splash: this.discovery_splash, - features: this.features, - vanity_url_code: null, - preferred_locale: this.preferred_locale || "en", - premium_subscription_count: this.premium_subscription_count, - approximate_member_count: await Member.countBy({ - guild_id: this.id, - }), - approximate_presence_count: await Member.countBy({ - guild_id: this.id, - user: { - sessions: { - status: "online", - }, - }, - }), - emojis: this.emojis ?? undefined, - emoji_count: this.emojis ? this.emojis.length : undefined, - stickers: this.stickers ?? undefined, - sticker_count: this.stickers ? this.stickers.length : undefined, - auto_removed: false, - primary_category_id: this.primary_category_id, - keywords: [], - is_published: false, - reasons_to_join: [], - }; - } - - static async createGuild(body: { - name?: string; - icon?: string | null; - owner_id?: string; - roles?: Partial[]; - channels?: Partial[]; - source_guild_id: string | null; - }) { - const guild_id = Snowflake.generate(); - - const guild = await Guild.create({ - id: guild_id, - name: body.name || "Spacebar", - icon: await handleFile(`/icons/${guild_id}`, body.icon as string), - owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds - presence_count: 0, - member_count: 0, // will automatically be increased by addMember() - mfa_level: 0, - preferred_locale: "en-US", - premium_subscription_count: 0, - premium_tier: 0, - system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance - nsfw_level: 0, - verification_level: 0, - welcome_screen: { - enabled: false, - description: "", - welcome_channels: [], - }, - channel_ordering: [], - afk_timeout: Config.get().defaults.guild.afkTimeout, - default_message_notifications: Config.get().defaults.guild.defaultMessageNotifications, - explicit_content_filter: Config.get().defaults.guild.explicitContentFilter, - features: Config.get().guild.defaultFeatures, - max_members: Config.get().limits.guild.maxMembers, - max_presences: Config.get().defaults.guild.maxPresences, - max_video_channel_users: Config.get().defaults.guild.maxVideoChannelUsers, - region: Config.get().regions.default, - }).save(); - - // we have to create the role _after_ the guild because else we would get a foreign key error - // TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage - await Role.create({ - id: guild_id, - guild_id: guild_id, - color: 0, - colors: { primary_color: 0 }, - hoist: false, - managed: false, - mentionable: false, - name: "@everyone", - permissions: "2251804225", - position: 0, - icon: undefined, - unicode_emoji: undefined, - flags: 0, // TODO? - }).save(); - - // create custom roles if provided - if (body.roles && body.roles.length) { - await Promise.all( - body.roles?.map( - (role) => - new Promise((resolve) => { - Role.create({ - ...role, - guild_id, - id: - // role.id === body.template_guild_id indicates that this is the @everyone role - role.id === body.source_guild_id || role.id == "0" ? guild_id : Snowflake.generate(), - }) - .save() - .then(resolve); - }), - ), - ); - } - - if (!body.channels || !body.channels.length) { - body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }]; - } - - const ids = new Map(); - - body.channels.forEach((x) => { - if (x.id) { - ids.set(x.id, Snowflake.generate()); - } - }); - - for (const channel of body.channels.sort((a) => (a.parent_id ? 1 : -1))) { - const id = ids.get(channel.id) || Snowflake.generate(); - - const parent_id = ids.get(channel.parent_id); - - const saved = await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, { - keepId: true, - skipExistsCheck: true, - skipPermissionCheck: true, - skipEventEmit: true, - }); - - await Guild.insertChannelInOrder(guild.id, saved.id, parent_id ?? channel.position ?? 0, guild); - } - - return guild; - } - - /** Insert a channel into the guild ordering by parent channel id or position */ - static async insertChannelInOrder(guild_id: string, channel_id: string, position: number, guild?: Guild): Promise; - static async insertChannelInOrder(guild_id: string, channel_id: string, parent_id: string, guild?: Guild): Promise; - static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise; - static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise { - if (!guild) - guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: { channel_ordering: true }, - }); - - guild.channel_ordering ??= []; - - let position; - if (typeof insertPoint == "string") position = guild.channel_ordering.indexOf(insertPoint) + 1; - else position = insertPoint; - - arrayRemove(guild.channel_ordering, channel_id); - - guild.channel_ordering.splice(position, 0, channel_id); - await Guild.update({ id: guild_id }, { channel_ordering: guild.channel_ordering }); - return position; - } - - toJSON(): Guild { - return { - ...this, - unavailable: this.unavailable == false ? undefined : true, - channel_ordering: undefined, - discovery_weight: undefined, - discovery_excluded: undefined, - parent: undefined, - primary_category_id: undefined, - nsfw: undefined, - template_id: undefined, - presence_count: undefined, - }; - } -} diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts deleted file mode 100644 index 03424e5a..00000000 --- a/src/util/entities/InstanceBan.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, CreateDateColumn, Entity, FindOptionsWhere, Index, JoinColumn, OneToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "instance_bans", -}) -export class InstanceBan extends BaseClass { - @Column({ type: "bigint" }) - @CreateDateColumn() - created_at: Date = new Date(); - - @Column() - reason: string; - - @Index() - @Column({ nullable: true }) - user_id?: string; - - @Index() - @Column({ nullable: true }) - fingerprint?: string; - - @Index() - @Column({ nullable: true }) - ip_address?: string; - - // chain of trust type tracking - - @Column({ default: false }) - is_allowlisted: boolean = false; - - @Column({ default: false }) - is_from_other_instance_ban: boolean = false; - - @Column({ nullable: true }) - @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban) - origin_instance_ban_id?: string; - - @JoinColumn({ name: "origin_instance_ban_id" }) - @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" }) - origin_instance_ban?: InstanceBan; - - static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) { - const optionalChecks: FindOptionsWhere[] = [{ user_id: opts.userId }]; - if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress }); - if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint }); - const instanceBans = await InstanceBan.find({ where: optionalChecks }); - - const banReasons = []; - for (const ban of instanceBans) { - if (ban.is_allowlisted) continue; - if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint"); - if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress"); - if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId"); - } - - const banViralityPromises: Promise[] = []; - if (opts.propagateBan && banReasons.length > 0) { - if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress)) - banViralityPromises.push( - InstanceBan.create({ - user_id: opts.userId, - ip_address: opts.ipAddress, - reason: "Propagated from other instance ban", - is_from_other_instance_ban: true, - origin_instance_ban: instanceBans[0], - }).save(), - ); - if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint)) - banViralityPromises.push( - InstanceBan.create({ - user_id: opts.userId, - fingerprint: opts.fingerprint, - reason: "Propagated from other instance ban", - is_from_other_instance_ban: true, - origin_instance_ban: instanceBans[0], - }).save(), - ); - if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId)) - banViralityPromises.push( - InstanceBan.create({ - user_id: opts.userId, - reason: "Propagated from other instance ban", - is_from_other_instance_ban: true, - origin_instance_ban: instanceBans[0], - }).save(), - ); - } - - await Promise.all(banViralityPromises); - return banReasons; - } -} diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts deleted file mode 100644 index cc5374eb..00000000 --- a/src/util/entities/Invite.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; -import { Channel } from "./Channel"; -import { Guild } from "./Guild"; -import { Member } from "./Member"; -import { User } from "./User"; - -export const PublicInviteRelation = ["inviter", "guild", "channel"]; - -@Entity({ - name: "invites", -}) -export class Invite extends BaseClassWithoutId { - @PrimaryColumn() - code: string; - - @Column() - temporary: boolean; - - @Column() - uses: number; - - @Column() - max_uses: number; - - @Column() - max_age: number; - - @Column() - created_at: Date; - - @Column({ nullable: true }) - expires_at?: Date; - - @Column({ nullable: true }) - @RelationId((invite: Invite) => invite.guild) - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.invites, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - @RelationId((invite: Invite) => invite.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column({ nullable: true }) - @RelationId((invite: Invite) => invite.inviter) - inviter_id?: string; - - @JoinColumn({ name: "inviter_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - inviter: User; - - @Column({ nullable: true }) - @RelationId((invite: Invite) => invite.target_user) - target_user_id: string; - - @JoinColumn({ name: "target_user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - target_user?: string; // could be used for "User specific invites" https://github.com/spacebarchat/server/issues/326 - - @Column({ nullable: true }) - target_user_type?: number; - - @Column({ nullable: true }) - vanity_url?: boolean; - - @Column() - flags: number; - - isExpired() { - if (this.max_age !== 0 && this.expires_at && this.expires_at < new Date()) return true; - if (this.max_uses !== 0 && this.uses >= this.max_uses) return true; - return false; - } - toPublicJSON() { - return { - ...this, - inviter: this.inviter.toPublicUser(), - }; - } - - static async joinGuild(user_id: string, code: string) { - const invite = await Invite.findOneOrFail({ where: { code } }); - if (invite.isExpired()) { - await Invite.delete({ code }); - throw new Error("Invite is expired"); - } - if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code }); - else await invite.save(); - - await Member.addToGuild(user_id, invite.guild_id); - return invite; - } -} diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts deleted file mode 100644 index b6b1ca11..00000000 --- a/src/util/entities/Member.ts +++ /dev/null @@ -1,488 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { HTTPError } from "lambert-server/HTTPError"; -import { BeforeInsert, BeforeUpdate, Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, Not, PrimaryGeneratedColumn, RelationId } from "typeorm"; -import { Ban, Channel, PublicGuildRelations } from "."; -import { ReadyGuildDTO } from "../dtos"; -import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } from "../interfaces"; -import { Config, emitEvent, DiscordApiErrors, Stopwatch } from "../util"; -import { BaseClassWithoutId } from "./BaseClass"; -import { Guild } from "./Guild"; -import { Message } from "./Message"; -import { Role } from "./Role"; -import { User } from "./User"; -import { AvatarDecorationData, Collectibles, DisplayNameStyle, PublicMember, PublicMemberProjection, UserGuildSettings } from "@spacebar/schemas"; - -export const MemberPrivateProjection: (keyof Member)[] = [ - "id", - "guild", - "guild_id", - "deaf", - "joined_at", - "last_message_id", - "mute", - "nick", - "pending", - "premium_since", - "roles", - "settings", - "user", - "avatar", - "banner", - "bio", - "theme_colors", - "pronouns", - "communication_disabled_until", - "flags", -]; - -@Entity({ - name: "members", -}) -@Index(["id", "guild_id"], { unique: true }) -export class Member extends BaseClassWithoutId { - @PrimaryGeneratedColumn() - index: string; - - @Column() - @RelationId((member: Member) => member.user) - id: string; - - @JoinColumn({ name: "id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column() - @RelationId((member: Member) => member.guild) - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - nick?: string; - - @JoinTable({ - name: "member_roles", - joinColumn: { name: "index", referencedColumnName: "index" }, - inverseJoinColumn: { - name: "role_id", - referencedColumnName: "id", - }, - }) - @ManyToMany(() => Role, { cascade: true }) - roles: Role[]; - - @Column() - joined_at: Date; - - @Column({ type: "bigint", nullable: true }) - premium_since?: number; - - @Column() - deaf: boolean; - - @Column() - mute: boolean; - - @Column() - pending: boolean; - - @Column({ type: "jsonb", select: false }) - settings: UserGuildSettings; - - @Column({ nullable: true }) - last_message_id?: string; - - /** - @JoinColumn({ name: "id" }) - @ManyToOne(() => User, { - onDelete: "DO NOTHING", - // do not auto-kick force-joined members just because their joiners left the server - }) **/ - @Column({ nullable: true }) - joined_by: string; - - @Column({ nullable: true }) - avatar?: string; - - @Column({ nullable: true }) - banner: string; - - @Column() - bio: string; - - @Column({ nullable: true, type: "int4", array: true }) - theme_colors?: number[]; // TODO: Separate `User` and `UserProfile` models - - @Column({ nullable: true }) - pronouns?: string; - - @Column({ nullable: true, type: Date }) - communication_disabled_until: Date | null; - - // TODO: add this when we have proper read receipts - // @Column({ type: "jsonb" }) - // read_state: ReadState; - - @Column({ type: "jsonb", nullable: true }) - avatar_decoration_data?: AvatarDecorationData; - - @Column({ type: "jsonb", nullable: true }) - display_name_styles?: DisplayNameStyle; - - @Column({ type: "jsonb", nullable: true }) - collectibles?: Collectibles; - - @Column({ type: "int", default: 0 }) - flags: number = 0; - - @BeforeUpdate() - @BeforeInsert() - validate() { - if (this.nick) { - this.nick = this.nick.split("\n").join(""); - this.nick = this.nick.split("\t").join(""); - } - if (this.nick === "") this.nick = undefined; - if (this.pronouns === "") this.pronouns = undefined; - } - - static async IsInGuildOrFail(user_id: string, guild_id: string) { - if ( - await Member.count({ - where: { id: user_id, guild_id }, - }) - ) - return; - throw new HTTPError("You are not member of this guild", 403); - } - - static async removeFromGuild(user_id: string, guild_id: string) { - const guild = await Guild.findOneOrFail({ - select: { owner_id: true }, - where: { id: guild_id }, - }); - if (guild.owner_id === user_id) throw new Error("The owner cannot be removed from the guild"); - const member = await Member.findOneOrFail({ - where: { id: user_id, guild_id }, - relations: { user: true }, - }); - - // use promise all to execute all promises at the same time -> save time - return Promise.all([ - Member.delete({ - id: user_id, - guild_id, - }), - Guild.decrement({ id: guild_id }, "member_count", 1), - - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id, - }, - user_id: user_id, - } satisfies GuildDeleteEvent), - emitEvent({ - event: "GUILD_MEMBER_REMOVE", - data: { guild_id, user: member.user.toPublicUser() }, - guild_id, - } satisfies GuildMemberRemoveEvent), - ]); - } - - static async addRole(user_id: string, guild_id: string, role_id: string) { - const [member] = await Promise.all([ - Member.findOneOrFail({ - where: { id: user_id, guild_id }, - relations: { user: true, roles: true }, // we don't want to load the role objects just the ids - select: { - index: true, - roles: { - id: true, - }, - }, - }), - Role.findOneOrFail({ - where: { id: role_id, guild_id }, - select: { id: true }, - }), - ]); - member.roles.push(Role.create({ id: role_id })); - - await Promise.all([ - member.save(), - emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id, - user: member.user, - roles: member.roles.map((x) => x.id), - }, - guild_id, - } satisfies GuildMemberUpdateEvent), - ]); - } - - static async removeRole(user_id: string, guild_id: string, role_id: string) { - const [member] = await Promise.all([ - Member.findOneOrFail({ - where: { id: user_id, guild_id }, - relations: { user: true, roles: true }, // we don't want to load the role objects just the ids - select: { - index: true, - roles: { - id: true, - }, - }, - }), - Role.findOneOrFail({ where: { id: role_id, guild_id } }), - ]); - member.roles = member.roles.filter((x) => x.id !== role_id); - - await Promise.all([ - member.save(), - emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id, - user: member.user, - roles: member.roles.map((x) => x.id), - }, - guild_id, - } satisfies GuildMemberUpdateEvent), - ]); - } - - static async changeNickname(user_id: string, guild_id: string, nickname: string) { - const member = await Member.findOneOrFail({ - where: { - id: user_id, - guild_id, - }, - relations: { user: true, roles: true }, - }); - - // @ts-expect-error Member nickname is nullable - member.nick = nickname || null; - - await Promise.all([ - member.save(), - - emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id, - user: member.user, - nick: nickname || undefined, - roles: member.roles.map((x) => x.id), - }, - guild_id, - } satisfies GuildMemberUpdateEvent), - ]); - } - - static async addToGuild(user_id: string, guild_id: string, isRegistration: boolean = false) { - const totalSw = Stopwatch.startNew(); - const incSw = Stopwatch.startNew(); - const logTrace = (...data: unknown[]) => { - if (process.env.LOG_VERBOSE_TRACES !== "true") return; - console.log("[Member.addToGuild]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); - }; - - if (!isRegistration) { - const isBanned = Ban.exists({ where: { guild_id, user_id } }); - const isMember = Member.exists({ where: { id: user_id, guild_id } }); - - if (await isBanned) throw DiscordApiErrors.USER_BANNED; - logTrace("Check bans"); - - if (await isMember) throw new HTTPError("You are already a member of this guild", 400); - logTrace("Check existing membership"); - - const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ where: { id: user_id } }); - if (guild_count >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} guild limit.`, 403); - } - logTrace("Enforce max guilds"); - } - - const guild = await Guild.findOneOrFail({ - where: { - id: guild_id, - }, - relations: PublicGuildRelations, - relationLoadStrategy: "query", - }); - const channelPositionsGuild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: { channel_ordering: true }, - }); - logTrace("Find guild"); - - const newMember = Member.create({ - id: user_id, - guild_id, - nick: undefined, - joined_at: new Date(), - deaf: false, - mute: false, - pending: false, - bio: "", - roles: [Role.create({ id: guild_id })], // @everyone role - // read_state: {}, - settings: { - guild_id: null, - mute_config: null, - mute_scheduled_events: false, - flags: 0, - hide_muted_channels: false, - notify_highlights: 0, - channel_overrides: {}, - message_notifications: guild.default_message_notifications, - mobile_push: true, - muted: false, - suppress_everyone: false, - suppress_roles: false, - version: 0, - }, - // Member.save is needed because else the roles relations wouldn't be updated - }); - - let memberCount = 0; - let memberPreview: PublicMember[] = []; - if (!isRegistration) { - for await (const channel of guild.channels) { - channel.position = await Channel.calculatePosition(channel.id, guild_id, channelPositionsGuild); - } - - logTrace("Reorder channels"); - - memberCount = isRegistration ? 0 : await Member.count({ where: { guild_id } }); - logTrace("Get member count"); - - memberPreview = ( - await Member.find({ - where: { - guild_id, - user: { - id: Not(user_id), - sessions: { - status: Not("invisible" as const), // lol typescript? - }, - }, - }, - relations: { user: true, roles: true }, - take: 10, - }) - ).map((member) => member.toPublicMember()); - logTrace("Calculate member preview"); - } - - const user = await User.getPublicUser(user_id); - logTrace("Get user"); - - await Promise.all([ - newMember.save(), // TODO: can we somehow insert the roles manually? We have no entity for this... Would skip a few select's - Guild.increment({ id: guild_id }, "member_count", 1), - emitEvent({ - event: "GUILD_MEMBER_ADD", - data: { - ...newMember.toPublicMember(), - user: user, - guild_id, - }, - guild_id, - origin: "util/entities/Member.ts:377/addToGuild(user_id, guild_id)", - } satisfies GuildMemberAddEvent), - isRegistration - ? null - : emitEvent({ - event: "GUILD_CREATE", - data: { - ...new ReadyGuildDTO(guild).toJSON(), - members: [...memberPreview, { ...newMember.toPublicMember(), user }], - member_count: memberCount + 1, - guild_hashes: {}, - guild_scheduled_events: [], - joined_at: newMember.joined_at, - presences: [], - stage_instances: [], - threads: [], - embedded_activities: [], - voice_states: guild.voice_states.map((x) => x.toPublicVoiceState()), - }, - user_id, - } satisfies GuildCreateEvent), - ]); - logTrace("Save member info"); - - const welcomeChannelId = guild.system_channel_id; - if (welcomeChannelId && (await Channel.exists({ where: { id: welcomeChannelId } }))) { - // Send a welcome message - const message = Message.create({ - type: 7, - guild_id: guild.id, - channel_id: welcomeChannelId, - author: user, - timestamp: new Date(), - reactions: [], - attachments: [], - embeds: [], - sticker_items: [], - edited_timestamp: undefined, - mentions: [], - mention_channels: [], - mention_roles: [], - mention_everyone: false, - }); - - await Promise.all([ - message.insert(), - emitEvent({ - event: "MESSAGE_CREATE", - channel_id: message.channel_id, - data: message.toJSON(), - } satisfies MessageCreateEvent), - Channel.update({ id: welcomeChannelId }, { last_message_id: message.id }), - ]); - logTrace("Send welcome message"); - } - } - - toPublicMember() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const member: any = {}; - PublicMemberProjection.forEach((x) => { - member[x] = this[x]; - }); - - if (this.roles) member.roles = this.roles.map((x: Role) => x.id); - if (this.user) member.user = this.user.toPublicUser(); - - return member as PublicMember; - } -} diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts deleted file mode 100644 index 68e7cbbb..00000000 --- a/src/util/entities/Message.ts +++ /dev/null @@ -1,486 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { User } from "./User"; -import { Member } from "./Member"; -import { Role } from "./Role"; -import { Channel } from "./Channel"; -import { Application } from "./Application"; -import { Column, CreateDateColumn, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId, FindOneOptions, Raw, Not, BaseEntity, In } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { Webhook } from "./Webhook"; -import { Sticker } from "./Sticker"; -import { Attachment } from "./Attachment"; -import { NewUrlUserSignatureData } from "../Signing"; -import { - ApplicationCommandType, - BaseMessageComponents, - Embed, - MessageComponentType, - MessageSnapshot, - MessageType, - PartialMessage, - Poll, - PublicMessage, - Reaction, - UnfurledMediaItem, - PartialUser, - InteractionType, -} from "@spacebar/schemas"; -import { MessageFlags } from "@spacebar/util"; -import { JsonRemoveEmpty } from "../util/Decorators"; - -@Entity({ - name: "messages", -}) -@Index(["channel_id", "id"], { unique: true }) -export class Message extends BaseClass { - @Column({ nullable: true }) - @RelationId((message: Message) => message.channel) - @Index() - channel_id?: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.thread) - @JsonRemoveEmpty - thread_id?: string; - - @JoinColumn({ name: "thread_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - @JsonRemoveEmpty - thread?: Channel; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.guild) - @JsonRemoveEmpty - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild?: Guild; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.author) - @Index() - author_id?: string; - - @JoinColumn({ name: "author_id", referencedColumnName: "id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - author?: User; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.member) - member_id?: string; - - @JoinColumn({ name: "member_id", referencedColumnName: "id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - member?: Member; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.webhook) - @JsonRemoveEmpty - webhook_id?: string; - - @JoinColumn({ name: "webhook_id" }) - @ManyToOne(() => Webhook) - webhook?: Webhook; - - @Column({ nullable: true }) - @RelationId((message: Message) => message.application) - application_id?: string; - - @JoinColumn({ name: "application_id" }) - @ManyToOne(() => Application) - application?: Application; - - @Column({ nullable: true }) - content?: string; - - @Column() - @CreateDateColumn() - timestamp: Date; - - @Column({ nullable: true }) - edited_timestamp?: Date; - - @Column({ nullable: true }) - tts?: boolean; - - @Column({ nullable: true }) - mention_everyone?: boolean; - - @JoinTable({ name: "message_user_mentions" }) - @ManyToMany(() => User) - mentions: User[]; - - @JoinTable({ name: "message_role_mentions" }) - @ManyToMany(() => Role) - mention_roles: Role[]; - - @JoinTable({ name: "message_channel_mentions" }) - @ManyToMany(() => Channel) - @JsonRemoveEmpty - mention_channels: Channel[]; - - @JoinTable({ name: "message_stickers" }) - @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) - @JsonRemoveEmpty - sticker_items?: Sticker[]; - - @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { - cascade: true, - orphanedRowAction: "delete", - }) - attachments?: Attachment[]; - - @Column({ type: "jsonb" }) - embeds: Embed[]; - - @Column({ type: "jsonb" }) - @JsonRemoveEmpty - reactions: Reaction[]; - - @Column({ type: "text", nullable: true }) - @JsonRemoveEmpty - nonce?: string; - - @Column({ nullable: true, type: Date }) - pinned_at?: Date | null; - - get pinned(): boolean { - return this.pinned_at != null; - } - - @Column({ type: "int" }) - type: MessageType; - - @Column({ type: "jsonb", nullable: true }) - @JsonRemoveEmpty - activity?: { - type: number; - party_id: string; - }; - - @Column({ default: 0 }) - flags: number; - - @Column({ type: "jsonb", nullable: true }) - @JsonRemoveEmpty - message_reference?: { - message_id?: string; - channel_id?: string; - guild_id?: string; - type?: number; // 0 = DEFAULT, 1 = FORWARD - }; - - @JoinColumn({ name: "message_reference_id" }) - @ManyToOne(() => Message, { onDelete: "SET NULL" }) - referenced_message?: Message | null; - - @Column({ type: "jsonb", nullable: true }) - @JsonRemoveEmpty - interaction?: { - id: string; - type: InteractionType; - name: string; - }; - - @Column({ type: "jsonb", nullable: true }) - @JsonRemoveEmpty - interaction_metadata?: { - id: string; - type: InteractionType; - user_id: string; - authorizing_integration_owners: object; - name: string; - command_type: ApplicationCommandType; - }; - - @Column({ type: "jsonb", nullable: true }) - components?: BaseMessageComponents[]; - - @Column({ type: "jsonb", nullable: true }) - @JsonRemoveEmpty - poll?: Poll; - - @Column({ nullable: true }) - username?: string; - - @Column({ nullable: true }) - avatar?: string; - - @Column({ default: "[]", type: "jsonb" }) - message_snapshots: MessageSnapshot[]; - - get isWebhook() { - return this.webhook_id != null && this.webhook != null; - } - - static async fillReplies(messages: Message[]) { - const ms = messages - .filter((msg) => msg.message_reference && !msg.referenced_message?.id && msg.message_reference.message_id) - .filter((msg) => [MessageType.REPLY, MessageType.THREAD_STARTER_MESSAGE, MessageType.CONTEXT_MENU_COMMAND].includes(msg.type)); - if (!ms.length) return; - const curMs = new Map(messages.map((m) => [m.id, m] as const)); - const neededIds = new Set(ms.map((m) => m.message_reference!.message_id as string)).difference(curMs); - if (neededIds.size) { - const newMessages = await Message.find({ - where: { - id: In([...neededIds]), - }, - relations: { author: true, mentions: true, mention_roles: true, mention_channels: true }, - }); - newMessages.forEach((msg) => curMs.set(msg.id, msg)); - } - for (const message of ms) { - message.referenced_message = curMs.get(message.message_reference!.message_id as string) || null; - } - } - - toJSON(shallow = false): PublicMessage { - // this.clean_data(); - return { - ...this, - channel_id: this.channel_id ?? this.channel.id, - channel: undefined, - - timestamp: this.timestamp.toISOString(), - edited_timestamp: this.edited_timestamp ? this.edited_timestamp.toISOString() : null, - - author_id: undefined, - member_id: undefined, - webhook_id: this.webhook_id ?? undefined, - application_id: undefined, - mentions: this.mentions?.map((user) => { - if (user && !user.toPublicUser) console.trace("toPublic user missing!!!"); - return (user?.toPublicUser?.() ?? user ?? undefined) as unknown as PartialUser; - }), - - mention_roles: this.mention_roles?.map((role) => role.id) ?? [], - mention_channels: this.mention_channels?.map((ch) => ch.toJSON()) ?? [], - attachments: this.attachments?.map((att) => att.toJSON()) ?? [], - - nonce: this.nonce ?? undefined, - tts: this.tts ?? false, - guild: this.guild ?? undefined, - webhook: this.webhook ?? undefined, - interaction: this.interaction ?? undefined, - interaction_metadata: this.interaction_metadata ?? undefined, - reactions: this.reactions ?? undefined, - sticker_items: this.sticker_items ?? undefined, - message_reference: this.message_reference ?? undefined, - mention_everyone: this.mention_everyone ?? false, - author: { - ...(this.author?.toPublicUser() ?? undefined), - // Webhooks - username: this.username ?? this.author?.username ?? null, - avatar: this.avatar ?? this.author?.avatar ?? null, - }, - activity: this.activity ?? undefined, - application: this.application ?? undefined, - components: this.components ?? [], - poll: this.poll ?? undefined, - content: this.content ?? "", - pinned: this.pinned, - thread: this.thread ? this.thread.toJSON() : this.thread, - referenced_message: this.referenced_message && !shallow ? this.referenced_message.toJSON(true) : undefined, - }; - } - - toPartialMessage(): PartialMessage { - return { - id: this.id, - // lobby_id: this.lobby_id, - channel_id: this.channel_id!, - type: this.type, - content: this.content!, - author: { ...this.author!, avatar: this.author?.avatar ?? null }, - flags: this.flags, - application_id: this.application_id, - //channel: this.channel, // TODO: ephemeral DM channels - // recipient_id: this.recipient_id, // TODO: ephemeral DM channels - }; - } - - toSnapshot(): MessageSnapshot { - return { - message: { - attachments: this.attachments?.map((x) => x.toJSON()), - components: this.components, - content: this.content!, - edited_timestamp: this.edited_timestamp, - embeds: this.embeds, - flags: this.flags, - mention_roles: this.mention_roles?.map((x) => x.id), - mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this - timestamp: this.timestamp, - type: this.type, - }, - }; - } - - withSignedAttachments(data: NewUrlUserSignatureData) { - function signMedia(media: UnfurledMediaItem) { - Object.assign(media, Attachment.prototype.signUrls.call(media, data)); - } - return { - ...this, - attachments: this.attachments?.map((attachment: Attachment) => Attachment.prototype.signUrls.call(attachment, data)), - components: this.components - ? this.components.map((comp) => { - comp = structuredClone(comp); - if (comp.type === MessageComponentType.Section) { - const accessory = comp.accessory; - if (accessory.type === MessageComponentType.Thumbnail) { - signMedia(accessory.media); - } - } else if (comp.type === MessageComponentType.MediaGallery) { - comp.items.forEach(({ media }) => signMedia(media)); - } else if (comp.type === MessageComponentType.File) { - signMedia(comp.file); - } else if (comp.type === MessageComponentType.Container) { - for (const elm of comp.components) { - switch (elm.type) { - case MessageComponentType.Separator: - case MessageComponentType.TextDisplay: - case MessageComponentType.ActionRow: - break; - case MessageComponentType.Section: { - const accessory = elm.accessory; - if (accessory.type === MessageComponentType.Thumbnail) { - signMedia(accessory.media); - } - break; - } - case MessageComponentType.MediaGallery: - elm.items.forEach(({ media }) => signMedia(media)); - break; - case MessageComponentType.File: { - signMedia(elm.file); - break; - } - - default: - elm satisfies never; - } - } - } - return comp; - }) - : this.components, - }; - } - - static async createWithDefaults(opts: Partial): Promise { - const message = Message.create(); - - if (!opts.author) { - if (!opts.author_id) throw new Error("Either author or author_id must be provided to create a Message"); - opts.author = await User.findOneOrFail({ where: { id: opts.author_id! } }); - } - - if (!opts.channel) { - if (!opts.channel_id) throw new Error("Either channel or channel_id must be provided to create a Message"); - opts.channel = await Channel.findOneOrFail({ where: { id: opts.channel_id! } }); - opts.guild_id ??= opts.channel.guild_id; - } - - if (!opts.member_id) opts.member_id = message.author_id; - if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.member_id! } }); - - if (!opts.guild) { - if (opts.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.guild_id! } }); - else if (opts.channel?.guild?.id) opts.guild = opts.channel.guild; - else if (opts.channel?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.channel.guild_id! } }); - else if (opts.member?.guild?.id) opts.guild = opts.member.guild; - else if (opts.member?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.member.guild_id! } }); - else throw new Error("Either guild, guild_id, channel.guild, channel.guild_id, member.guild or member.guild_id must be provided to create a Message"); - } - - // try 2 now that we have a guild - if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.author!.id, guild_id: opts.guild!.id } }); - - // set reply type if a message if referenced - if (opts.message_reference && !opts.type) message.type = MessageType.REPLY; - - // backpropagate ids - opts.channel_id = opts.channel.id; - opts.guild_id = opts.guild.id; - opts.author_id = opts.author.id; - opts.member_id = opts.member.id; - opts.webhook_id = opts.webhook?.id; - opts.application_id = opts.application?.id; - - delete opts.member; - - Object.assign(message, { - tts: false, - embeds: [], - reactions: [], - flags: 0, - type: 0, - timestamp: new Date(), - ...opts, - }); - return message; - } - static addDefault(options: FindOneOptions) { - if (options.where) { - const arr = options.where instanceof Array ? options.where : [options.where]; - for (const thing of arr) { - if (!("flags" in thing)) { - thing.flags = Not(Raw((alias) => `${alias} & ${MessageFlags.FLAGS.EPHEMERAL} = ${MessageFlags.FLAGS.EPHEMERAL}`)); - } - } - } - } -} - -//@ts-expect-error It works but TS types hate it -Message.findOneOrFail = function (this: Message, options: FindOneOptions): Promise { - Message.addDefault(options as FindOneOptions); - //@ts-expect-error how to use generics on call, who knows! - return BaseEntity.findOneOrFail.call(Message, options); -}; -//@ts-expect-error It works but TS types hate it -Message.findOne = function (this: Message, options: FindOneOptions): Promise { - Message.addDefault(options as FindOneOptions); - //@ts-expect-error how to use generics on call, who knows! - return BaseEntity.findOne.call(Message, options); -}; -//@ts-expect-error It works but TS types hate it -Message.find = function (this: Message, options: FindOneOptions): Promise { - Message.addDefault(options as FindOneOptions); - //@ts-expect-error how to use generics on call, who knows! - return BaseEntity.find.call(Message, options); -}; diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts deleted file mode 100644 index b4a5ff78..00000000 --- a/src/util/entities/Migration.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, PrimaryGeneratedColumn, BaseEntity } from "typeorm"; - -@Entity({ - name: "migrations", -}) -export class Migration extends BaseEntity { - @PrimaryGeneratedColumn() - id: number; - - @Column({ type: "bigint" }) - timestamp: number; - - @Column() - name: string; -} diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts deleted file mode 100644 index e9f173ff..00000000 --- a/src/util/entities/Note.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; - -@Entity({ - name: "notes", -}) -@Unique(["owner", "target"]) -export class Note extends BaseClass { - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User, { onDelete: "CASCADE" }) - owner: User; - - @JoinColumn({ name: "target_id" }) - @ManyToOne(() => User, { onDelete: "CASCADE" }) - target: User; - - @Column() - content: string; -} diff --git a/src/util/entities/RateLimit.ts b/src/util/entities/RateLimit.ts deleted file mode 100644 index ab85285c..00000000 --- a/src/util/entities/RateLimit.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity } from "typeorm"; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "rate_limits", -}) -export class RateLimit extends BaseClass { - @Column() // no relation as it also - executor_id: string; - - @Column() - hits: number; - - @Column() - blocked: boolean; - - @Column() - expires_at: Date; -} diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts deleted file mode 100644 index 4d15c6cc..00000000 --- a/src/util/entities/ReadState.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Channel } from "./Channel"; -import { User } from "./User"; -import { ReadStateFlags, ReadStateType } from "@spacebar/schemas"; - -// for read receipts -// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable -// public read receipt ≥ notification cursor ≥ private fully read marker - -@Entity({ - name: "read_states", -}) -@Index(["channel_id", "user_id"], { unique: true }) -export class ReadState extends BaseClass { - @Column() - @RelationId((read_state: ReadState) => read_state.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column() - @RelationId((read_state: ReadState) => read_state.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ nullable: true }) - last_message_id?: string; - - @Column({ nullable: true }) - last_acked_id?: string; - - @Column({ nullable: true }) - notifications_cursor: string; - - @Column({ default: 0 }) - mention_count: number; - - @Column({ default: 0 }) - badge_count: number; - - @Column({ nullable: true }) - last_pin_timestamp?: Date; - - @Column({ default: ReadStateType.CHANNEL }) - read_state_type: ReadStateType; - - @Column({ default: 0 }) - flags: ReadStateFlags; - - // toJSON() { - // const res = { ...this } as Partial; - // if (this.read_state_type === ReadStateType.CHANNEL) { - // delete res.badge_count; - // delete res.last_acked_id; - // } else { - // delete res.mention_count; // mutually exclusive with badge_count - // delete res.last_message_id; // mutually exclusive with last_acked_id - // // these only apply to channels: - // delete res.last_pin_timestamp; - // delete res.flags; - // // delete res.last_viewed; // TODO - // } - // return res; - // } -} diff --git a/src/util/entities/Recipient.ts b/src/util/entities/Recipient.ts deleted file mode 100644 index 2dcff244..00000000 --- a/src/util/entities/Recipient.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "recipients", -}) -export class Recipient extends BaseClass { - @Column() - @RelationId((recipient: Recipient) => recipient.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => require("./Channel").Channel, { - onDelete: "CASCADE", - }) - channel: import("./Channel").Channel; - - @Column() - @RelationId((recipient: Recipient) => recipient.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => require("./User").User, { - onDelete: "CASCADE", - }) - user: import("./User").User; - - @Column({ default: false }) - closed: boolean; - - // TODO: settings/mute/nick/added at/encryption keys/read_state -} diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts deleted file mode 100644 index c2256ed3..00000000 --- a/src/util/entities/Relationship.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { RelationshipType } from "@spacebar/schemas"; - -@Entity({ - name: "relationships", -}) -@Index(["from_id", "to_id"], { unique: true }) -export class Relationship extends BaseClass { - @Column({}) - @RelationId((relationship: Relationship) => relationship.from) - from_id: string; - - @JoinColumn({ name: "from_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - from: User; - - @Column({}) - @RelationId((relationship: Relationship) => relationship.to) - to_id: string; - - @JoinColumn({ name: "to_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - to: User; - - @Column({ nullable: true }) - nickname?: string; - - @Column({ type: "int" }) - type: RelationshipType; - - toPublicRelationship() { - return { - id: this.to?.id || this.to_id, - type: this.type, - nickname: this.nickname, - user: this.to?.toPublicUser(), - }; - } -} diff --git a/src/util/entities/ReportMenu.ts b/src/util/entities/ReportMenu.ts deleted file mode 100644 index a37ae159..00000000 --- a/src/util/entities/ReportMenu.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2024 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 . -*/ - -import { BaseClass } from "./BaseClass"; -import { Entity, Column } from "typeorm"; -import { ReportMenuType } from "../../schemas/api/reports/ReportMenu"; - -@Entity({ - name: "report_menus", -}) -export class ReportMenu extends BaseClass { - @Column() - type: ReportMenuType; - - @Column() - variant: string; - - @Column() - isCurrent: boolean; - - @Column({ nullable: true }) - inherits?: string; - - @Column({ type: "jsonb" }) - content: unknown; -} diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts deleted file mode 100644 index e6b2e459..00000000 --- a/src/util/entities/Role.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; - -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { RoleColors } from "@spacebar/schemas"; - -@Entity({ - name: "roles", -}) -export class Role extends BaseClass { - @Column() - @RelationId((role: Role) => role.guild) - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.roles, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column() - color: number; - - @Column() - hoist: boolean; - - @Column({ default: false }) - managed: boolean; - - @Column() - mentionable: boolean; - - @Column() - name: string; - - @Column() - permissions: string; - - @Column() - position: number; - - @Column({ nullable: true }) - icon?: string; - - @Column({ nullable: true }) - unicode_emoji?: string; - - @Column({ type: "jsonb", nullable: true }) - tags?: { - bot_id?: string; - integration_id?: string; - premium_subscriber?: boolean; - }; - - @Column({ default: 0 }) - flags: number; - - @Column({ nullable: false, type: "jsonb" }) - colors: RoleColors; - - toJSON(): Role { - return { - ...this, - tags: this.tags ?? undefined, - }; - } -} diff --git a/src/util/entities/SecurityKey.ts b/src/util/entities/SecurityKey.ts deleted file mode 100644 index cbf80d3d..00000000 --- a/src/util/entities/SecurityKey.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; - -@Entity({ - name: "security_keys", -}) -export class SecurityKey extends BaseClass { - @Column({ nullable: true }) - @RelationId((key: SecurityKey) => key.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column() - key_id: string; - - @Column() - public_key: string; - - @Column() - counter: number; - - @Column() - name: string; -} diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts deleted file mode 100644 index 1cb77ebe..00000000 --- a/src/util/entities/Session.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - 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 . -*/ - -import crypto from "node:crypto"; -import { User } from "./User"; -import { BaseClassWithoutId } from "./BaseClass"; -import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; -import { Activity, ClientStatus, GatewaySession, GatewaySessionClientInfo, PrivateStatus } from "../interfaces"; -import { randomUpperString } from "@spacebar/api"; -import { DateBuilder, IpDataClient, TimeSpan } from "../util"; - -@Entity({ - name: "sessions", -}) -export class Session extends BaseClassWithoutId { - @PrimaryColumn({ nullable: false }) - session_id: string = randomUpperString(); - - @Column() - @RelationId((session: Session) => session.user) - @Index({}) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ type: "jsonb", default: "[]" }) - activities: Activity[]; - - @Column({ type: "jsonb" }) - client_info: { - platform?: string; - os?: string; - version?: number; - location?: string; - }; - - @Column({ type: "jsonb" }) - client_status: ClientStatus; - - @Column({ nullable: false, type: String }) - status: PrivateStatus; - - @Column({ default: false }) - is_admin_session: boolean; - - @CreateDateColumn({ type: Date }) - created_at: Date; - - @Column({ nullable: true, type: Date }) - last_seen?: Date; - - @Column({ nullable: true, type: String }) - last_seen_ip?: string; - - @Column({ nullable: true, type: String }) - last_seen_location?: string; - - @Column({ nullable: true, type: "jsonb" }) - last_seen_location_info?: ExtendedLocationInfo; - - @Column({ nullable: true, type: String }) - session_nickname?: string; - - getPublicStatus() { - return this.status === "invisible" ? "offline" : this.status; - } - - getDiscordDeviceInfo() { - return { - id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), - approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(), - client_info: { - os: this.client_info?.os, - platform: - this.client_info?.platform + (this.client_info?.version ? ` ${this.client_info?.version}` : "") + (this.session_nickname ? ` (${this.session_nickname})` : ""), - location: this.last_seen_location, - }, - }; - } - - getExtendedDeviceInfo() { - return { - id: this.session_id, - id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"), - status: this.status, - activities: this.activities, - client_status: this.client_status, - approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(), - client_info: { - ...(this.client_info ?? {}), - location: this.last_seen_location, - }, - last_seen: this.last_seen, - last_seen_ip: this.last_seen_ip, - last_seen_location: this.last_seen_location, - last_seen_location_info: this.last_seen_location_info, - }; - } - - toPrivateGatewayDeviceInfo(): GatewaySession { - // TODO: ... or has `show_current_game` privacy setting enabled - except spotify (always visible) - const hasPrivateActivities = this.status == "offline" || this.status == "invisible"; - const inactiveTreshold = new DateBuilder(new Date(0)).addMinutes(5).buildTimestamp(); - - return { - session_id: this.session_id, - client_info: { - client: this.client_info?.platform ?? "", - os: this.client_info?.os ?? "", - version: this.client_info?.version ?? 0, - } as GatewaySessionClientInfo, - status: this.status, - activities: hasPrivateActivities ? [] : this.activities, - hidden_activities: hasPrivateActivities ? this.activities : [], - active: TimeSpan.fromDates(this.last_seen?.getTime() ?? 0, new Date().getTime()).totalMillis < inactiveTreshold, - }; - } - - async updateIpInfo() { - const ipInfo = await IpDataClient.getIpInfo(this.last_seen_ip!); - if (ipInfo?.ip) { - this.last_seen_location = `${ipInfo.emoji_flag} ${ipInfo.postal} ${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country_name}`; - this.last_seen_location_info = { - is_eu: ipInfo.is_eu, - city: ipInfo.city, - region: ipInfo.region, - region_code: ipInfo.region_code, - country_name: ipInfo.country_name, - country_code: ipInfo.country_code, - continent_name: ipInfo.continent_name, - continent_code: ipInfo.continent_code, - latitude: ipInfo.latitude, - longitude: ipInfo.longitude, - postal: ipInfo.postal, - calling_code: ipInfo.calling_code, - flag: ipInfo.flag, - emoji_flag: ipInfo.emoji_flag, - emoji_unicode: ipInfo.emoji_unicode, - }; - } - } -} - -export interface ExtendedLocationInfo { - is_eu: boolean; - city: string; - region: string; - region_code: string; - country_name: string; - country_code: string; - continent_name: string; - continent_code: string; - latitude: number; - longitude: number; - postal: string; - calling_code: string; - flag: string; - emoji_flag: string; - emoji_unicode: string; -} - -export const PrivateSessionProjection: (keyof Session)[] = ["user_id", "session_id", "activities", "client_info", "status"]; diff --git a/src/util/entities/Sticker.ts b/src/util/entities/Sticker.ts deleted file mode 100644 index c88e0c29..00000000 --- a/src/util/entities/Sticker.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { User } from "./User"; -import { StickerFormatType, StickerType } from "@spacebar/schemas"; - -@Entity({ - name: "stickers", -}) -export class Sticker extends BaseClass { - @Column() - name: string; - - @Column({ nullable: true }) - description?: string; - - @Column({ nullable: true }) - available?: boolean; - - @Column({ nullable: true }) - tags?: string; - - @Column({ nullable: true }) - @RelationId((sticker: Sticker) => sticker.pack) - pack_id?: string; - - @JoinColumn({ name: "pack_id" }) - @ManyToOne(() => require("./StickerPack").StickerPack, { - onDelete: "CASCADE", - nullable: true, - }) - pack: import("./StickerPack").StickerPack; - - @Column({ nullable: true }) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.stickers, { - onDelete: "CASCADE", - }) - guild?: Guild; - - @Column({ nullable: true }) - user_id?: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user?: User; - - @Column({ type: "int" }) - type: StickerType; - - @Column({ type: "int" }) - format_type: StickerFormatType; -} diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts deleted file mode 100644 index d4a598f4..00000000 --- a/src/util/entities/StickerPack.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { Sticker } from "."; -import { BaseClass } from "./BaseClass"; - -@Entity({ - name: "sticker_packs", -}) -export class StickerPack extends BaseClass { - @Column() - name: string; - - @Column({ nullable: true }) - description?: string; - - @Column({ nullable: true }) - banner_asset_id?: string; - - @OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, { - cascade: true, - orphanedRowAction: "delete", - }) - stickers: Sticker[]; - - // sku_id: string - - @Column({ nullable: true }) - @RelationId((pack: StickerPack) => pack.cover_sticker) - cover_sticker_id?: string; - - @ManyToOne(() => Sticker, { nullable: true }) - @JoinColumn() - cover_sticker?: Sticker; -} diff --git a/src/util/entities/Stream.ts b/src/util/entities/Stream.ts deleted file mode 100644 index bf2f3a62..00000000 --- a/src/util/entities/Stream.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { Channel } from "./Channel"; - -@Entity({ - name: "streams", -}) -export class Stream extends BaseClass { - @Column() - @RelationId((stream: Stream) => stream.owner) - owner_id: string; - - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - owner: User; - - @Column() - @RelationId((stream: Stream) => stream.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column() - endpoint: string; -} diff --git a/src/util/entities/StreamSession.ts b/src/util/entities/StreamSession.ts deleted file mode 100644 index 1dca2424..00000000 --- a/src/util/entities/StreamSession.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { Stream } from "./Stream"; - -@Entity({ - name: "stream_sessions", -}) -export class StreamSession extends BaseClass { - @Column() - @RelationId((session: StreamSession) => session.stream) - stream_id: string; - - @JoinColumn({ name: "stream_id" }) - @ManyToOne(() => Stream, { - onDelete: "CASCADE", - }) - stream: Stream; - - @Column() - @RelationId((session: StreamSession) => session.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ nullable: true }) - token: string; - - // this is for gateway session - @Column() - session_id: string; - - @Column({ default: false }) - used: boolean; -} diff --git a/src/util/entities/Tag.ts b/src/util/entities/Tag.ts deleted file mode 100644 index 63ad4b86..00000000 --- a/src/util/entities/Tag.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Channel } from "./Channel"; - -@Entity({ - name: "tags", -}) -export class Tag extends BaseClass { - @Column() - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel) => channel.available_tags, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column() - name: string; - - @Column() - moderated: boolean = false; - - @Column({ nullable: true }) - emoji_id?: string; - - @Column({ nullable: true }) - emoji_name?: string; - - toJSON() { - return { - name: this.name, - id: this.id, - moderated: this.moderated, - emoji_id: this.emoji_id, - emoji_name: this.emoji_name, - }; - } -} diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts deleted file mode 100644 index e440e965..00000000 --- a/src/util/entities/Team.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { TeamMember } from "./TeamMember"; -import { User } from "./User"; - -@Entity({ - name: "teams", -}) -export class Team extends BaseClass { - @Column({ nullable: true }) - icon?: string; - - @JoinColumn({ name: "member_ids" }) - @OneToMany(() => TeamMember, (member: TeamMember) => member.team, { - orphanedRowAction: "delete", - }) - members: TeamMember[]; - - @Column() - name: string; - - @Column({ nullable: true }) - @RelationId((team: Team) => team.owner_user) - owner_user_id: string; - - @JoinColumn({ name: "owner_user_id" }) - @ManyToOne(() => User) - owner_user: User; -} diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts deleted file mode 100644 index 72e23b76..00000000 --- a/src/util/entities/TeamMember.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { User } from "./User"; -import { TeamMemberRole, TeamMemberState } from "@spacebar/schemas"; - -@Entity({ - name: "team_members", -}) -export class TeamMember extends BaseClass { - @Column({ type: "int" }) - membership_state: TeamMemberState; - - @Column({ type: "varchar", array: true }) - permissions: string[]; - - @Column() - role: TeamMemberRole; - - @Column({ nullable: true }) - @RelationId((member: TeamMember) => member.team) - team_id: string; - - @JoinColumn({ name: "team_id" }) - @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, { - onDelete: "CASCADE", - }) - team: import("./Team").Team; - - @Column({ nullable: true }) - @RelationId((member: TeamMember) => member.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; -} diff --git a/src/util/entities/Template.ts b/src/util/entities/Template.ts deleted file mode 100644 index 79804eed..00000000 --- a/src/util/entities/Template.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { User } from "./User"; - -@Entity({ - name: "templates", -}) -export class Template extends BaseClass { - @Column({ unique: true }) - code: string; - - @Column() - name: string; - - @Column({ nullable: true }) - description?: string; - - @Column({ nullable: true }) - usage_count?: number; - - @Column({ nullable: true }) - @RelationId((template: Template) => template.creator) - creator_id: string; - - @JoinColumn({ name: "creator_id" }) - @ManyToOne(() => User) - creator: User; - - @Column() - created_at: Date; - - @Column() - updated_at: Date; - - @Column({ nullable: true }) - @RelationId((template: Template) => template.source_guild) - source_guild_id: string; - - @JoinColumn({ name: "source_guild_id" }) - @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - source_guild: Guild; - - @Column({ type: "jsonb" }) - serialized_source_guild: Guild; -} diff --git a/src/util/entities/ThreadMember.ts b/src/util/entities/ThreadMember.ts deleted file mode 100644 index cea61f51..00000000 --- a/src/util/entities/ThreadMember.ts +++ /dev/null @@ -1,195 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2026 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 . -*/ - -import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm"; -import { ThreadMembersUpdateEvent } from "../interfaces"; -import { emitEvent } from "../util"; -import { BaseClassWithoutId } from "./BaseClass"; -import { Channel } from "./Channel"; -import { HTTPError } from "lambert-server/HTTPError"; -import { Member } from "./Member"; - -// TODO: move -interface ThreadMemberMuteConfig { - end_time?: Date; - selected_time_window?: number; -} - -// TODO: move -export enum ThreadMemberFlags { - NONE = 0, - HAS_INTERACTED = 1 << 0, - ALL_MESSAGES = 1 << 1, - ONLY_MENTIONS = 1 << 2, - NO_MESSAGES = 1 << 3, -} - -@Entity("thread_members") -@Index(["id", "member_idx"], { unique: true }) -export class ThreadMember extends BaseClassWithoutId { - @PrimaryGeneratedColumn() - index: string; - - @Column() - @RelationId((member: ThreadMember) => member.channel) - id: string; - - @JoinColumn({ name: "id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column() - @RelationId((member: ThreadMember) => member.member) - member_idx: string; - - @JoinColumn({ name: "member_idx" }) - @ManyToOne(() => Member, { - onDelete: "CASCADE", - }) - member: Member; - - @Column() - join_timestamp: Date; - - @Column() - muted: boolean; - - @Column({ nullable: true, type: "jsonb" }) - mute_config?: ThreadMemberMuteConfig; - - @Column() - flags: ThreadMemberFlags; - - static async IsInThreadOrFail(member_id: string, thread_id: string) { - if (await ThreadMember.count({ where: { id: thread_id, member_idx: member_id } })) return true; - throw new HTTPError("You are not member of this thread", 403); - } - - static async removeFromThread(member_id: string, thread_id: string) { - const channel = await Channel.findOneOrFail({ where: { id: thread_id } }); - if ( - !(await ThreadMember.count({ - where: { - id: thread_id, - member_idx: member_id, - }, - })) - ) - throw new HTTPError("You are not member of this thread", 403); - // // use promise all to execute all promises at the same time -> save time - // TODO: check for bugs - if (channel.member_count) channel.member_count--; - return Promise.all([ - ThreadMember.delete({ - id: thread_id, - member_idx: member_id, - }), - // //Guild.decrement({ id: guild_id }, "member_count", -1), - - emitEvent({ - event: "THREAD_MEMBERS_UPDATE", - data: { - guild_id: channel.guild_id!, // TODO: is this the right fix? - id: channel.id, - member_count: channel.member_count ?? 0, - removed_member_ids: [member_id], - }, - channel_id: thread_id, - } satisfies ThreadMembersUpdateEvent), - ]); - } - - // static async addRole(user_id: string, guild_id: string, role_id: string) { - // const [member, role] = await Promise.all([ - // // @ts-ignore - // Member.findOneOrFail({ - // where: { id: user_id, guild_id }, - // relations: ["user", "roles"], // we don't want to load the role objects just the ids - // select: ["index"] - // }), - // Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }) - // ]); - // member.roles.push(OrmUtils.mergeDeep(new Role(), { id: role_id })); - - // await Promise.all([ - // member.save(), - // emitEvent({ - // event: "GUILD_MEMBER_UPDATE", - // data: { - // guild_id, - // user: member.user, - // roles: member.roles.map((x) => x.id) - // }, - // guild_id - // } satisfies GuildMemberUpdateEvent) - // ]); - // } - - // static async removeRole(user_id: string, guild_id: string, role_id: string) { - // const [member] = await Promise.all([ - // // @ts-ignore - // Member.findOneOrFail({ - // where: { id: user_id, guild_id }, - // relations: ["user", "roles"], // we don't want to load the role objects just the ids - // select: ["index"] - // }), - // await Role.findOneOrFail({ where: { id: role_id, guild_id } }) - // ]); - // member.roles = member.roles.filter((x) => x.id == role_id); - - // await Promise.all([ - // member.save(), - // emitEvent({ - // event: "GUILD_MEMBER_UPDATE", - // data: { - // guild_id, - // user: member.user, - // roles: member.roles.map((x) => x.id) - // }, - // guild_id - // } satisfies GuildMemberUpdateEvent) - // ]); - // } - - // static async changeNickname(user_id: string, guild_id: string, nickname: string) { - // const member = await Member.findOneOrFail({ - // where: { - // id: user_id, - // guild_id - // }, - // relations: ["user"] - // }); - // member.nick = nickname; - - // await Promise.all([ - // member.save(), - - // emitEvent({ - // event: "GUILD_MEMBER_UPDATE", - // data: { - // guild_id, - // user: member.user, - // nick: nickname - // }, - // guild_id - // } satisfies GuildMemberUpdateEvent) - // ]); - // } -} diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts deleted file mode 100644 index afde3e9b..00000000 --- a/src/util/entities/User.ts +++ /dev/null @@ -1,408 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Request } from "express"; -import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm"; -import { Channel, Config, Email, FieldErrors, Snowflake, Stopwatch, trimSpecial } from ".."; -import { Random } from "../util"; -import { BaseClass } from "./BaseClass"; -import { ConnectedAccount } from "./ConnectedAccount"; -import { Member } from "./Member"; -import { Relationship } from "./Relationship"; -import { SecurityKey } from "./SecurityKey"; -import { Session } from "./Session"; -import { UserSettings } from "./UserSettings"; -import { - AvatarDecorationData, - ChannelType, - Collectibles, - DisplayNameStyle, - PrimaryGuild, - PrivateUserProjection, - PublicUser, - PublicUserProjection, - UserPrivate, -} from "@spacebar/schemas"; -import { JsonNumber } from "../util/Decorators"; - -@Entity({ - name: "users", -}) -export class User extends BaseClass { - @Column() - username: string; // username max length 32, min 2 (should be configurable) - - @Column() - discriminator: string; // opaque string: 4 digits on discord.com - - @Column({ nullable: true }) - avatar?: string; // hash of the user avatar - - @Column({ nullable: true }) - accent_color?: number; // banner color of user - - @Column({ nullable: true }) - banner?: string; // hash of the user banner - - // TODO: Separate `User` and `UserProfile` models - // puyo: changed from [number, number] because it breaks openapi - @Column({ nullable: true, type: "int4", array: true }) - theme_colors?: number[]; - - @Column({ nullable: true }) - pronouns?: string; - - @Column({ nullable: true, select: false }) - phone?: string; // phone number of the user - - @Column({ select: false }) - desktop: boolean = false; // if the user has desktop app installed - - @Column({ select: false }) - mobile: boolean = false; // if the user has mobile app installed - - @Column() - premium: boolean; // if user bought individual premium - - @Column() - premium_type: number; // individual premium level - - @Column() - bot: boolean = false; // if user is bot - - @Column() - bio: string = ""; // short description of the user - - @Column() - system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author - - @Column({ select: false }) - nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age - - @Column({ select: false }) - mfa_enabled: boolean = false; // if multi factor authentication is enabled - - @Column({ select: false, default: false }) - webauthn_enabled: boolean = false; // if webauthn multi factor authentication is enabled - - @Column({ select: false, nullable: true }) - totp_secret?: string = ""; - - @Column({ nullable: true, select: false }) - totp_last_ticket?: string = ""; - - @Column() - created_at: Date; // registration date - - @Column({ nullable: true }) - premium_since: Date; // premium date - - @Column({ select: false }) - verified: boolean; // email is verified - - @Column() - disabled: boolean = false; // if the account is disabled - - @Column() - deleted: boolean = false; // if the user was deleted - - @Column({ nullable: true, select: false }) - email?: string; // email of the user - - @Column({ type: "bigint" }) - @JsonNumber - flags: number = 0; // UserFlags // TODO: generate - - @Column({ type: "bigint" }) - @JsonNumber - public_flags: number = 0; - - @Column({ type: "bigint" }) - @JsonNumber - purchased_flags: number = 0; - - @Column() - premium_usage_flags: number = 0; - - @Column({ type: "bigint" }) - @JsonNumber - rights: string; - - @OneToMany(() => Session, (session: Session) => session.user) - sessions: Session[]; - - @JoinColumn({ name: "relationship_ids" }) - @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { - cascade: true, - orphanedRowAction: "delete", - }) - relationships: Relationship[]; - - @JoinColumn({ name: "connected_account_ids" }) - @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { - cascade: true, - orphanedRowAction: "delete", - }) - connected_accounts: ConnectedAccount[]; - - @Column({ type: "jsonb", select: false }) - data: { - valid_tokens_since: Date; // all tokens with a previous issue date are invalid - hash?: string; // hash of the password, salt is saved in password (bcrypt) - }; - - @Column({ type: "varchar", array: true, select: false }) - fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts - - @OneToOne(() => UserSettings, { - cascade: true, - orphanedRowAction: "delete", - nullable: true, - }) - @JoinColumn() - settings?: UserSettings; - - @OneToMany(() => SecurityKey, (key: SecurityKey) => key.user) - security_keys: SecurityKey[]; - - @Column({ type: "int8", array: true, nullable: true }) - badge_ids?: string[]; - - @Column({ type: "jsonb", nullable: true }) - avatar_decoration_data?: AvatarDecorationData; - - @Column({ type: "jsonb", nullable: true }) - display_name_styles?: DisplayNameStyle; - - @Column({ type: "jsonb", nullable: true }) - collectibles?: Collectibles; - - @Column({ type: "jsonb", nullable: true }) - primary_guild?: PrimaryGuild; - - // TODO: I don't like this method? - validate() { - if (this.discriminator) { - const discrim = Number(this.discriminator); - if (isNaN(discrim) || !Number.isInteger(discrim) || discrim <= 0 || discrim >= 10000) - throw FieldErrors({ - discriminator: { - message: "Discriminator must be a number.", - code: "DISCRIMINATOR_INVALID", - }, - }); - - this.discriminator = discrim.toString().padStart(4, "0"); - } - } - - toPublicUser() { - this.clean_data(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = {}; - PublicUserProjection.forEach((x) => { - user[x] = this[x]; - }); - return user as PublicUser; - } - - toPrivateUser(extraFields: (keyof User)[] = []) { - this.clean_data(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = {}; - [...PrivateUserProjection, ...extraFields].forEach((x) => { - user[x] = this[x]; - }); - return user as UserPrivate; - } - - static async getPublicUser(user_id: string): Promise { - const user = await User.findOneOrFail({ - where: { id: user_id }, - select: PublicUserProjection, - }); - return user.toPublicUser(); - } - - public static async generateDiscriminator(username: string): Promise { - if (Config.get().register.incrementingDiscriminators) { - // discriminator will be incrementally generated - - // First we need to figure out the currently highest discrimnator for the given username and then increment it - const users = await User.find({ - where: { username }, - select: { discriminator: true }, - }); - const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator))); - - const discriminator = highestDiscriminator + 1; - if (discriminator >= 10000) { - return undefined; - } - - return discriminator.toString().padStart(4, "0"); - } else { - // discriminator will be randomly generated - - // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists - // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? - const takenDiscriminators = (await User.find({ where: { username }, select: { discriminator: true } })).map((x) => x.discriminator); - if (takenDiscriminators.length >= 9999) return undefined; - - for (let tries = 0; tries < 15; tries++) { - const discriminator = Random.nextInt(1, 9999).toString().padStart(4, "0"); - if (!takenDiscriminators.includes(discriminator)) return discriminator; - } - - return undefined; - } - } - - public get tag(): string { - //const { uniqueUsernames } = Config.get().general; - const uniqueUsernames = false; - - return uniqueUsernames ? this.username : `${this.username}#${this.discriminator}`; - } - - static async register({ - email, - username, - password, - id, - req, - bot, - }: { - username: string; - password?: string; - email?: string; - date_of_birth?: Date; // "2000-04-03" - id?: string; - req?: Request; - bot?: boolean; - }) { - const totalSw = Stopwatch.startNew(); - const incSw = Stopwatch.startNew(); - const logTrace = (...data: unknown[]) => { - if (process.env.LOG_VERBOSE_TRACES !== "true") return; - console.log("[User.register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); - }; - - // trim special utf8 control characters -> Backspace, Newline, ... - username = trimSpecial(username); - - const discriminator = await User.generateDiscriminator(username); - if (!discriminator) { - // We've failed to generate a valid and unused discriminator - throw FieldErrors({ - username: { - code: "USERNAME_TOO_MANY_USERS", - message: req?.t("auth:register.USERNAME_TOO_MANY_USERS") || "", - }, - }); - } - logTrace("Generate discriminator"); - - // TODO: save date_of_birth - // apparently discord doesn't save the date of birth and just calculate if nsfw is allowed - // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - const language = req?.language === "en" ? "en-US" : req?.language || "en-US"; - - const settings = UserSettings.create({ - locale: language, - }); - - const user = User.create({ - username: username, - discriminator, - id: id || Snowflake.generate(), - email: email, - data: { - hash: password, - valid_tokens_since: new Date(), - }, - settings: settings, - - premium_since: Config.get().defaults.user.premium ? new Date() : undefined, - rights: Config.get().register.defaultRights, - premium: Config.get().defaults.user.premium ?? false, - premium_type: Config.get().defaults.user.premiumType ?? 0, - verified: Config.get().defaults.user.verified ?? true, - created_at: new Date(), - bot: !!bot, - }); - - user.validate(); - logTrace("Generate/validate user"); - - await Promise.all([user.save(), settings.save()]); - logTrace("Save user"); - - // send verification email if users aren't verified by default and we have an email - if (!Config.get().defaults.user.verified && email) { - await Email.sendVerifyEmail(user, email).catch((e) => { - console.error(`Failed to send verification email to ${user.tag}: ${e}`); - }); - logTrace("Send verify email"); - } - - const { autoJoin } = Config.get().guild; - if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) { - await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild, true).catch((e) => console.error("[Autojoin]", e)))); - logTrace("Autojoin", autoJoin.guilds.length, "guilds"); - } - - return user; - } - - async getDmChannelWith(user_id: string) { - const qry = await Channel.getRepository() - .createQueryBuilder() - .leftJoinAndSelect("Channel.recipients", "rcp") - .where("Channel.type = :type", { type: ChannelType.DM }) - .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] }) - .groupBy("Channel.id") - .having("COUNT(rcp.user_id) = 2") - .getMany(); - - // Emma [it/its]@Rory&: is this technically a bug, or am I being too over-cautious? - if (qry.length > 1) { - console.warn(`[WARN] User(${this.id})#getDmChannel(${user_id}) returned multiple channels:`); - for (const channel of qry) { - console.warn(JSON.stringify(channel)); - } - throw new Error("Array contains more than one matching element"); - } - - return qry[0]; - } - - async getDmChannels() { - const qry = await Channel.getRepository() - .createQueryBuilder("channel") - .leftJoinAndSelect("channel.recipients", "rcp") - .where("channel.type = :type", { type: ChannelType.DM }) - .andWhere("rcp.user_id = :user_id", { user_id: this.id }) - .groupBy("channel.id") - .addGroupBy("rcp.id") - .having("COUNT(rcp.id) = 2") - .getMany(); - - return qry; - } -} diff --git a/src/util/entities/UserSettings.ts b/src/util/entities/UserSettings.ts deleted file mode 100644 index 7ccb8e1e..00000000 --- a/src/util/entities/UserSettings.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; -import { CustomStatus, FriendSourceFlags, GuildFolder } from "@spacebar/schemas"; - -@Entity({ - name: "user_settings", -}) -export class UserSettings extends BaseClassWithoutId { - @PrimaryGeneratedColumn() - index: string; - - @Column({ nullable: true }) - afk_timeout: number = 3600; - - @Column({ nullable: true }) - allow_accessibility_detection: boolean = true; - - @Column({ nullable: true }) - animate_emoji: boolean = true; - - @Column({ nullable: true }) - animate_stickers: number = 0; - - @Column({ nullable: true }) - contact_sync_enabled: boolean = false; - - @Column({ nullable: true }) - convert_emoticons: boolean = false; - - @Column({ nullable: true, type: "jsonb" }) - custom_status: CustomStatus | null = null; - - @Column({ nullable: true }) - default_guilds_restricted: boolean = false; - - @Column({ nullable: true }) - detect_platform_accounts: boolean = false; - - @Column({ nullable: true }) - developer_mode: boolean = true; - - @Column({ nullable: true }) - disable_games_tab: boolean = true; - - @Column({ nullable: true }) - enable_tts_command: boolean = false; - - @Column({ nullable: true }) - explicit_content_filter: number = 0; - - @Column({ nullable: true }) - friend_discovery_flags: number = 0; - - @Column({ nullable: true, type: "jsonb" }) - friend_source_flags: FriendSourceFlags = { all: true }; - - @Column({ nullable: true }) - gateway_connected: boolean = false; - - @Column({ nullable: true }) - gif_auto_play: boolean = false; - - @Column({ nullable: true, type: "jsonb" }) - guild_folders: GuildFolder[] = []; // every top guild is displayed as a "folder" - - @Column({ nullable: true, type: "jsonb" }) - guild_positions: string[] = []; // guild ids ordered by position - - @Column({ nullable: true }) - inline_attachment_media: boolean = true; - - @Column({ nullable: true }) - inline_embed_media: boolean = true; - - @Column({ nullable: true }) - locale: string = "en-US"; // en_US - - @Column({ nullable: true }) - message_display_compact: boolean = false; - - @Column({ nullable: true }) - native_phone_integration_enabled: boolean = true; - - @Column({ nullable: true }) - render_embeds: boolean = true; - - @Column({ nullable: true }) - render_reactions: boolean = true; - - @Column({ nullable: true, type: "jsonb" }) - restricted_guilds: string[] = []; - - @Column({ nullable: true }) - show_current_game: boolean = true; - - @Column({ nullable: true }) - status: "online" | "offline" | "dnd" | "idle" | "invisible" = "online"; - - @Column({ nullable: true }) - stream_notifications_enabled: boolean = false; - - @Column({ nullable: true }) - theme: "dark" | "light" = "dark"; // dark - - @Column({ nullable: true }) - timezone_offset: number = 0; // e.g -60 - - @Column({ nullable: true }) - view_nsfw_guilds: boolean = true; - - public static async getOrDefault(userId: string) { - // raw sql query - const userSettingsIndex = (await this.getRepository().query('SELECT "settingsIndex" FROM users WHERE id = $1', [userId]))[0]?.settingsIndex as string | null; - - console.log(`[INFO/UserSettings] Fetched settings index for user ${userId}:`, userSettingsIndex); - - if (!userSettingsIndex) return new UserSettings(); - - const settings = await UserSettings.findOne({ where: { index: userSettingsIndex } }); - if (!settings) return new UserSettings(); - - return settings; - } -} diff --git a/src/util/entities/UserSettingsProtos.ts b/src/util/entities/UserSettingsProtos.ts deleted file mode 100644 index c8ee9d12..00000000 --- a/src/util/entities/UserSettingsProtos.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - 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 . -*/ - -import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; -import { BaseClassWithoutId } from "./BaseClass"; -import { User } from "./User"; -import { FrecencyUserSettings, PreloadedUserSettings } from "discord-protos"; - -@Entity({ - name: "user_settings_protos", -}) -export class UserSettingsProtos extends BaseClassWithoutId { - @OneToOne(() => User, { - cascade: true, - orphanedRowAction: "delete", - eager: false, - }) - @JoinColumn({ name: "user_id" }) - user: User; - - @PrimaryColumn({ type: "text" }) - user_id: string; - - @Column({ nullable: true, type: String, name: "userSettings" }) - _userSettings: string | undefined; - - @Column({ nullable: true, type: String, name: "frecencySettings" }) - _frecencySettings: string | undefined; - - // @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)); - } - - set userSettings(value: PreloadedUserSettings | undefined) { - if (value) { - // this._userSettings = JSON.stringify(value, this.bigintReplacer); - this._userSettings = PreloadedUserSettings.toJsonString(value); - } else { - this._userSettings = undefined; - } - } - - get frecencySettings(): FrecencyUserSettings | undefined { - if (!this._frecencySettings) return undefined; - return FrecencyUserSettings.fromJson(JSON.parse(this._frecencySettings, this.bigintReviver)); - } - - set frecencySettings(value: FrecencyUserSettings | undefined) { - if (value) { - this._frecencySettings = JSON.stringify(value, this.bigintReplacer); - } else { - this._frecencySettings = undefined; - } - } - - static async getOrDefault(user_id: string, save: boolean = false): Promise { - await User.findOneOrFail({ - where: { id: user_id }, - select: { settings: true }, - }); - - let userSettings = await UserSettingsProtos.findOne({ - where: { user_id }, - }); - - let modified = false; - if (!userSettings) { - userSettings = UserSettingsProtos.create({ - user_id, - }); - modified = true; - } - - if (!userSettings.userSettings) { - userSettings.userSettings = PreloadedUserSettings.create({ - versions: { - dataVersion: 0, - clientVersion: 0, - serverVersion: 0, - }, - }); - modified = true; - } - - if (!userSettings.frecencySettings) { - userSettings.frecencySettings = FrecencyUserSettings.create({ - versions: { - dataVersion: 0, - clientVersion: 0, - serverVersion: 0, - }, - }); - modified = true; - } - - if (modified && save) userSettings = await userSettings.save(); - - return userSettings; - } -} diff --git a/src/util/entities/ValidRegistrationTokens.ts b/src/util/entities/ValidRegistrationTokens.ts deleted file mode 100644 index f8cb35a4..00000000 --- a/src/util/entities/ValidRegistrationTokens.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm"; - -@Entity({ - name: "valid_registration_tokens", -}) -export class ValidRegistrationToken extends BaseEntity { - @PrimaryColumn() - token: string; - - @Column() - created_at: Date = new Date(); - - @Column() - expires_at: Date; -} diff --git a/src/util/entities/VoiceState.ts b/src/util/entities/VoiceState.ts deleted file mode 100644 index 5abdb7da..00000000 --- a/src/util/entities/VoiceState.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Channel } from "./Channel"; -import { Guild } from "./Guild"; -import { Member } from "./Member"; -import { User } from "./User"; -import { PublicVoiceState, PublicVoiceStateProjection } from "@spacebar/schemas"; - -//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex -@Entity({ - name: "voice_states", -}) -export class VoiceState extends BaseClass { - @Column({ nullable: true }) - @RelationId((voice_state: VoiceState) => voice_state.guild) - guild_id: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild) => guild.voice_states, { - onDelete: "CASCADE", - }) - guild?: Guild; - - @Column({ nullable: true }) - @RelationId((voice_state: VoiceState) => voice_state.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column({ nullable: true }) - @RelationId((voice_state: VoiceState) => voice_state.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - // @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }]) - // @ManyToOne(() => Member, { - // onDelete: "CASCADE", - // }) - //TODO find a way to make it work without breaking Guild.voice_states - member: Member; - - @Column() - session_id: string; - - @Column({ nullable: true }) - token: string; - - @Column() - deaf: boolean; - - @Column() - mute: boolean; - - @Column() - self_deaf: boolean; - - @Column() - self_mute: boolean; - - @Column({ nullable: true }) - self_stream?: boolean; - - @Column() - self_video: boolean; - - @Column() - suppress: boolean; // whether this user is muted by the current user - - @Column({ nullable: true, default: null }) - request_to_speak_timestamp?: Date; - - toPublicVoiceState() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const voiceState: any = {}; - PublicVoiceStateProjection.forEach((x) => { - voiceState[x] = this[x]; - }); - return voiceState as PublicVoiceState; - } -} diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts deleted file mode 100644 index f631c393..00000000 --- a/src/util/entities/Webhook.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 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 . -*/ - -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { Application } from "./Application"; -import { BaseClass } from "./BaseClass"; -import { Channel } from "./Channel"; -import { Guild } from "./Guild"; -import { User } from "./User"; -import { WebhookType } from "@spacebar/schemas"; - -@Entity({ - name: "webhooks", -}) -export class Webhook extends BaseClass { - @Column({ type: "int" }) - type: WebhookType; - - @Column({ nullable: true }) - name: string; - - @Column({ nullable: true }) - avatar: string; - - @Column({ nullable: true }) - token?: string; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.guild) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild?: Guild; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.channel) - channel_id: string; - - @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - channel: Channel; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.application) - application_id: string; - - @JoinColumn({ name: "application_id" }) - @ManyToOne(() => Application, { - onDelete: "CASCADE", - }) - application: Application; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, { - onDelete: "CASCADE", - }) - user: User; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.guild) - source_guild_id?: string; - - @JoinColumn({ name: "source_guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - source_guild?: Guild; - - @Column({ nullable: true }) - @RelationId((webhook: Webhook) => webhook.channel) - source_channel_id: string; - - @JoinColumn({ name: "source_channel_id" }) - @ManyToOne(() => Channel, { - onDelete: "CASCADE", - }) - source_channel: Channel; - - url: string; -} diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts deleted file mode 100644 index 79fceecc..00000000 --- a/src/util/entities/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2024 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 . -*/ - -export * from "./Application"; -export * from "./ApplicationCommand"; -export * from "./Attachment"; -export * from "./AuditLog"; -export * from "./AutomodRule"; -export * from "./BackupCodes"; -export * from "./Badge"; -export * from "./Ban"; -export * from "./BaseClass"; -export * from "./Categories"; -export * from "./Channel"; -export * from "./ClientRelease"; -export * from "./CloudAttachment"; -export * from "./Config"; -export * from "./ConnectedAccount"; -export * from "./ConnectionConfigEntity"; -export * from "./EmbedCache"; -export * from "./Emoji"; -export * from "./Encryption"; -export * from "./Guild"; -export * from "./InstanceBan"; -export * from "./Invite"; -export * from "./Member"; -export * from "./Message"; -export * from "./Migration"; -export * from "./Note"; -export * from "./RateLimit"; -export * from "./ReadState"; -export * from "./Recipient"; -export * from "./Relationship"; -export * from "./Role"; -export * from "./SecurityKey"; -export * from "./Session"; -export * from "./Sticker"; -export * from "./StickerPack"; -export * from "./Stream"; -export * from "./StreamSession"; -export * from "./Team"; -export * from "./TeamMember"; -export * from "./Template"; -export * from "./ThreadMember"; -export * from "./User"; -export * from "./UserSettings"; -export * from "./UserSettingsProtos"; -export * from "./ValidRegistrationTokens"; -export * from "./VoiceState"; -export * from "./Webhook"; -export * from "./Tag"; diff --git a/src/util/index.ts b/src/util/index.ts index dd2f7f32..2b739838 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -22,7 +22,7 @@ import "reflect-metadata"; export * from "./util/index"; export * from "./interfaces/index"; -export * from "./entities/index"; + export * from "./dtos/index"; export * from "./imports"; export * from "./config"; diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 6152f70a..22eec64f 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -16,27 +16,8 @@ along with this program. If not, see . */ -import { - ConnectedAccount, - Invite, - Role, - Emoji, - Channel, - User, - Sticker, - Activity, - Status, - Presence, - UserSettings, - IReadyGuildDTO, - ReadState, - ReadyUserGuildSettingsEntries, - ReadyPrivateChannel, - GuildOrUnavailable, - Snowflake, - ThreadMember, - PrivateStatus, -} from "@spacebar/util"; +import { ConnectedAccount, Invite, Role, Emoji, Channel, User, Sticker, UserSettings, ReadState, ThreadMember } from "@spacebar/database"; +import { Activity, Presence, IReadyGuildDTO, ReadyUserGuildSettingsEntries, ReadyPrivateChannel, GuildOrUnavailable, Snowflake, PrivateStatus } from "@spacebar/util"; import { JsonValue } from "@protobuf-ts/runtime"; import { ApplicationCommand, diff --git a/src/util/util/Application.ts b/src/util/util/Application.ts index 64c87775..f4e08cde 100644 --- a/src/util/util/Application.ts +++ b/src/util/util/Application.ts @@ -1,5 +1,23 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + import { Request } from "express"; -import { Application, User } from "../entities"; +import { Application, User } from "../../database/entities"; export async function createAppBotUser(app: Application, req: Request) { const user = await User.register({ diff --git a/src/util/util/Config.ts b/src/util/util/Config.ts index 3c1c45f1..46c5a168 100644 --- a/src/util/util/Config.ts +++ b/src/util/util/Config.ts @@ -20,7 +20,7 @@ import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import { OrmUtils } from ".."; import { ConfigValue } from "../config"; -import { ConfigEntity } from "../entities"; +import { ConfigEntity } from "../../database/entities"; import { JsonValue } from "@protobuf-ts/runtime"; import { bold, red, redBright } from "picocolors"; diff --git a/src/util/util/Decorators.ts b/src/util/util/Decorators.ts index 18910b9d..3211cb27 100644 --- a/src/util/util/Decorators.ts +++ b/src/util/util/Decorators.ts @@ -1,4 +1,22 @@ -import { BaseClassWithoutId } from "@spacebar/util"; +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 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 . +*/ + +import { BaseClassWithoutId } from "@spacebar/database"; export const annotationsKey = Symbol("Annotations"); diff --git a/src/util/util/Permissions.ts b/src/util/util/Permissions.ts index 66dd1943..6585f692 100644 --- a/src/util/util/Permissions.ts +++ b/src/util/util/Permissions.ts @@ -2,7 +2,7 @@ // Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah // @fc-license-skip -import { Channel, Guild, Member, Role, User } from "../entities"; +import { Channel, Guild, Member, Role, User } from "../../database/entities"; import { BitField, BitFieldResolvable, BitFlag } from "./BitField"; import { HTTPError } from "lambert-server/HTTPError"; import { ChannelPermissionOverwrite, ChannelPermissionOverwriteType, ChannelType, UserFlags } from "@spacebar/schemas"; diff --git a/src/util/util/Presence.ts b/src/util/util/Presence.ts index 5a475adc..8180a026 100644 --- a/src/util/util/Presence.ts +++ b/src/util/util/Presence.ts @@ -16,7 +16,8 @@ along with this program. If not, see . */ -import { emitEvent, Member, PresenceUpdateEvent, Recipient, Relationship, Session } from "@spacebar/util"; +import { Member, Recipient, Relationship, Session } from "@spacebar/database"; +import { emitEvent, PresenceUpdateEvent } from "@spacebar/util"; import { RelationshipType } from "@spacebar/schemas"; import { Not } from "typeorm"; diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts index f0425749..0eee0a3f 100644 --- a/src/util/util/Rights.ts +++ b/src/util/util/Rights.ts @@ -17,7 +17,7 @@ */ import { BitField, BitFieldResolvable, BitFlag } from "./BitField"; -import { User } from "../entities"; +import { User } from "../../database/entities"; import { HTTPError } from "lambert-server/HTTPError"; export type RightResolvable = bigint | number | Rights | RightResolvable[] | RightString; diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index a1c4536e..10bd5e2c 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -18,7 +18,7 @@ import jwt from "jsonwebtoken"; import { Config } from "./Config"; -import { InstanceBan, Session, User } from "../entities"; +import { InstanceBan, Session, User } from "../../database/entities"; import crypto from "node:crypto"; import fs from "node:fs/promises"; import { existsSync } from "node:fs"; diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts index 46fa0ebd..ef934f78 100644 --- a/src/util/util/cdn.ts +++ b/src/util/util/cdn.ts @@ -18,7 +18,7 @@ import FormData from "form-data"; import { HTTPError } from "lambert-server/HTTPError"; -import { Attachment } from "../entities"; +import { Attachment } from "../../database/entities"; import { Config } from "./Config"; export async function uploadFile( diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts index cceb99f4..9b37b1b0 100644 --- a/src/util/util/email/index.ts +++ b/src/util/util/email/index.ts @@ -18,7 +18,7 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { User } from "../../entities"; +import { User } from "../../../database/entities"; import { Config } from "../Config"; import { generateToken } from "../Token"; import { IEmail, IEmailClient } from "./clients/IEmailClient"; diff --git a/src/webrtc/opcodes/Identify.ts b/src/webrtc/opcodes/Identify.ts index 5068e42b..7d776d9c 100644 --- a/src/webrtc/opcodes/Identify.ts +++ b/src/webrtc/opcodes/Identify.ts @@ -16,8 +16,8 @@ along with this program. If not, see . */ +import { StreamSession, VoiceState } from "@spacebar/database"; import { CLOSECODES } from "@spacebar/gateway"; -import { StreamSession, VoiceState } from "@spacebar/util"; import { validateSchema, VoiceIdentifySchema } from "@spacebar/schemas"; import { generateSsrc, mediaServer, Send, VoiceOPCodes, VoicePayload, WebRtcWebSocket } from "@spacebar/webrtc"; import { SSRCs } from "@spacebarchat/spacebar-webrtc-types"; diff --git a/src/webrtc/opcodes/Video.ts b/src/webrtc/opcodes/Video.ts index 323438a6..e448536a 100644 --- a/src/webrtc/opcodes/Video.ts +++ b/src/webrtc/opcodes/Video.ts @@ -15,7 +15,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Stream } from "@spacebar/util"; + +import { Stream } from "@spacebar/database"; import { mediaServer, Send, VoiceOPCodes, VoicePayload, WebRtcWebSocket } from "@spacebar/webrtc"; import type { WebRtcClient } from "@spacebarchat/spacebar-webrtc-types"; import { validateSchema, VoiceVideoSchema } from "@spacebar/schemas"; -- cgit 1.5.1