Merge branch 'master' into feat/federation

This commit is contained in:
Madeline
2023-09-22 15:46:35 +10:00
22 changed files with 323 additions and 205 deletions
+3 -3
View File
@@ -2,9 +2,9 @@ name: Build
on:
push:
branches: [ "**" ]
branches: [ "master" ]
pull_request:
branches: [ "**" ]
branches: [ "master" ]
jobs:
build:
@@ -25,4 +25,4 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm run test --if-present
- run: npm run test --if-present
+3 -3
View File
@@ -2,9 +2,9 @@ name: Style
on:
push:
branches: [ "**" ]
branches: [ "master" ]
pull_request:
branches: [ "**" ]
branches: [ "master" ]
jobs:
build:
@@ -25,4 +25,4 @@ jobs:
cache: 'npm'
- run: npm i --only=dev
- run: npx eslint .
- run: npx prettier --check .
- run: npx prettier --check .
+11 -1
View File
@@ -16,6 +16,9 @@
## [About](https://spacebar.chat)
Spacebar/server is a Discord backend re-implementation and extension.
We aim to reverse engineer and add additional features to the Discord backend, while remaining completely backwards compatible with existing bots, applications, and clients.
This repository contains:
- [Spacebar HTTP API Server](/src/api)
@@ -23,6 +26,13 @@ This repository contains:
- [HTTP CDN Server](/src/cdn)
- [Utility and Database Models](/src/util)
## [Documentation](https://docs.spacebar.chat)
## [Contributing](https://docs.spacebar.chat/contributing/)
## [Setup](https://docs.spacebar.chat/setup/server/)
## Clients
You *should* be able to use any client designed for Discord.com to connect to a Spacebar instance.
However, some incompatibilities still exist between Spacebar and Discord. For this reason, not every client will connect.
The [WIP official Spacebar client](https://github.com/spacebarchat/client) will always work.
You can find a [live version here](https://app.spacebar.chat).
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -3
View File
@@ -68,9 +68,8 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.385.0",
"@sentry/integrations": "^7.61.1",
"@sentry/node": "^7.61.1",
"@sentry/tracing": "^7.61.1",
"@sentry/integrations": "^7.66.0",
"@sentry/node": "^7.66.0",
"ajv": "8.6.2",
"ajv-formats": "2.1.1",
"amqplib": "^0.10.3",
+1
View File
@@ -72,6 +72,7 @@ function main() {
(x.endsWith("Schema") ||
x.endsWith("Response") ||
x.startsWith("API")) &&
!x.startsWith("AP") &&
!Excluded.includes(x)
);
});
+1 -1
View File
@@ -126,7 +126,7 @@ export const messageFromAP = async (data: APNote): Promise<Message> => {
const member =
channel instanceof Channel
? await Member.findOneOrFail({
where: { id: user.id, guild_id: channel.guild.id },
where: { id: user.id, guild_id: channel.guild!.id },
})
: undefined;
@@ -40,7 +40,13 @@ import {
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import multer from "multer";
import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
import {
FindManyOptions,
FindOperator,
LessThan,
MoreThan,
MoreThanOrEqual,
} from "typeorm";
import { URL } from "url";
const router: Router = Router();
@@ -122,12 +128,24 @@ router.get(
if (around) {
query.take = Math.floor(limit / 2);
const [right, left] = await Promise.all([
Message.find({ ...query, where: { id: LessThan(around) } }),
Message.find({ ...query, where: { id: MoreThan(around) } }),
]);
right.push(...left);
messages = right;
if (query.take != 0) {
const [right, left] = await Promise.all([
Message.find({ ...query, where: { id: LessThan(around) } }),
Message.find({
...query,
where: { id: MoreThanOrEqual(around) },
}),
]);
left.push(...right);
messages = left;
} else {
query.take = 1;
const message = await Message.findOne({
...query,
where: { id: around },
});
messages = message ? [message] : [];
}
} else {
if (after) {
if (BigInt(after) > BigInt(Snowflake.generate()))
@@ -18,6 +18,7 @@
import { route } from "@spacebar/api";
import {
DiscordApiErrors,
emitEvent,
Emoji,
getPermission,
@@ -198,7 +199,9 @@ router.put(
member_id = req.user_id;
rights.hasThrow("JOIN_GUILDS");
} else {
// TODO: join others by controller
// TODO: check oauth2 scope
throw DiscordApiErrors.MISSING_REQUIRED_OAUTH2_SCOPE;
}
const guild = await Guild.findOneOrFail({
+167 -95
View File
@@ -16,12 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Config, Embed, EmbedType } from "@spacebar/util";
import fetch, { RequestInit } from "node-fetch";
import { Config, Embed, EmbedImage, EmbedType } from "@spacebar/util";
import * as cheerio from "cheerio";
import probe from "probe-image-size";
import crypto from "crypto";
import fetch, { RequestInit } from "node-fetch";
import { yellow } from "picocolors";
import probe from "probe-image-size";
export const DEFAULT_FETCH_OPTIONS: RequestInit = {
redirect: "follow",
@@ -35,6 +35,20 @@ export const DEFAULT_FETCH_OPTIONS: RequestInit = {
method: "GET",
};
const makeEmbedImage = (
url: string | undefined,
width: number | undefined,
height: number | undefined,
): Required<EmbedImage> | undefined => {
if (!url || !width || !height) return undefined;
return {
url,
width,
height,
proxy_url: getProxyUrl(new URL(url), width, height),
};
};
let hasWarnedAboutImagor = false;
export const getProxyUrl = (
@@ -78,13 +92,24 @@ export const getProxyUrl = (
const getMeta = ($: cheerio.CheerioAPI, name: string): string | undefined => {
let elem = $(`meta[property="${name}"]`);
if (!elem.length) elem = $(`meta[name="${name}"]`);
return elem.attr("content") || elem.text();
const ret = elem.attr("content") || elem.text();
return ret.trim().length == 0 ? undefined : ret;
};
const tryParseInt = (str: string | undefined) => {
if (!str) return undefined;
try {
return parseInt(str);
} catch (e) {
return undefined;
}
};
export const getMetaDescriptions = (text: string) => {
const $ = cheerio.load(text);
return {
type: getMeta($, "og:type"),
title: getMeta($, "og:title") || $("title").first().text(),
provider_name: getMeta($, "og:site_name"),
author: getMeta($, "article:author"),
@@ -92,10 +117,13 @@ export const getMetaDescriptions = (text: string) => {
image: getMeta($, "og:image") || getMeta($, "twitter:image"),
image_fallback: $(`image`).attr("src"),
video_fallback: $(`video`).attr("src"),
width: parseInt(getMeta($, "og:image:width") || "0"),
height: parseInt(getMeta($, "og:image:height") || "0"),
width: tryParseInt(getMeta($, "og:image:width")),
height: tryParseInt(getMeta($, "og:image:height")),
url: getMeta($, "og:url"),
youtube_embed: getMeta($, "og:video:secure_url"),
site_name: getMeta($, "og:site_name"),
$,
};
};
@@ -116,13 +144,11 @@ const genericImageHandler = async (url: URL): Promise<Embed | null> => {
method: "HEAD",
});
let width, height, image;
let image;
if (type.headers.get("content-type")?.indexOf("image") !== -1) {
const result = await probe(url.href);
width = result.width;
height = result.height;
image = url.href;
image = makeEmbedImage(url.href, result.width, result.height);
} else if (type.headers.get("content-type")?.indexOf("video") !== -1) {
// TODO
return null;
@@ -131,22 +157,19 @@ const genericImageHandler = async (url: URL): Promise<Embed | null> => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
width = metas.width;
height = metas.height;
image = metas.image || metas.image_fallback;
image = makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
);
}
if (!width || !height || !image) return null;
if (!image) return null;
return {
url: url.href,
type: EmbedType.image,
thumbnail: {
width: width,
height: height,
url: url.href,
proxy_url: getProxyUrl(new URL(image), width, height),
},
thumbnail: image,
};
};
@@ -165,7 +188,8 @@ export const EmbedHandlers: {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
const text = await response.text();
const metas = getMetaDescriptions(text);
// TODO: handle video
@@ -178,26 +202,27 @@ export const EmbedHandlers: {
}
if (!metas.image && (!metas.title || !metas.description)) {
// we don't have any content to display
return null;
}
let embedType = EmbedType.link;
if (metas.type == "article") embedType = EmbedType.article;
if (metas.type == "object") embedType = EmbedType.article; // github
if (metas.type == "rich") embedType = EmbedType.rich;
return {
url: url.href,
type: EmbedType.link,
type: embedType,
title: metas.title,
thumbnail: {
width: metas.width,
height: metas.height,
url: metas.image,
proxy_url: metas.image
? getProxyUrl(
new URL(metas.image),
metas.width,
metas.height,
)
: undefined,
},
thumbnail: makeEmbedImage(metas.image, metas.width, metas.height),
description: metas.description,
provider: metas.site_name
? {
name: metas.site_name,
url: url.origin,
}
: undefined,
};
},
@@ -207,12 +232,23 @@ export const EmbedHandlers: {
"c.tenor.com": genericImageHandler,
"media.tenor.com": genericImageHandler,
// TODO: facebook
// have to use their APIs or something because they don't send the metas in initial html
"facebook.com": (url) => EmbedHandlers["www.facebook.com"](url),
"www.facebook.com": async (url: URL) => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
"twitter.com": (url: URL) => {
return EmbedHandlers["www.twitter.com"](url);
return {
url: url.href,
type: EmbedType.link,
title: metas.title,
description: metas.description,
thumbnail: makeEmbedImage(metas.image, 640, 640),
color: 16777215,
};
},
"twitter.com": (url) => EmbedHandlers["www.twitter.com"](url),
"www.twitter.com": async (url: URL) => {
const token = Config.get().external.twitter;
if (!token) return null;
@@ -330,14 +366,7 @@ export const EmbedHandlers: {
type: EmbedType.link,
title: metas.title,
description: metas.description,
thumbnail: {
width: 640,
height: 640,
proxy_url: metas.image
? getProxyUrl(new URL(metas.image), 640, 640)
: undefined,
url: metas.image,
},
thumbnail: makeEmbedImage(metas.image, 640, 640),
provider: {
url: "https://spotify.com",
name: "Spotify",
@@ -345,32 +374,25 @@ export const EmbedHandlers: {
};
},
"pixiv.net": (url: URL) => {
return EmbedHandlers["www.pixiv.net"](url);
},
// TODO: docs: Pixiv won't work without Imagor
"pixiv.net": (url) => EmbedHandlers["www.pixiv.net"](url),
"www.pixiv.net": async (url: URL) => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
// TODO: doesn't show images. think it's a bug in the cdn
if (!metas.image) return null;
return {
url: url.href,
type: EmbedType.image,
title: metas.title,
description: metas.description,
image: {
width: metas.width,
height: metas.height,
url: url.href,
proxy_url: metas.image
? getProxyUrl(
new URL(metas.image),
metas.width,
metas.height,
)
: undefined,
},
image: makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
),
provider: {
url: "https://pixiv.net",
name: "Pixiv",
@@ -382,6 +404,42 @@ export const EmbedHandlers: {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
const numReviews = metas.$("#review_summary_num_reviews").val() as
| string
| undefined;
const price = metas
.$(".game_purchase_price.price")
.data("price-final") as number | undefined;
const releaseDate = metas
.$(".release_date")
.find("div.date")
.text()
.trim();
const isReleased = new Date(releaseDate) < new Date();
const fields: Embed["fields"] = [];
if (numReviews)
fields.push({
name: "Reviews",
value: numReviews,
inline: true,
});
if (price)
fields.push({
name: "Price",
value: `$${price / 100}`,
inline: true,
});
// if the release date is in the past, it's already out
if (releaseDate && !isReleased)
fields.push({
name: "Release Date",
value: releaseDate,
inline: true,
});
return {
url: url.href,
@@ -402,14 +460,12 @@ export const EmbedHandlers: {
url: "https://store.steampowered.com",
name: "Steam",
},
// TODO: fields for release date
fields,
// TODO: Video
};
},
"reddit.com": (url: URL) => {
return EmbedHandlers["www.reddit.com"](url);
},
"reddit.com": (url) => EmbedHandlers["www.reddit.com"](url),
"www.reddit.com": async (url: URL) => {
const res = await EmbedHandlers["default"](url);
return {
@@ -420,49 +476,65 @@ export const EmbedHandlers: {
},
};
},
"youtu.be": (url: URL) => {
return EmbedHandlers["www.youtube.com"](url);
},
"youtube.com": (url: URL) => {
return EmbedHandlers["www.youtube.com"](url);
},
"youtu.be": (url) => EmbedHandlers["www.youtube.com"](url),
"youtube.com": (url) => EmbedHandlers["www.youtube.com"](url),
"www.youtube.com": async (url: URL): Promise<Embed | null> => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
return {
video: {
// TODO: does this adjust with aspect ratio?
width: metas.width,
height: metas.height,
url: metas.youtube_embed,
},
video: makeEmbedImage(
metas.youtube_embed,
metas.width,
metas.height,
),
url: url.href,
type: EmbedType.video,
type: metas.youtube_embed ? EmbedType.video : EmbedType.link,
title: metas.title,
thumbnail: {
width: metas.width,
height: metas.height,
url: metas.image,
proxy_url: metas.image
? getProxyUrl(
new URL(metas.image),
metas.width,
metas.height,
)
: undefined,
},
thumbnail: makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
),
provider: {
url: "https://www.youtube.com",
name: "YouTube",
},
description: metas.description,
color: 16711680,
author: {
name: metas.author,
// TODO: author channel url
},
author: metas.author
? {
name: metas.author,
// TODO: author channel url
}
: undefined,
};
},
"www.xkcd.com": (url) => EmbedHandlers["xkcd.com"](url),
"xkcd.com": async (url) => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
const hoverText = metas.$("#comic img").attr("title");
if (!metas.image) return null;
const { width, height } = await probe(metas.image);
return {
url: url.href,
type: EmbedType.rich,
title: `xkcd: ${metas.title}`,
image: makeEmbedImage(metas.image, width, height),
footer: hoverText
? {
text: hoverText,
}
: undefined,
};
},
+25 -30
View File
@@ -16,15 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { WebSocket, Payload, CLOSECODES, OPCODES } from "@spacebar/gateway";
import OPCodeHandlers from "../opcodes";
import { check } from "../opcodes/instanceOf";
import WS from "ws";
import { PayloadSchema, ErlpackType } from "@spacebar/util";
import * as Sentry from "@sentry/node";
import { CLOSECODES, OPCODES, Payload, WebSocket } from "@spacebar/gateway";
import { ErlpackType, PayloadSchema } from "@spacebar/util";
import fs from "fs/promises";
import BigIntJson from "json-bigint";
import path from "path";
import fs from "fs/promises";
import WS from "ws";
import OPCodeHandlers from "../opcodes";
import { check } from "../opcodes/instanceOf";
const bigIntJson = BigIntJson({ storeAsString: true });
let erlpack: ErlpackType | null = null;
@@ -88,33 +88,28 @@ export async function Message(this: WebSocket, buffer: WS.Data) {
return;
}
const transaction =
data.op != 1
? Sentry.startTransaction({
op: OPCODES[data.op],
name: `GATEWAY ${OPCODES[data.op]}`,
data: {
...data.d,
token: data?.d?.token ? "[Redacted]" : undefined,
},
})
: undefined;
try {
const ret = await OPCodeHandler.call(this, data);
Sentry.withScope((scope) => {
scope.setSpan(transaction);
scope.setUser({ id: this.user_id });
transaction?.finish();
});
return ret;
return await Sentry.startActiveSpan(
{
op: "websocket.server",
name: `GATEWAY ${OPCODES[data.op]}`,
data: {
...data.d,
token: data?.d?.token ? "[Redacted]" : undefined,
},
},
async () => {
const ret = await OPCodeHandler.call(this, data);
Sentry.setUser({ id: this.user_id });
return ret;
},
);
} catch (error) {
Sentry.withScope((scope) => {
scope.setSpan(transaction);
if (this.user_id) scope.setUser({ id: this.user_id });
Sentry.captureException(error);
Sentry.captureException(error, {
user: {
id: this.user_id,
},
});
transaction?.finish();
console.error(`Error: Op ${data.op}`, error);
// if (!this.CLOSED && this.CLOSING)
return this.close(CLOSECODES.Unknown_error);
+39 -32
View File
@@ -17,49 +17,57 @@
*/
import {
WebSocket,
Payload,
setupListener,
Capabilities,
CLOSECODES,
Capabilities,
OPCODES,
Payload,
Send,
WebSocket,
setupListener,
} from "@spacebar/gateway";
import {
checkToken,
Application,
Config,
DMChannel,
DefaultUserGuildSettings,
EVENTEnum,
Guild,
GuildOrUnavailable,
IdentifySchema,
Intents,
Member,
ReadyEventData,
Session,
EVENTEnum,
Config,
PublicUser,
PrivateUserProjection,
ReadState,
Application,
emitEvent,
SessionsReplace,
PrivateSessionProjection,
MemberPrivateProjection,
PresenceUpdateEvent,
IdentifySchema,
DefaultUserGuildSettings,
ReadyGuildDTO,
Guild,
PublicUserProjection,
ReadyUserGuildSettingsEntries,
UserSettings,
Permissions,
DMChannel,
GuildOrUnavailable,
Recipient,
OPCodes,
Permissions,
PresenceUpdateEvent,
PrivateSessionProjection,
PrivateUserProjection,
PublicUser,
PublicUserProjection,
ReadState,
ReadyEventData,
ReadyGuildDTO,
ReadyUserGuildSettingsEntries,
Recipient,
Session,
SessionsReplace,
UserSettings,
checkToken,
emitEvent,
} from "@spacebar/util";
import { check } from "./instanceOf";
// TODO: user sharding
// TODO: check privileged intents, if defined in the config
const tryGetUserFromToken = async (...args: Parameters<typeof checkToken>) => {
try {
return (await checkToken(...args)).user;
} catch (e) {
return null;
}
};
export async function onIdentify(this: WebSocket, data: Payload) {
if (this.user_id) {
// we've already identified
@@ -74,7 +82,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.capabilities = new Capabilities(identify.capabilities || 0);
const { user } = await checkToken(identify.token, {
const user = await tryGetUserFromToken(identify.token, {
relations: ["relationships", "relationships.to", "settings"],
select: [...PrivateUserProjection, "relationships"],
});
@@ -332,10 +340,9 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// TODO how is active determined?
// in our lazy request impl, we just pick the 'most relevant' session
active: x.session_id == session.session_id,
activities: x.activities,
activities: x.activities ?? [],
client_info: x.client_info,
// TODO: what does all mean?
session_id: x.session_id == session.session_id ? "all" : x.session_id,
session_id: x.session_id, // TODO: discord.com sends 'all', what is that???
status: x.status,
}));
+4 -3
View File
@@ -102,10 +102,11 @@ export class Channel extends BaseClass {
guild_id?: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.channels, {
onDelete: "CASCADE",
nullable: true,
})
guild: Guild;
guild?: Guild;
@Column({ nullable: true })
@RelationId((channel: Channel) => channel.parent)
@@ -571,7 +572,6 @@ export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
export function isTextChannel(type: ChannelType): boolean {
switch (type) {
case ChannelType.GUILD_STORE:
case ChannelType.GUILD_VOICE:
case ChannelType.GUILD_STAGE_VOICE:
case ChannelType.GUILD_CATEGORY:
case ChannelType.GUILD_FORUM:
@@ -580,6 +580,7 @@ export function isTextChannel(type: ChannelType): boolean {
case ChannelType.DM:
case ChannelType.GROUP_DM:
case ChannelType.GUILD_NEWS:
case ChannelType.GUILD_VOICE:
case ChannelType.GUILD_NEWS_THREAD:
case ChannelType.GUILD_PUBLIC_THREAD:
case ChannelType.GUILD_PRIVATE_THREAD:
+1 -1
View File
@@ -33,7 +33,7 @@ export class Emoji extends BaseClass {
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.emojis, {
onDelete: "CASCADE",
})
guild: Guild;
+1 -1
View File
@@ -53,7 +53,7 @@ export class Invite extends BaseClassWithoutId {
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.invites, {
onDelete: "CASCADE",
})
guild: Guild;
+1
View File
@@ -327,6 +327,7 @@ export class Member extends BaseClassWithoutId {
id: guild_id,
},
relations: PublicGuildRelations,
relationLoadStrategy: "query",
});
const memberCount = await Member.count({ where: { guild_id } });
+2 -2
View File
@@ -23,12 +23,12 @@ import { Guild } from "./Guild";
@Entity("roles")
export class Role extends BaseClass {
@Column({ nullable: true })
@Column()
@RelationId((role: Role) => role.guild)
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.roles, {
onDelete: "CASCADE",
})
guild: Guild;
+2 -2
View File
@@ -17,9 +17,9 @@
*/
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { User } from "./User";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { User } from "./User";
export enum StickerType {
STANDARD = 1,
@@ -62,7 +62,7 @@ export class Sticker extends BaseClass {
guild_id?: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.stickers, {
onDelete: "CASCADE",
})
guild?: Guild;
+2 -2
View File
@@ -20,8 +20,8 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
import { User } from "./User";
import { Member } from "./Member";
import { User } from "./User";
//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex
@Entity("voice_states")
@@ -31,7 +31,7 @@ export class VoiceState extends BaseClass {
guild_id: string;
@JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, {
@ManyToOne(() => Guild, (guild) => guild.voice_states, {
onDelete: "CASCADE",
})
guild?: Guild;
+6 -6
View File
@@ -16,12 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DataSource } from "typeorm";
import { yellow, green, red } from "picocolors";
import { Migration } from "../entities/Migration";
import { ConfigEntity } from "../entities/Config";
import { config } from "dotenv";
import path from "path";
import { green, red, yellow } from "picocolors";
import { DataSource } from "typeorm";
import { ConfigEntity } from "../entities/Config";
import { Migration } from "../entities/Migration";
// UUID extension option is only supported with postgres
// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
@@ -50,7 +50,7 @@ const DataSourceOptions = new DataSource({
database: isSqlite ? dbConnectionString : undefined,
entities: [path.join(__dirname, "..", "entities", "*.js")],
synchronize: !!process.env.DB_SYNC,
logging: false,
logging: !!process.env.DB_LOGGING,
bigNumberStrings: false,
supportBigNumbers: true,
name: "default",
@@ -129,7 +129,7 @@ export async function initDatabase(): Promise<DataSource> {
return dbConnection;
}
export { dbConnection, DataSourceOptions, DatabaseType };
export { DataSourceOptions, DatabaseType, dbConnection };
export async function closeDatabase() {
await dbConnection?.destroy();
+23 -12
View File
@@ -16,13 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Config } from "./Config";
import { yellow } from "picocolors";
import { Config } from "./Config";
import express from "express";
import * as SentryNode from "@sentry/node";
import * as Tracing from "@sentry/tracing";
import * as Integrations from "@sentry/integrations";
import * as SentryNode from "@sentry/node";
import express from "express";
// Work around for when bundle calls api/etc
let errorHandlersUsed = false;
@@ -46,16 +45,28 @@ export const Sentry = {
);
}
const integrations = [
new SentryNode.Integrations.Http({ tracing: true }),
new Integrations.RewriteFrames({
root: __dirname,
}),
new SentryNode.Integrations.Http({
tracing: true,
breadcrumbs: true,
}),
...SentryNode.autoDiscoverNodePerformanceMonitoringIntegrations(),
];
if (app)
integrations.push(
new SentryNode.Integrations.Express({
app,
}),
);
SentryNode.init({
dsn: endpoint,
integrations: [
new SentryNode.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app }),
new Tracing.Integrations.Mysql(),
new Integrations.RewriteFrames({
root: __dirname,
}),
],
integrations,
tracesSampleRate: traceSampleRate, // naming?
environment,
});