add connections
This commit is contained in:
parent
ea89f62ccb
commit
21bfda32e4
@ -2790,6 +2790,608 @@
|
|||||||
},
|
},
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$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": {
|
"DmChannelCreateSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -52,6 +52,8 @@ export const NO_AUTHORIZATION_ROUTES = [
|
|||||||
"/oauth2/callback",
|
"/oauth2/callback",
|
||||||
// Asset delivery
|
// Asset delivery
|
||||||
/\/guilds\/\d+\/widget\.(json|png)/,
|
/\/guilds\/\d+\/widget\.(json|png)/,
|
||||||
|
// Connections
|
||||||
|
/\/connections\/\w+\/callback/
|
||||||
];
|
];
|
||||||
|
|
||||||
export const API_PREFIX = /^\/api(\/v\d+)?/;
|
export const API_PREFIX = /^\/api(\/v\d+)?/;
|
||||||
|
@ -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;
|
35
src/api/routes/connections/#connection_name/authorize.ts
Normal file
35
src/api/routes/connections/#connection_name/authorize.ts
Normal 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;
|
52
src/api/routes/connections/#connection_name/callback.ts
Normal file
52
src/api/routes/connections/#connection_name/callback.ts
Normal 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;
|
@ -16,14 +16,32 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response, Router } from "express";
|
|
||||||
import { route } from "@fosscord/api";
|
import { route } from "@fosscord/api";
|
||||||
|
import { ConnectedAccount, ConnectedAccountDTO } from "@fosscord/util";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
|
||||||
const router: Router = Router();
|
const router: Router = Router();
|
||||||
|
|
||||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
//TODO
|
const connections = await ConnectedAccount.find({
|
||||||
res.json([]).status(200);
|
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;
|
export default router;
|
||||||
|
5
src/connections/BattleNet/BattleNetSettings.ts
Normal file
5
src/connections/BattleNet/BattleNetSettings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class BattleNetSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
133
src/connections/BattleNet/index.ts
Normal file
133
src/connections/BattleNet/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
5
src/connections/GitHub/GitHubSettings.ts
Normal file
5
src/connections/GitHub/GitHubSettings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class GitHubSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
114
src/connections/GitHub/index.ts
Normal file
114
src/connections/GitHub/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@ import {
|
|||||||
ReadyGuildDTO,
|
ReadyGuildDTO,
|
||||||
Guild,
|
Guild,
|
||||||
UserTokenData,
|
UserTokenData,
|
||||||
|
ConnectedAccount,
|
||||||
} from "@fosscord/util";
|
} from "@fosscord/util";
|
||||||
import { Send } from "../util/Send";
|
import { Send } from "../util/Send";
|
||||||
import { CLOSECODES, OPCODES } from "../util/Constants";
|
import { CLOSECODES, OPCODES } from "../util/Constants";
|
||||||
@ -78,7 +79,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
|||||||
this.user_id = decoded.id;
|
this.user_id = decoded.id;
|
||||||
const session_id = this.session_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([
|
await Promise.all([
|
||||||
User.findOneOrFail({
|
User.findOneOrFail({
|
||||||
where: { id: this.user_id },
|
where: { id: this.user_id },
|
||||||
@ -123,6 +124,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
|||||||
activities: [],
|
activities: [],
|
||||||
}).save(),
|
}).save(),
|
||||||
Application.findOne({ where: { id: this.user_id } }),
|
Application.findOne({ where: { id: this.user_id } }),
|
||||||
|
ConnectedAccount.find({ where: { user_id: this.user_id } })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
||||||
@ -304,7 +306,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
|||||||
private_channels: channels,
|
private_channels: channels,
|
||||||
session_id: session_id,
|
session_id: session_id,
|
||||||
analytics_token: "", // TODO
|
analytics_token: "", // TODO
|
||||||
connected_accounts: [], // TODO
|
connected_accounts,
|
||||||
consents: {
|
consents: {
|
||||||
personalization: {
|
personalization: {
|
||||||
consented: false, // TODO
|
consented: false, // TODO
|
||||||
|
72
src/util/connections/Connection.ts
Normal file
72
src/util/connections/Connection.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
79
src/util/connections/ConnectionConfig.ts
Normal file
79
src/util/connections/ConnectionConfig.ts
Normal 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;
|
||||||
|
}
|
65
src/util/connections/ConnectionLoader.ts
Normal file
65
src/util/connections/ConnectionLoader.ts
Normal 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,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
5
src/util/connections/ConnectionStore.ts
Normal file
5
src/util/connections/ConnectionStore.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Connection from "./Connection";
|
||||||
|
|
||||||
|
export class ConnectionStore {
|
||||||
|
public static connections: Map<string, Connection> = new Map();
|
||||||
|
}
|
4
src/util/connections/index.ts
Normal file
4
src/util/connections/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./Connection";
|
||||||
|
export * from "./ConnectionConfig";
|
||||||
|
export * from "./ConnectionLoader";
|
||||||
|
export * from "./ConnectionStore";
|
41
src/util/dtos/ConnectedAccountDTO.ts
Normal file
41
src/util/dtos/ConnectedAccountDTO.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./ConnectedAccountDTO";
|
||||||
export * from "./DmChannelDTO";
|
export * from "./DmChannelDTO";
|
||||||
export * from "./ReadyGuildDTO";
|
export * from "./ReadyGuildDTO";
|
||||||
export * from "./UserDTO";
|
export * from "./UserDTO";
|
||||||
|
@ -27,6 +27,9 @@ export type PublicConnectedAccount = Pick<
|
|||||||
|
|
||||||
@Entity("connected_accounts")
|
@Entity("connected_accounts")
|
||||||
export class ConnectedAccount extends BaseClass {
|
export class ConnectedAccount extends BaseClass {
|
||||||
|
@Column()
|
||||||
|
external_id: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@RelationId((account: ConnectedAccount) => account.user)
|
@RelationId((account: ConnectedAccount) => account.user)
|
||||||
user_id: string;
|
user_id: string;
|
||||||
@ -41,16 +44,16 @@ export class ConnectedAccount extends BaseClass {
|
|||||||
access_token: string;
|
access_token: string;
|
||||||
|
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
friend_sync: boolean;
|
friend_sync: boolean = false;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
revoked: boolean;
|
revoked: boolean = false;
|
||||||
|
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
show_activity: boolean;
|
show_activity: boolean = true;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
type: string;
|
type: string;
|
||||||
@ -59,5 +62,17 @@ export class ConnectedAccount extends BaseClass {
|
|||||||
verified: boolean;
|
verified: boolean;
|
||||||
|
|
||||||
@Column({ select: false })
|
@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;
|
||||||
}
|
}
|
||||||
|
11
src/util/entities/ConnectionConfigEntity.ts
Normal file
11
src/util/entities/ConnectionConfigEntity.ts
Normal 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;
|
||||||
|
}
|
@ -27,6 +27,7 @@ export * from "./Channel";
|
|||||||
export * from "./ClientRelease";
|
export * from "./ClientRelease";
|
||||||
export * from "./Config";
|
export * from "./Config";
|
||||||
export * from "./ConnectedAccount";
|
export * from "./ConnectedAccount";
|
||||||
|
export * from "./ConnectionConfigEntity";
|
||||||
export * from "./EmbedCache";
|
export * from "./EmbedCache";
|
||||||
export * from "./Emoji";
|
export * from "./Emoji";
|
||||||
export * from "./Encryption";
|
export * from "./Encryption";
|
||||||
|
@ -420,6 +420,10 @@ export interface UserDeleteEvent extends Event {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserConnectionsUpdateEvent extends Event {
|
||||||
|
event: "USER_CONNECTIONS_UPDATE";
|
||||||
|
}
|
||||||
|
|
||||||
export interface VoiceStateUpdateEvent extends Event {
|
export interface VoiceStateUpdateEvent extends Event {
|
||||||
event: "VOICE_STATE_UPDATE";
|
event: "VOICE_STATE_UPDATE";
|
||||||
data: VoiceState & {
|
data: VoiceState & {
|
||||||
@ -561,6 +565,7 @@ export type EventData =
|
|||||||
| TypingStartEvent
|
| TypingStartEvent
|
||||||
| UserUpdateEvent
|
| UserUpdateEvent
|
||||||
| UserDeleteEvent
|
| UserDeleteEvent
|
||||||
|
| UserConnectionsUpdateEvent
|
||||||
| VoiceStateUpdateEvent
|
| VoiceStateUpdateEvent
|
||||||
| VoiceServerUpdateEvent
|
| VoiceServerUpdateEvent
|
||||||
| WebhooksUpdateEvent
|
| WebhooksUpdateEvent
|
||||||
@ -612,6 +617,7 @@ export enum EVENTEnum {
|
|||||||
TypingStart = "TYPING_START",
|
TypingStart = "TYPING_START",
|
||||||
UserUpdate = "USER_UPDATE",
|
UserUpdate = "USER_UPDATE",
|
||||||
UserDelete = "USER_DELETE",
|
UserDelete = "USER_DELETE",
|
||||||
|
UserConnectionsUpdate = "USER_CONNECTIONS_UPDATE",
|
||||||
WebhooksUpdate = "WEBHOOKS_UPDATE",
|
WebhooksUpdate = "WEBHOOKS_UPDATE",
|
||||||
InteractionCreate = "INTERACTION_CREATE",
|
InteractionCreate = "INTERACTION_CREATE",
|
||||||
VoiceStateUpdate = "VOICE_STATE_UPDATE",
|
VoiceStateUpdate = "VOICE_STATE_UPDATE",
|
||||||
@ -663,6 +669,7 @@ export type EVENT =
|
|||||||
| "TYPING_START"
|
| "TYPING_START"
|
||||||
| "USER_UPDATE"
|
| "USER_UPDATE"
|
||||||
| "USER_DELETE"
|
| "USER_DELETE"
|
||||||
|
| "USER_CONNECTIONS_UPDATE"
|
||||||
| "USER_NOTE_UPDATE"
|
| "USER_NOTE_UPDATE"
|
||||||
| "WEBHOOKS_UPDATE"
|
| "WEBHOOKS_UPDATE"
|
||||||
| "INTERACTION_CREATE"
|
| "INTERACTION_CREATE"
|
||||||
|
7
src/util/schemas/ConnectionCallbackSchema.ts
Normal file
7
src/util/schemas/ConnectionCallbackSchema.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface ConnectionCallbackSchema {
|
||||||
|
code?: string;
|
||||||
|
state: string;
|
||||||
|
insecure: boolean;
|
||||||
|
friend_sync: boolean;
|
||||||
|
openid_params?: any; // TODO: types
|
||||||
|
}
|
@ -30,6 +30,7 @@ export * from "./ChannelModifySchema";
|
|||||||
export * from "./ChannelPermissionOverwriteSchema";
|
export * from "./ChannelPermissionOverwriteSchema";
|
||||||
export * from "./ChannelReorderSchema";
|
export * from "./ChannelReorderSchema";
|
||||||
export * from "./CodesVerificationSchema";
|
export * from "./CodesVerificationSchema";
|
||||||
|
export * from "./ConnectionCallbackSchema";
|
||||||
export * from "./DmChannelCreateSchema";
|
export * from "./DmChannelCreateSchema";
|
||||||
export * from "./EmojiCreateSchema";
|
export * from "./EmojiCreateSchema";
|
||||||
export * from "./EmojiModifySchema";
|
export * from "./EmojiModifySchema";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user