diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts
new file mode 100644
index 00000000..02104d39
--- /dev/null
+++ b/src/util/connections/Connection.ts
@@ -0,0 +1,72 @@
+import crypto from "crypto";
+import { ConnectedAccount } from "../entities";
+import { OrmUtils } from "../imports";
+import { ConnectionCallbackSchema } from "../schemas";
+import { DiscordApiErrors } from "../util";
+
+export default abstract class Connection {
+ id: string;
+ settings: { enabled: boolean };
+ states: Map<string, string> = new Map();
+
+ abstract init(): void;
+
+ /**
+ * Generates an authorization url for the connection.
+ * @param args
+ */
+ abstract getAuthorizationUrl(userId: string): string;
+
+ /**
+ * Processes the callback
+ * @param args Callback arguments
+ */
+ abstract handleCallback(params: ConnectionCallbackSchema): Promise<boolean>;
+
+ /**
+ * Gets a user id from state
+ * @param state the state to get the user id from
+ * @returns the user id associated with the state
+ */
+ getUserId(state: string): string {
+ if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE;
+ return this.states.get(state) as string;
+ }
+
+ /**
+ * Generates a state
+ * @param user_id The user id to generate a state for.
+ * @returns a new state
+ */
+ createState(userId: string): string {
+ const state = crypto.randomBytes(16).toString("hex");
+ this.states.set(state, userId);
+
+ return state;
+ }
+
+ /**
+ * Takes a state and checks if it is valid, and deletes it.
+ * @param state The state to check.
+ */
+ validateState(state: string): void {
+ if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE;
+ this.states.delete(state);
+ }
+
+ async createConnection(data: any): Promise<void> {
+ const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data);
+ await ca.save();
+ }
+
+ async hasConnection(userId: string, externalId: string): Promise<boolean> {
+ const existing = await ConnectedAccount.findOne({
+ where: {
+ user_id: userId,
+ external_id: externalId,
+ },
+ });
+
+ return !!existing;
+ }
+}
diff --git a/src/util/connections/ConnectionConfig.ts b/src/util/connections/ConnectionConfig.ts
new file mode 100644
index 00000000..9b120c93
--- /dev/null
+++ b/src/util/connections/ConnectionConfig.ts
@@ -0,0 +1,79 @@
+import { ConnectionConfigEntity } from "../entities/ConnectionConfigEntity";
+
+let config: any;
+let pairs: ConnectionConfigEntity[];
+
+export const ConnectionConfig = {
+ init: async function init() {
+ if (config) return config;
+ console.log("[ConnectionConfig] Loading configuration...");
+ pairs = await ConnectionConfigEntity.find();
+ config = pairsToConfig(pairs);
+
+ return this.set(config);
+ },
+ get: function get() {
+ if (!config) {
+ return {};
+ }
+ return config;
+ },
+ set: function set(val: Partial<any>) {
+ if (!config || !val) return;
+ config = val.merge(config);
+ console.debug("config", config); // TODO: if no more issues with sql, remove this or find the reason why it's happening
+
+ return applyConfig(config);
+ },
+};
+
+function applyConfig(val: any) {
+ async function apply(obj: any, key = ""): Promise<any> {
+ if (typeof obj === "object" && obj !== null && !(obj instanceof Date))
+ return Promise.all(
+ Object.keys(obj).map((k) =>
+ apply(obj[k], key ? `${key}_${k}` : k),
+ ),
+ );
+
+ let pair = pairs.find((x) => x.key === key);
+ if (!pair) pair = new ConnectionConfigEntity();
+
+ pair.key = key;
+
+ if (pair.value !== obj) {
+ pair.value = obj;
+ if (!pair.key || pair.key == null) {
+ console.log(`[ConnectionConfig] WARN: Empty key`);
+ console.log(pair);
+ } else return pair.save();
+ }
+ }
+
+ return apply(val);
+}
+
+function pairsToConfig(pairs: ConnectionConfigEntity[]) {
+ let value: any = {};
+
+ pairs.forEach((p) => {
+ const keys = p.key.split("_");
+ let obj = value;
+ let prev = "";
+ let prevObj = obj;
+ let i = 0;
+
+ for (const key of keys) {
+ if (!isNaN(Number(key)) && !prevObj[prev]?.length)
+ prevObj[prev] = obj = [];
+ if (i++ === keys.length - 1) obj[key] = p.value;
+ else if (!obj[key]) obj[key] = {};
+
+ prev = key;
+ prevObj = obj;
+ obj = obj[key];
+ }
+ });
+
+ return value;
+}
diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts
new file mode 100644
index 00000000..d06a6446
--- /dev/null
+++ b/src/util/connections/ConnectionLoader.ts
@@ -0,0 +1,65 @@
+import fs from "fs";
+import path from "path";
+import { OrmUtils } from "../imports";
+import Connection from "./Connection";
+import { ConnectionConfig } from "./ConnectionConfig";
+import { ConnectionStore } from "./ConnectionStore";
+
+const root = "dist/connections";
+let connectionsLoaded = false;
+
+export class ConnectionLoader {
+ public static async loadConnections() {
+ if (connectionsLoaded) return;
+ ConnectionConfig.init();
+ const dirs = fs.readdirSync(root).filter((x) => {
+ try {
+ fs.readdirSync(path.join(root, x));
+ return true;
+ } catch (e) {
+ return false;
+ }
+ });
+
+ dirs.forEach(async (x) => {
+ let modPath = path.resolve(path.join(root, x));
+ console.log(`Loading connection: ${modPath}`);
+ const mod = new (require(modPath).default)() as Connection;
+ ConnectionStore.connections.set(mod.id, mod);
+
+ mod.init();
+ console.log(`[Connections] Loaded connection '${mod.id}'`);
+ });
+ }
+
+ public static getConnectionConfig(id: string, defaults?: any): any {
+ let cfg = ConnectionConfig.get()[id];
+ if (defaults) {
+ if (cfg) cfg = OrmUtils.mergeDeep(defaults, cfg);
+ else cfg = defaults;
+ this.setConnectionConfig(id, cfg);
+ }
+
+ if (!cfg)
+ console.log(
+ `[ConnectionConfig/WARN] Getting connection settings for '${id}' returned null! (Did you forget to add settings?)`,
+ );
+ return cfg;
+ }
+
+ public static async setConnectionConfig(
+ id: string,
+ config: Partial<any>,
+ ): Promise<void> {
+ if (!config)
+ console.log(
+ `[ConnectionConfig/WARN] ${id} tried to set config=null!`,
+ );
+ await ConnectionConfig.set({
+ [id]: OrmUtils.mergeDeep(
+ ConnectionLoader.getConnectionConfig(id) || {},
+ config,
+ ),
+ });
+ }
+}
diff --git a/src/util/connections/ConnectionStore.ts b/src/util/connections/ConnectionStore.ts
new file mode 100644
index 00000000..406e8232
--- /dev/null
+++ b/src/util/connections/ConnectionStore.ts
@@ -0,0 +1,5 @@
+import Connection from "./Connection";
+
+export class ConnectionStore {
+ public static connections: Map<string, Connection> = new Map();
+}
diff --git a/src/util/connections/index.ts b/src/util/connections/index.ts
new file mode 100644
index 00000000..e15d0c8c
--- /dev/null
+++ b/src/util/connections/index.ts
@@ -0,0 +1,4 @@
+export * from "./Connection";
+export * from "./ConnectionConfig";
+export * from "./ConnectionLoader";
+export * from "./ConnectionStore";
diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts
new file mode 100644
index 00000000..a0287086
--- /dev/null
+++ b/src/util/dtos/ConnectedAccountDTO.ts
@@ -0,0 +1,41 @@
+import { ConnectedAccount } from "../entities";
+
+export class ConnectedAccountDTO {
+ id: string;
+ user_id: string;
+ access_token?: string;
+ friend_sync: boolean;
+ name: string;
+ revoked: boolean;
+ show_activity: boolean;
+ type: string;
+ verified: boolean;
+ visibility: boolean;
+ integrations: string[];
+ metadata_: any;
+ metadata_visibility: boolean;
+ two_way_link: boolean;
+
+ constructor(
+ connectedAccount: ConnectedAccount,
+ with_token: boolean = false,
+ ) {
+ this.id = connectedAccount.external_id;
+ this.user_id = connectedAccount.user_id;
+ this.access_token =
+ connectedAccount.access_token && with_token
+ ? connectedAccount.access_token
+ : undefined;
+ this.friend_sync = connectedAccount.friend_sync;
+ this.name = connectedAccount.name;
+ this.revoked = connectedAccount.revoked;
+ this.show_activity = connectedAccount.show_activity;
+ this.type = connectedAccount.type;
+ this.verified = connectedAccount.verified;
+ this.visibility = connectedAccount.visibility;
+ this.integrations = connectedAccount.integrations;
+ this.metadata_ = connectedAccount.metadata_;
+ this.metadata_visibility = connectedAccount.metadata_visibility;
+ this.two_way_link = connectedAccount.two_way_link;
+ }
+}
diff --git a/src/util/dtos/index.ts b/src/util/dtos/index.ts
index cf3863fa..b91889ff 100644
--- a/src/util/dtos/index.ts
+++ b/src/util/dtos/index.ts
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+export * from "./ConnectedAccountDTO";
export * from "./DmChannelDTO";
export * from "./ReadyGuildDTO";
export * from "./UserDTO";
diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
index 9f0ce35e..70923d2c 100644
--- a/src/util/entities/ConnectedAccount.ts
+++ b/src/util/entities/ConnectedAccount.ts
@@ -27,6 +27,9 @@ export type PublicConnectedAccount = Pick<
@Entity("connected_accounts")
export class ConnectedAccount extends BaseClass {
+ @Column()
+ external_id: string;
+
@Column({ nullable: true })
@RelationId((account: ConnectedAccount) => account.user)
user_id: string;
@@ -41,16 +44,16 @@ export class ConnectedAccount extends BaseClass {
access_token: string;
@Column({ select: false })
- friend_sync: boolean;
+ friend_sync: boolean = false;
@Column()
name: string;
@Column({ select: false })
- revoked: boolean;
+ revoked: boolean = false;
@Column({ select: false })
- show_activity: boolean;
+ show_activity: boolean = true;
@Column()
type: string;
@@ -59,5 +62,17 @@ export class ConnectedAccount extends BaseClass {
verified: boolean;
@Column({ select: false })
- visibility: number;
+ visibility: boolean = true;
+
+ @Column({ type: "simple-array" })
+ integrations: string[];
+
+ @Column({ type: "simple-json", name: "metadata" })
+ metadata_: any;
+
+ @Column()
+ metadata_visibility: boolean = true;
+
+ @Column()
+ two_way_link: boolean = false;
}
diff --git a/src/util/entities/ConnectionConfigEntity.ts b/src/util/entities/ConnectionConfigEntity.ts
new file mode 100644
index 00000000..9c212b15
--- /dev/null
+++ b/src/util/entities/ConnectionConfigEntity.ts
@@ -0,0 +1,11 @@
+import { Column, Entity } from "typeorm";
+import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
+
+@Entity("connection_config")
+export class ConnectionConfigEntity extends BaseClassWithoutId {
+ @PrimaryIdColumn()
+ key: string;
+
+ @Column({ type: "simple-json", nullable: true })
+ value: number | boolean | null | string | Date | undefined;
+}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index 6dfbd822..ad34f67b 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -27,6 +27,7 @@ export * from "./Channel";
export * from "./ClientRelease";
export * from "./Config";
export * from "./ConnectedAccount";
+export * from "./ConnectionConfigEntity";
export * from "./EmbedCache";
export * from "./Emoji";
export * from "./Encryption";
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index c3bfbf9b..6609c9ee 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -420,6 +420,10 @@ export interface UserDeleteEvent extends Event {
};
}
+export interface UserConnectionsUpdateEvent extends Event {
+ event: "USER_CONNECTIONS_UPDATE";
+}
+
export interface VoiceStateUpdateEvent extends Event {
event: "VOICE_STATE_UPDATE";
data: VoiceState & {
@@ -561,6 +565,7 @@ export type EventData =
| TypingStartEvent
| UserUpdateEvent
| UserDeleteEvent
+ | UserConnectionsUpdateEvent
| VoiceStateUpdateEvent
| VoiceServerUpdateEvent
| WebhooksUpdateEvent
@@ -612,6 +617,7 @@ export enum EVENTEnum {
TypingStart = "TYPING_START",
UserUpdate = "USER_UPDATE",
UserDelete = "USER_DELETE",
+ UserConnectionsUpdate = "USER_CONNECTIONS_UPDATE",
WebhooksUpdate = "WEBHOOKS_UPDATE",
InteractionCreate = "INTERACTION_CREATE",
VoiceStateUpdate = "VOICE_STATE_UPDATE",
@@ -663,6 +669,7 @@ export type EVENT =
| "TYPING_START"
| "USER_UPDATE"
| "USER_DELETE"
+ | "USER_CONNECTIONS_UPDATE"
| "USER_NOTE_UPDATE"
| "WEBHOOKS_UPDATE"
| "INTERACTION_CREATE"
diff --git a/src/util/schemas/ConnectionCallbackSchema.ts b/src/util/schemas/ConnectionCallbackSchema.ts
new file mode 100644
index 00000000..09ae8a46
--- /dev/null
+++ b/src/util/schemas/ConnectionCallbackSchema.ts
@@ -0,0 +1,7 @@
+export interface ConnectionCallbackSchema {
+ code?: string;
+ state: string;
+ insecure: boolean;
+ friend_sync: boolean;
+ openid_params?: any; // TODO: types
+}
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 44909a3a..2831d42a 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -30,6 +30,7 @@ export * from "./ChannelModifySchema";
export * from "./ChannelPermissionOverwriteSchema";
export * from "./ChannelReorderSchema";
export * from "./CodesVerificationSchema";
+export * from "./ConnectionCallbackSchema";
export * from "./DmChannelCreateSchema";
export * from "./EmojiCreateSchema";
export * from "./EmojiModifySchema";
|