add connections

This commit is contained in:
Puyodead1 2022-12-22 10:05:51 -05:00
parent ea89f62ccb
commit 21bfda32e4
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
24 changed files with 1297 additions and 9 deletions

View File

@ -2790,6 +2790,608 @@
},
"$schema": "http://json-schema.org/draft-07/schema#"
},
"ConnectionCallbackSchema": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"state": {
"type": "string"
},
"insecure": {
"type": "boolean"
},
"friend_sync": {
"type": "boolean"
},
"openid_params": {}
},
"additionalProperties": false,
"required": [
"friend_sync",
"insecure",
"state"
],
"definitions": {
"ChannelPermissionOverwriteType": {
"enum": [
0,
1,
2
],
"type": "number"
},
"ChannelModifySchema": {
"type": "object",
"properties": {
"name": {
"maxLength": 100,
"type": "string"
},
"type": {
"enum": [
0,
1,
10,
11,
12,
13,
14,
15,
2,
255,
3,
33,
34,
35,
4,
5,
6,
64,
7,
8,
9
],
"type": "number"
},
"topic": {
"type": "string"
},
"icon": {
"type": [
"null",
"string"
]
},
"bitrate": {
"type": "integer"
},
"user_limit": {
"type": "integer"
},
"rate_limit_per_user": {
"type": "integer"
},
"position": {
"type": "integer"
},
"permission_overwrites": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"$ref": "#/definitions/ChannelPermissionOverwriteType"
},
"allow": {
"type": "string"
},
"deny": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"allow",
"deny",
"id",
"type"
]
}
},
"parent_id": {
"type": "string"
},
"id": {
"type": "string"
},
"nsfw": {
"type": "boolean"
},
"rtc_region": {
"type": "string"
},
"default_auto_archive_duration": {
"type": "integer"
},
"default_reaction_emoji": {
"type": [
"null",
"string"
]
},
"flags": {
"type": "integer"
},
"default_thread_rate_limit_per_user": {
"type": "integer"
},
"video_quality_mode": {
"type": "integer"
}
},
"additionalProperties": false
},
"ActivitySchema": {
"type": "object",
"properties": {
"afk": {
"type": "boolean"
},
"status": {
"$ref": "#/definitions/Status"
},
"activities": {
"type": "array",
"items": {
"$ref": "#/definitions/Activity"
}
},
"since": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"status"
]
},
"Status": {
"enum": [
"dnd",
"idle",
"invisible",
"offline",
"online"
],
"type": "string"
},
"Activity": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/ActivityType"
},
"url": {
"type": "string"
},
"created_at": {
"type": "integer"
},
"timestamps": {
"type": "object",
"properties": {
"start": {
"type": "integer"
},
"end": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"end",
"start"
]
},
"application_id": {
"type": "string"
},
"details": {
"type": "string"
},
"state": {
"type": "string"
},
"emoji": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"animated": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"animated",
"name"
]
},
"party": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"size": {
"type": "array",
"items": [
{
"type": "integer"
}
],
"minItems": 1,
"maxItems": 1
}
},
"additionalProperties": false
},
"assets": {
"type": "object",
"properties": {
"large_image": {
"type": "string"
},
"large_text": {
"type": "string"
},
"small_image": {
"type": "string"
},
"small_text": {
"type": "string"
}
},
"additionalProperties": false
},
"secrets": {
"type": "object",
"properties": {
"join": {
"type": "string"
},
"spectate": {
"type": "string"
},
"match": {
"type": "string"
}
},
"additionalProperties": false
},
"instance": {
"type": "boolean"
},
"flags": {
"type": "string"
},
"id": {
"type": "string"
},
"sync_id": {
"type": "string"
},
"metadata": {
"type": "object",
"properties": {
"context_uri": {
"type": "string"
},
"album_id": {
"type": "string"
},
"artist_ids": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"album_id",
"artist_ids"
]
},
"session_id": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"flags",
"name",
"session_id",
"type"
]
},
"ActivityType": {
"enum": [
0,
1,
2,
4,
5
],
"type": "number"
},
"Record<string,[number,number][]>": {
"type": "object",
"additionalProperties": false
},
"Embed": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"type": {
"enum": [
"article",
"gifv",
"image",
"link",
"rich",
"video"
],
"type": "string"
},
"description": {
"type": "string"
},
"url": {
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"color": {
"type": "integer"
},
"footer": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"icon_url": {
"type": "string"
},
"proxy_icon_url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"text"
]
},
"image": {
"$ref": "#/definitions/EmbedImage"
},
"thumbnail": {
"$ref": "#/definitions/EmbedImage"
},
"video": {
"$ref": "#/definitions/EmbedImage"
},
"provider": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"icon_url": {
"type": "string"
},
"proxy_icon_url": {
"type": "string"
}
},
"additionalProperties": false
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
},
"inline": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"name",
"value"
]
}
}
},
"additionalProperties": false
},
"EmbedImage": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"proxy_url": {
"type": "string"
},
"height": {
"type": "integer"
},
"width": {
"type": "integer"
}
},
"additionalProperties": false
},
"Partial<ChannelOverride>": {
"type": "object",
"properties": {
"message_notifications": {
"type": "integer"
},
"mute_config": {
"$ref": "#/definitions/MuteConfig"
},
"muted": {
"type": "boolean"
},
"channel_id": {
"type": [
"null",
"string"
]
}
},
"additionalProperties": false
},
"MuteConfig": {
"type": "object",
"properties": {
"end_time": {
"type": "integer"
},
"selected_time_window": {
"type": "integer"
}
},
"additionalProperties": false,
"required": [
"end_time",
"selected_time_window"
]
},
"CustomStatus": {
"type": "object",
"properties": {
"emoji_id": {
"type": "string"
},
"emoji_name": {
"type": "string"
},
"expires_at": {
"type": "integer"
},
"text": {
"type": "string"
}
},
"additionalProperties": false
},
"FriendSourceFlags": {
"type": "object",
"properties": {
"all": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"all"
]
},
"GuildFolder": {
"type": "object",
"properties": {
"color": {
"type": "integer"
},
"guild_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"color",
"guild_ids",
"id",
"name"
]
},
"Partial<GenerateWebAuthnCredentialsSchema>": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
},
"additionalProperties": false
},
"Partial<CreateWebAuthnCredentialSchema>": {
"type": "object",
"properties": {
"credential": {
"type": "string"
},
"name": {
"type": "string"
},
"ticket": {
"type": "string"
}
},
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
},
"DmChannelCreateSchema": {
"type": "object",
"properties": {

View File

@ -52,6 +52,8 @@ export const NO_AUTHORIZATION_ROUTES = [
"/oauth2/callback",
// Asset delivery
/\/guilds\/\d+\/widget\.(json|png)/,
// Connections
/\/connections\/\w+\/callback/
];
export const API_PREFIX = /^\/api(\/v\d+)?/;

View File

@ -0,0 +1,11 @@
import { route } from "@fosscord/api";
import { Request, Response, Router } from "express";
const router = Router();
router.post("/", route({}), async (req: Request, res: Response) => {
// TODO:
const { connection_name, connection_id } = req.params;
res.sendStatus(204);
});
export default router;

View File

@ -0,0 +1,35 @@
import { Request, Response, Router } from "express";
import { FieldErrors } from "../../../../util";
import { ConnectionStore } from "../../../../util/connections";
import { route } from "../../../util";
const router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { connection_id: connection_name } = req.params;
const connection = ConnectionStore.connections.get(connection_name);
if (!connection)
throw FieldErrors({
provider_id: {
code: "BASE_TYPE_CHOICES",
message: req.t("common:field.BASE_TYPE_CHOICES", {
types: Array.from(ConnectionStore.connections.keys()).join(
", ",
),
}),
},
});
if (!connection.settings.enabled)
throw FieldErrors({
provider_id: {
message: "This connection has been disabled server-side.",
},
});
res.json({
url: await connection.getAuthorizationUrl(req.user_id),
});
});
export default router;

View File

@ -0,0 +1,52 @@
import { Request, Response, Router } from "express";
import {
ConnectionCallbackSchema,
emitEvent,
FieldErrors,
} from "../../../../util";
import { ConnectionStore } from "../../../../util/connections";
import { route } from "../../../util";
const router = Router();
router.post(
"/",
route({ body: "ConnectionCallbackSchema" }),
async (req: Request, res: Response) => {
const { connection_id: connection_name } = req.params;
const connection = ConnectionStore.connections.get(connection_name);
if (!connection)
throw FieldErrors({
provider_id: {
code: "BASE_TYPE_CHOICES",
message: req.t("common:field.BASE_TYPE_CHOICES", {
types: Array.from(
ConnectionStore.connections.keys(),
).join(", "),
}),
},
});
if (!connection.settings.enabled)
throw FieldErrors({
provider_id: {
message: "This connection has been disabled server-side.",
},
});
const body = req.body as ConnectionCallbackSchema;
const userId = connection.getUserId(body.state);
const emit = await connection.handleCallback(body);
// whether we should emit a connections update event, only used when a connection doesnt already exist
if (emit)
emitEvent({
event: "USER_CONNECTIONS_UPDATE",
data: {},
user_id: userId,
});
res.sendStatus(204);
},
);
export default router;

View File

@ -16,14 +16,32 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
import { ConnectedAccount, ConnectedAccountDTO } from "@fosscord/util";
import { Request, Response, Router } from "express";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
//TODO
res.json([]).status(200);
const connections = await ConnectedAccount.find({
where: {
user_id: req.user_id,
},
select: [
"external_id",
"type",
"name",
"verified",
"visibility",
"show_activity",
"revoked",
"access_token",
"friend_sync",
"integrations",
],
});
res.json(connections.map((x) => new ConnectedAccountDTO(x, true)));
});
export default router;

View File

@ -0,0 +1,5 @@
export class BattleNetSettings {
enabled: boolean = false;
clientId: string | null = null;
clientSecret: string | null = null;
}

View File

@ -0,0 +1,133 @@
import fetch from "node-fetch";
import { Config, ConnectionCallbackSchema, DiscordApiErrors } from "../../util";
import Connection from "../../util/connections/Connection";
import { ConnectionLoader } from "../../util/connections/ConnectionLoader";
import { BattleNetSettings } from "./BattleNetSettings";
interface OAuthTokenResponse {
access_token: string;
token_type: string;
scope: string;
refresh_token?: string;
expires_in?: number;
}
interface BattleNetConnectionUser {
sub: string;
id: number;
battletag: string;
}
interface BattleNetErrorResponse {
error: string;
error_description: string;
}
export default class BattleNetConnection extends Connection {
public readonly id = "battlenet";
public readonly authorizeUrl = "https://oauth.battle.net/authorize";
public readonly tokenUrl = "https://oauth.battle.net/token";
public readonly userInfoUrl = "https://us.battle.net/oauth/userinfo";
public readonly scopes = [];
settings: BattleNetSettings = new BattleNetSettings();
init(): void {
this.settings = ConnectionLoader.getConnectionConfig(
this.id,
this.settings,
) as BattleNetSettings;
}
getAuthorizationUrl(userId: string): string {
const state = this.createState(userId);
const url = new URL(this.authorizeUrl);
url.searchParams.append("client_id", this.settings.clientId!);
// TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting.
url.searchParams.append(
"redirect_uri",
`${
Config.get().cdn.endpointPrivate || "http://localhost:3001"
}/connections/${this.id}/callback`,
);
url.searchParams.append("scope", this.scopes.join(" "));
url.searchParams.append("state", state);
url.searchParams.append("response_type", "code");
return url.toString();
}
getTokenUrl(): string {
return this.tokenUrl;
}
async exchangeCode(state: string, code: string): Promise<string> {
this.validateState(state);
const url = this.getTokenUrl();
return fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: this.settings.clientId!,
client_secret: this.settings.clientSecret!,
redirect_uri: `${
Config.get().cdn.endpointPrivate || "http://localhost:3001"
}/connections/${this.id}/callback`,
}),
})
.then((res) => res.json())
.then((res: OAuthTokenResponse & BattleNetErrorResponse) => {
if (res.error) throw new Error(res.error_description);
return res.access_token;
})
.catch((e) => {
console.error(
`Error exchanging token for ${this.id} connection: ${e}`,
);
throw DiscordApiErrors.INVALID_OAUTH_TOKEN;
});
}
async getUser(token: string): Promise<BattleNetConnectionUser> {
const url = new URL(this.userInfoUrl);
return fetch(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => res.json())
.then((res: BattleNetConnectionUser & BattleNetErrorResponse) => {
if (res.error) throw new Error(res.error_description);
return res;
});
}
async handleCallback(params: ConnectionCallbackSchema): Promise<boolean> {
const userId = this.getUserId(params.state);
const token = await this.exchangeCode(params.state, params.code!);
const userInfo = await this.getUser(token);
const exists = await this.hasConnection(userId, userInfo.id.toString());
if (exists) return false;
await this.createConnection({
user_id: userId,
external_id: userInfo.id,
friend_sync: params.friend_sync,
name: userInfo.battletag,
revoked: false,
show_activity: false,
type: this.id,
verified: true,
visibility: 0,
integrations: [],
});
return true;
}
}

View File

@ -0,0 +1,5 @@
export class GitHubSettings {
enabled: boolean = false;
clientId: string | null = null;
clientSecret: string | null = null;
}

View File

@ -0,0 +1,114 @@
import fetch from "node-fetch";
import { Config, ConnectionCallbackSchema, DiscordApiErrors } from "../../util";
import Connection from "../../util/connections/Connection";
import { ConnectionLoader } from "../../util/connections/ConnectionLoader";
import { GitHubSettings } from "./GitHubSettings";
interface OAuthTokenResponse {
access_token: string;
token_type: string;
scope: string;
refresh_token?: string;
expires_in?: number;
}
interface UserResponse {
login: string;
id: number;
name: string;
}
export default class GitHubConnection extends Connection {
public readonly id = "github";
public readonly authorizeUrl = "https://github.com/login/oauth/authorize";
public readonly tokenUrl = "https://github.com/login/oauth/access_token";
public readonly userInfoUrl = "https://api.github.com/user";
public readonly scopes = ["read:user"];
settings: GitHubSettings = new GitHubSettings();
init(): void {
this.settings = ConnectionLoader.getConnectionConfig(
this.id,
this.settings,
) as GitHubSettings;
}
getAuthorizationUrl(userId: string): string {
const state = this.createState(userId);
const url = new URL(this.authorizeUrl);
url.searchParams.append("client_id", this.settings.clientId!);
// TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting.
url.searchParams.append(
"redirect_uri",
`${
Config.get().cdn.endpointPrivate || "http://localhost:3001"
}/connections/${this.id}/callback`,
);
url.searchParams.append("scope", this.scopes.join(" "));
url.searchParams.append("state", state);
return url.toString();
}
getTokenUrl(code: string): string {
const url = new URL(this.tokenUrl);
url.searchParams.append("client_id", this.settings.clientId!);
url.searchParams.append("client_secret", this.settings.clientSecret!);
url.searchParams.append("code", code);
return url.toString();
}
async exchangeCode(state: string, code: string): Promise<string> {
this.validateState(state);
const url = this.getTokenUrl(code);
return fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
},
})
.then((res) => res.json())
.then((res: OAuthTokenResponse) => res.access_token)
.catch((e) => {
console.error(
`Error exchanging token for ${this.id} connection: ${e}`,
);
throw DiscordApiErrors.INVALID_OAUTH_TOKEN;
});
}
async getUser(token: string): Promise<UserResponse> {
const url = new URL(this.userInfoUrl);
return fetch(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => res.json());
}
async handleCallback(params: ConnectionCallbackSchema): Promise<boolean> {
const userId = this.getUserId(params.state);
const token = await this.exchangeCode(params.state, params.code!);
const userInfo = await this.getUser(token);
const exists = await this.hasConnection(userId, userInfo.id.toString());
if (exists) return false;
await this.createConnection({
user_id: userId,
external_id: userInfo.id,
friend_sync: params.friend_sync,
name: userInfo.name,
revoked: false,
show_activity: false,
type: this.id,
verified: true,
visibility: 0,
integrations: [],
});
return true;
}
}

View File

@ -43,6 +43,7 @@ import {
ReadyGuildDTO,
Guild,
UserTokenData,
ConnectedAccount,
} from "@fosscord/util";
import { Send } from "../util/Send";
import { CLOSECODES, OPCODES } from "../util/Constants";
@ -78,7 +79,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.user_id = decoded.id;
const session_id = this.session_id;
const [user, read_states, members, recipients, session, application] =
const [user, read_states, members, recipients, session, application, connected_accounts] =
await Promise.all([
User.findOneOrFail({
where: { id: this.user_id },
@ -123,6 +124,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
activities: [],
}).save(),
Application.findOne({ where: { id: this.user_id } }),
ConnectedAccount.find({ where: { user_id: this.user_id } })
]);
if (!user) return this.close(CLOSECODES.Authentication_failed);
@ -304,7 +306,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
private_channels: channels,
session_id: session_id,
analytics_token: "", // TODO
connected_accounts: [], // TODO
connected_accounts,
consents: {
personalization: {
consented: false, // TODO

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
),
});
}
}

View File

@ -0,0 +1,5 @@
import Connection from "./Connection";
export class ConnectionStore {
public static connections: Map<string, Connection> = new Map();
}

View File

@ -0,0 +1,4 @@
export * from "./Connection";
export * from "./ConnectionConfig";
export * from "./ConnectionLoader";
export * from "./ConnectionStore";

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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"

View File

@ -0,0 +1,7 @@
export interface ConnectionCallbackSchema {
code?: string;
state: string;
insecure: boolean;
friend_sync: boolean;
openid_params?: any; // TODO: types
}

View File

@ -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";