Merge branch 'master' into feat/refactorIdentify
This commit is contained in:
commit
5a6cb33f5e
@ -7,5 +7,8 @@
|
|||||||
"no-mixed-spaces-and-tabs": "off",
|
"no-mixed-spaces-and-tabs": "off",
|
||||||
"@typescript-eslint/no-inferrable-types": "off", // Required by typeorm
|
"@typescript-eslint/no-inferrable-types": "off", // Required by typeorm
|
||||||
"@typescript-eslint/no-var-requires": "off" // Sometimes requred by typeorm to resolve circular deps
|
"@typescript-eslint/no-var-requires": "off" // Sometimes requred by typeorm to resolve circular deps
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -24,7 +24,5 @@ jobs:
|
|||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npx eslint .
|
|
||||||
- run: npx prettier --check .
|
|
||||||
- run: npm run build --if-present
|
- run: npm run build --if-present
|
||||||
- run: npm run test --if-present
|
- run: npm run test --if-present
|
28
.github/workflows/style.yml
vendored
Normal file
28
.github/workflows/style.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: Style
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "**" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "**" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm i --only=dev
|
||||||
|
- run: npx eslint .
|
||||||
|
- run: npx prettier --check .
|
@ -8,7 +8,7 @@
|
|||||||
<img src="https://img.shields.io/discord/806142446094385153?color=7489d5&logo=discord&logoColor=ffffff" />
|
<img src="https://img.shields.io/discord/806142446094385153?color=7489d5&logo=discord&logoColor=ffffff" />
|
||||||
</a>
|
</a>
|
||||||
<img src="https://img.shields.io/static/v1?label=Status&message=Development&color=blue">
|
<img src="https://img.shields.io/static/v1?label=Status&message=Development&color=blue">
|
||||||
<a title="Crowdin" target="_blank" href="https://translate.spacebar.chat/"><img src="https://badges.crowdin.net/spacebar/localized.svg"></a>
|
<a title="Crowdin" target="_blank" href="https://translate.spacebar.chat/"><img src="https://badges.crowdin.net/fosscord/localized.svg"></a>
|
||||||
<a href="https://opencollective.com/spacebar">
|
<a href="https://opencollective.com/spacebar">
|
||||||
<img src="https://opencollective.com/spacebar/tiers/badge.svg">
|
<img src="https://opencollective.com/spacebar/tiers/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
|
@ -2838,6 +2838,149 @@
|
|||||||
"connected_accounts",
|
"connected_accounts",
|
||||||
"user"
|
"user"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"ConnectedAccountCommonOAuthTokenResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_in": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"access_token",
|
||||||
|
"scope",
|
||||||
|
"token_type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ConnectedAccountTokenData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_in": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"fetched_at": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"access_token",
|
||||||
|
"fetched_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ConnectedAccountSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"external_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_data": {
|
||||||
|
"$ref": "#/components/schemas/ConnectedAccountTokenData"
|
||||||
|
},
|
||||||
|
"friend_sync": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"revoked": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"show_activity": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"verified": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"integrations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata_": {},
|
||||||
|
"metadata_visibility": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"two_way_link": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"external_id",
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ConnectionCallbackSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"insecure": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"friend_sync": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"openid_params": {}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"friend_sync",
|
||||||
|
"insecure",
|
||||||
|
"state"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ConnectionUpdateSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"visibility": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"show_activity": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2934,6 +3077,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "read-states"
|
"name": "read-states"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "connections"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
@ -7561,6 +7707,286 @@
|
|||||||
"auth"
|
"auth"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/users/@me/connections/{connection_name}/{connection_id}/": {
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConnectionUpdateSchema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "connection_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "connection_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/@me/connections/{connection_name}/{connection_id}/access-token/": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "connection_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/guilds/{guild_id}/roles/member-counts/": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "guild_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "guild_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"guilds"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/guilds/{guild_id}/roles/{role_id}/members/": {
|
||||||
|
"patch": {
|
||||||
|
"x-permission-required": "MANAGE_ROLES",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "guild_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "guild_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "role_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"guilds"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/guilds/{guild_id}/roles/{role_id}/member-ids/": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "guild_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "guild_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "role_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"guilds"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/connections/{connection_name}/callback/": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ConnectionCallbackSchema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"connections"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/connections/{connection_name}/authorize/": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"connections"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/connections/{connection_name}/{connection_id}/refresh/": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "connection_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "connection_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "connection_id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"connections"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
4197
assets/schemas.json
4197
assets/schemas.json
File diff suppressed because it is too large
Load Diff
15
package-lock.json
generated
15
package-lock.json
generated
@ -46,11 +46,11 @@
|
|||||||
"probe-image-size": "^7.2.3",
|
"probe-image-size": "^7.2.3",
|
||||||
"proxy-agent": "^5.0.0",
|
"proxy-agent": "^5.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sqlite3": "^5.1.5",
|
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typeorm": "^0.3.10",
|
"typeorm": "^0.3.10",
|
||||||
"typescript-json-schema": "^0.50.1",
|
"typescript-json-schema": "^0.50.1",
|
||||||
|
"wretch": "^2.3.2",
|
||||||
"ws": "^8.9.0"
|
"ws": "^8.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -7989,6 +7989,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/wretch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wretch/-/wretch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-brN97Z2Mwed+w5z+keYI1u5OwWhPIaW0sJi9CxtKBVxLc3aqP6j1+2FCoIskM7WJq6SUHdxTFx20ox0iDLa0mQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.11.0",
|
"version": "8.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||||
@ -14170,6 +14178,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"wretch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wretch/-/wretch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-brN97Z2Mwed+w5z+keYI1u5OwWhPIaW0sJi9CxtKBVxLc3aqP6j1+2FCoIskM7WJq6SUHdxTFx20ox0iDLa0mQ=="
|
||||||
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.11.0",
|
"version": "8.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||||
|
@ -105,6 +105,7 @@
|
|||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typeorm": "^0.3.10",
|
"typeorm": "^0.3.10",
|
||||||
"typescript-json-schema": "^0.50.1",
|
"typescript-json-schema": "^0.50.1",
|
||||||
|
"wretch": "^2.3.2",
|
||||||
"ws": "^8.9.0"
|
"ws": "^8.9.0"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
|
@ -26,8 +26,6 @@
|
|||||||
This grabs the new changelog from `spacebarchat/server/assets/changelog.txt`
|
This grabs the new changelog from `spacebarchat/server/assets/changelog.txt`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
const fs = require("fs/promises");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
Does not prepend is file contains @fc-license-skip
|
Does not prepend is file contains @fc-license-skip
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
const Path = require("path");
|
const Path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const walk = require("./util/walk");
|
const walk = require("./util/walk");
|
||||||
|
@ -16,8 +16,6 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
require("module-alias/register");
|
require("module-alias/register");
|
||||||
const getRouteDescriptions = require("./util/getRouteDescriptions");
|
const getRouteDescriptions = require("./util/getRouteDescriptions");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
@ -20,8 +20,6 @@
|
|||||||
Calculates a discord.com-like rights value.
|
Calculates a discord.com-like rights value.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
require("module-alias/register");
|
require("module-alias/register");
|
||||||
const { Rights } = require("..");
|
const { Rights } = require("..");
|
||||||
|
|
||||||
|
@ -20,8 +20,6 @@
|
|||||||
Regenerates the `spacebarchat/server/assets/schemas.json` file, used for API/Gateway input validation.
|
Regenerates the `spacebarchat/server/assets/schemas.json` file, used for API/Gateway input validation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const TJS = require("typescript-json-schema");
|
const TJS = require("typescript-json-schema");
|
||||||
|
@ -25,8 +25,6 @@
|
|||||||
it doesn't break the below, thus we're left with this :sob:
|
it doesn't break the below, thus we're left with this :sob:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
require("module-alias/register");
|
require("module-alias/register");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
const { initDatabase } = require("..");
|
const { initDatabase } = require("..");
|
||||||
|
@ -25,6 +25,8 @@ import {
|
|||||||
registerRoutes,
|
registerRoutes,
|
||||||
Sentry,
|
Sentry,
|
||||||
WebAuthn,
|
WebAuthn,
|
||||||
|
ConnectionConfig,
|
||||||
|
ConnectionLoader,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { Server, ServerOptions } from "lambert-server";
|
import { Server, ServerOptions } from "lambert-server";
|
||||||
@ -72,6 +74,7 @@ export class SpacebarServer extends Server {
|
|||||||
await Config.init();
|
await Config.init();
|
||||||
await initEvent();
|
await initEvent();
|
||||||
await Email.init();
|
await Email.init();
|
||||||
|
await ConnectionConfig.init();
|
||||||
await initInstance();
|
await initInstance();
|
||||||
await Sentry.init(this.app);
|
await Sentry.init(this.app);
|
||||||
WebAuthn.init();
|
WebAuthn.init();
|
||||||
@ -142,6 +145,8 @@ export class SpacebarServer extends Server {
|
|||||||
|
|
||||||
Sentry.errorHandler(this.app);
|
Sentry.errorHandler(this.app);
|
||||||
|
|
||||||
|
ConnectionLoader.loadConnections();
|
||||||
|
|
||||||
if (logRequests)
|
if (logRequests)
|
||||||
console.log(
|
console.log(
|
||||||
red(
|
red(
|
||||||
|
@ -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+)?/;
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
MessageReactionRemoveEmojiEvent,
|
MessageReactionRemoveEmojiEvent,
|
||||||
MessageReactionRemoveEvent,
|
MessageReactionRemoveEvent,
|
||||||
PartialEmoji,
|
PartialEmoji,
|
||||||
|
PublicMemberProjection,
|
||||||
PublicUserProjection,
|
PublicUserProjection,
|
||||||
User,
|
User,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
@ -180,6 +181,7 @@ router.put(
|
|||||||
if (already_added.user_ids.includes(req.user_id))
|
if (already_added.user_ids.includes(req.user_id))
|
||||||
return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
|
return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
|
||||||
already_added.count++;
|
already_added.count++;
|
||||||
|
already_added.user_ids.push(req.user_id);
|
||||||
} else
|
} else
|
||||||
message.reactions.push({
|
message.reactions.push({
|
||||||
count: 1,
|
count: 1,
|
||||||
@ -191,7 +193,12 @@ router.put(
|
|||||||
|
|
||||||
const member =
|
const member =
|
||||||
channel.guild_id &&
|
channel.guild_id &&
|
||||||
(await Member.findOneOrFail({ where: { id: req.user_id } }));
|
(
|
||||||
|
await Member.findOneOrFail({
|
||||||
|
where: { id: req.user_id },
|
||||||
|
select: PublicMemberProjection,
|
||||||
|
})
|
||||||
|
).toPublicMember();
|
||||||
|
|
||||||
await emitEvent({
|
await emitEvent({
|
||||||
event: "MESSAGE_REACTION_ADD",
|
event: "MESSAGE_REACTION_ADD",
|
||||||
@ -247,6 +254,11 @@ router.delete(
|
|||||||
already_added.count--;
|
already_added.count--;
|
||||||
|
|
||||||
if (already_added.count <= 0) message.reactions.remove(already_added);
|
if (already_added.count <= 0) message.reactions.remove(already_added);
|
||||||
|
else
|
||||||
|
already_added.user_ids.splice(
|
||||||
|
already_added.user_ids.indexOf(user_id),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
await message.save();
|
await message.save();
|
||||||
|
|
||||||
|
@ -73,9 +73,11 @@ export function isTextChannel(type: ChannelType): boolean {
|
|||||||
|
|
||||||
// https://discord.com/developers/docs/resources/channel#create-message
|
// https://discord.com/developers/docs/resources/channel#create-message
|
||||||
// get messages
|
// get messages
|
||||||
router.get("/", async (req: Request, res: Response) => {
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
const channel_id = req.params.channel_id;
|
const channel_id = req.params.channel_id;
|
||||||
const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
|
const channel = await Channel.findOneOrFail({
|
||||||
|
where: { id: channel_id },
|
||||||
|
});
|
||||||
if (!channel) throw new HTTPError("Channel not found", 404);
|
if (!channel) throw new HTTPError("Channel not found", 404);
|
||||||
|
|
||||||
isTextChannel(channel.type);
|
isTextChannel(channel.type);
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
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 "@spacebar/api";
|
import { route } from "@spacebar/api";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
const router: Router = Router();
|
router.post("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
// TODO:
|
||||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
const { connection_name, connection_id } = req.params;
|
||||||
//TODO
|
res.sendStatus(204);
|
||||||
res.json([]).status(200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
52
src/api/routes/connections/#connection_name/authorize.ts
Normal file
52
src/api/routes/connections/#connection_name/authorize.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import { ConnectionStore, FieldErrors } from "../../../../util";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const { 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;
|
71
src/api/routes/connections/#connection_name/callback.ts
Normal file
71
src/api/routes/connections/#connection_name/callback.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import {
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionStore,
|
||||||
|
emitEvent,
|
||||||
|
FieldErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/",
|
||||||
|
route({ body: "ConnectionCallbackSchema" }),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const { 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 connectedAccnt = await connection.handleCallback(body);
|
||||||
|
|
||||||
|
// whether we should emit a connections update event, only used when a connection doesnt already exist
|
||||||
|
if (connectedAccnt)
|
||||||
|
emitEvent({
|
||||||
|
event: "USER_CONNECTIONS_UPDATE",
|
||||||
|
data: { ...connectedAccnt, token_data: undefined },
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
42
src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts
Normal file
42
src/api/routes/guilds/#guild_id/roles/#role_id/member-ids.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { Member } from "@spacebar/util";
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const { guild_id, role_id } = req.params;
|
||||||
|
|
||||||
|
// TODO: Is this route really not paginated?
|
||||||
|
const members = await Member.find({
|
||||||
|
select: ["id"],
|
||||||
|
where: {
|
||||||
|
roles: {
|
||||||
|
id: role_id,
|
||||||
|
},
|
||||||
|
guild_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(members.map((x) => x.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
62
src/api/routes/guilds/#guild_id/roles/#role_id/members.ts
Normal file
62
src/api/routes/guilds/#guild_id/roles/#role_id/members.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { DiscordApiErrors, Member, partition } from "@spacebar/util";
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/",
|
||||||
|
route({ permission: "MANAGE_ROLES" }),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
// Payload is JSON containing a list of member_ids, the new list of members to have the role
|
||||||
|
const { guild_id, role_id } = req.params;
|
||||||
|
const { member_ids } = req.body;
|
||||||
|
|
||||||
|
// don't mess with @everyone
|
||||||
|
if (role_id == guild_id) throw DiscordApiErrors.INVALID_ROLE;
|
||||||
|
|
||||||
|
const members = await Member.find({
|
||||||
|
where: { guild_id },
|
||||||
|
relations: ["roles"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [add, remove] = partition(
|
||||||
|
members,
|
||||||
|
(member) =>
|
||||||
|
member_ids.includes(member.id) &&
|
||||||
|
!member.roles.map((role) => role.id).includes(role_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO (erkin): have a bulk add/remove function that adds the roles in a single txn
|
||||||
|
await Promise.all([
|
||||||
|
...add.map((member) =>
|
||||||
|
Member.addRole(member.id, guild_id, role_id),
|
||||||
|
),
|
||||||
|
...remove.map((member) =>
|
||||||
|
Member.removeRole(member.id, guild_id, role_id),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
39
src/api/routes/guilds/#guild_id/roles/member-counts.ts
Normal file
39
src/api/routes/guilds/#guild_id/roles/member-counts.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import { Role, Member } from "@spacebar/util";
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import {} from "typeorm";
|
||||||
|
|
||||||
|
const router: Router = Router();
|
||||||
|
|
||||||
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const { guild_id } = req.params;
|
||||||
|
await Member.IsInGuildOrFail(req.user_id, guild_id);
|
||||||
|
|
||||||
|
const role_ids = await Role.find({ where: { guild_id }, select: ["id"] });
|
||||||
|
const counts: { [id: string]: number } = {};
|
||||||
|
for (const { id } of role_ids) {
|
||||||
|
counts[id] = await Member.count({ where: { roles: { id }, guild_id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(counts);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
@ -31,7 +31,7 @@ router.get("/", route({}), async (req: Request, res: Response) => {
|
|||||||
process.env.GATEWAY ||
|
process.env.GATEWAY ||
|
||||||
"ws://localhost:3001",
|
"ws://localhost:3001",
|
||||||
defaultApiVersion: api.defaultVersion ?? 9,
|
defaultApiVersion: api.defaultVersion ?? 9,
|
||||||
apiEndpoint: api.endpointPublic ?? "/api",
|
apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json(IdentityForm);
|
res.json(IdentityForm);
|
||||||
|
@ -133,7 +133,9 @@ router.get(
|
|||||||
guild_id,
|
guild_id,
|
||||||
};
|
};
|
||||||
res.json({
|
res.json({
|
||||||
connected_accounts: user.connected_accounts,
|
connected_accounts: user.connected_accounts.filter(
|
||||||
|
(x) => x.visibility != 0,
|
||||||
|
),
|
||||||
premium_guild_since: premium_guild_since, // TODO
|
premium_guild_since: premium_guild_since, // TODO
|
||||||
premium_since: user.premium_since, // TODO
|
premium_since: user.premium_since, // TODO
|
||||||
mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
|
mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectionStore,
|
||||||
|
DiscordApiErrors,
|
||||||
|
FieldErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import RefreshableConnection from "../../../../../../../util/connections/RefreshableConnection";
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also)
|
||||||
|
|
||||||
|
// spotify is disabled here because it cant be used
|
||||||
|
const ALLOWED_CONNECTIONS = ["twitch", "youtube"];
|
||||||
|
|
||||||
|
// NOTE: this route has not been extensively tested, as the required connections are not implemented as of writing
|
||||||
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const { connection_name, connection_id } = req.params;
|
||||||
|
|
||||||
|
const connection = ConnectionStore.connections.get(connection_name);
|
||||||
|
|
||||||
|
if (!ALLOWED_CONNECTIONS.includes(connection_name) || !connection)
|
||||||
|
throw FieldErrors({
|
||||||
|
provider_id: {
|
||||||
|
code: "BASE_TYPE_CHOICES",
|
||||||
|
message: req.t("common:field.BASE_TYPE_CHOICES", {
|
||||||
|
types: ALLOWED_CONNECTIONS.join(", "),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connection.settings.enabled)
|
||||||
|
throw FieldErrors({
|
||||||
|
provider_id: {
|
||||||
|
message: "This connection has been disabled server-side.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectedAccount = await ConnectedAccount.findOne({
|
||||||
|
where: {
|
||||||
|
type: connection_name,
|
||||||
|
external_id: connection_id,
|
||||||
|
user_id: req.user_id,
|
||||||
|
},
|
||||||
|
select: [
|
||||||
|
"external_id",
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"verified",
|
||||||
|
"visibility",
|
||||||
|
"show_activity",
|
||||||
|
"revoked",
|
||||||
|
"token_data",
|
||||||
|
"friend_sync",
|
||||||
|
"integrations",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (!connectedAccount) throw DiscordApiErrors.UNKNOWN_CONNECTION;
|
||||||
|
if (connectedAccount.revoked) throw DiscordApiErrors.CONNECTION_REVOKED;
|
||||||
|
if (!connectedAccount.token_data)
|
||||||
|
throw new ApiError("No token data", 0, 400);
|
||||||
|
|
||||||
|
let access_token = connectedAccount.token_data.access_token;
|
||||||
|
const { expires_at, expires_in, fetched_at } = connectedAccount.token_data;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(expires_at && expires_at < Date.now()) ||
|
||||||
|
(expires_in && fetched_at + expires_in * 1000 < Date.now())
|
||||||
|
) {
|
||||||
|
if (!(connection instanceof RefreshableConnection))
|
||||||
|
throw new ApiError("Access token expired", 0, 400);
|
||||||
|
const tokenData = await connection.refresh(connectedAccount);
|
||||||
|
access_token = tokenData.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ access_token });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectionUpdateSchema,
|
||||||
|
DiscordApiErrors,
|
||||||
|
emitEvent,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// TODO: connection update schema
|
||||||
|
router.patch(
|
||||||
|
"/",
|
||||||
|
route({ body: "ConnectionUpdateSchema" }),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const { connection_name, connection_id } = req.params;
|
||||||
|
const body = req.body as ConnectionUpdateSchema;
|
||||||
|
|
||||||
|
const connection = await ConnectedAccount.findOne({
|
||||||
|
where: {
|
||||||
|
user_id: req.user_id,
|
||||||
|
external_id: connection_id,
|
||||||
|
type: connection_name,
|
||||||
|
},
|
||||||
|
select: [
|
||||||
|
"external_id",
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"verified",
|
||||||
|
"visibility",
|
||||||
|
"show_activity",
|
||||||
|
"revoked",
|
||||||
|
"friend_sync",
|
||||||
|
"integrations",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION;
|
||||||
|
// TODO: do we need to do anything if the connection is revoked?
|
||||||
|
|
||||||
|
if (typeof body.visibility === "boolean")
|
||||||
|
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
|
||||||
|
body.visibility = body.visibility ? 1 : 0;
|
||||||
|
if (typeof body.show_activity === "boolean")
|
||||||
|
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
|
||||||
|
body.show_activity = body.show_activity ? 1 : 0;
|
||||||
|
if (typeof body.metadata_visibility === "boolean")
|
||||||
|
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
|
||||||
|
body.metadata_visibility = body.metadata_visibility ? 1 : 0;
|
||||||
|
|
||||||
|
connection.assign(req.body);
|
||||||
|
|
||||||
|
await ConnectedAccount.update(
|
||||||
|
{
|
||||||
|
user_id: req.user_id,
|
||||||
|
external_id: connection_id,
|
||||||
|
type: connection_name,
|
||||||
|
},
|
||||||
|
connection,
|
||||||
|
);
|
||||||
|
res.json(connection.toJSON());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const { connection_name, connection_id } = req.params;
|
||||||
|
|
||||||
|
const account = await ConnectedAccount.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
user_id: req.user_id,
|
||||||
|
external_id: connection_id,
|
||||||
|
type: connection_name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ConnectedAccount.remove(account),
|
||||||
|
emitEvent({
|
||||||
|
event: "USER_CONNECTIONS_UPDATE",
|
||||||
|
data: account,
|
||||||
|
user_id: req.user_id,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
47
src/api/routes/users/@me/connections/index.ts
Normal file
47
src/api/routes/users/@me/connections/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Request, Response, Router } from "express";
|
||||||
|
import { route } from "@spacebar/api";
|
||||||
|
import { ConnectedAccount, ConnectedAccountDTO } from "@spacebar/util";
|
||||||
|
|
||||||
|
const router: Router = Router();
|
||||||
|
|
||||||
|
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||||
|
const connections = await ConnectedAccount.find({
|
||||||
|
where: {
|
||||||
|
user_id: req.user_id,
|
||||||
|
},
|
||||||
|
select: [
|
||||||
|
"external_id",
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"verified",
|
||||||
|
"visibility",
|
||||||
|
"show_activity",
|
||||||
|
"revoked",
|
||||||
|
"token_data",
|
||||||
|
"friend_sync",
|
||||||
|
"integrations",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(connections.map((x) => new ConnectedAccountDTO(x, true)));
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
23
src/connections/BattleNet/BattleNetSettings.ts
Normal file
23
src/connections/BattleNet/BattleNetSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class BattleNetSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
134
src/connections/BattleNet/index.ts
Normal file
134
src/connections/BattleNet/index.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { BattleNetSettings } from "./BattleNetSettings";
|
||||||
|
|
||||||
|
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!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
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<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.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: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<BattleNetConnectionUser> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<BattleNetConnectionUser>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id.toString());
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id.toString(),
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.battletag,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Discord/DiscordSettings.ts
Normal file
23
src/connections/Discord/DiscordSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class DiscordSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
133
src/connections/Discord/index.ts
Normal file
133
src/connections/Discord/index.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { DiscordSettings } from "./DiscordSettings";
|
||||||
|
|
||||||
|
interface UserResponse {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
discriminator: string;
|
||||||
|
avatar_url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DiscordConnection extends Connection {
|
||||||
|
public readonly id = "discord";
|
||||||
|
public readonly authorizeUrl = "https://discord.com/api/oauth2/authorize";
|
||||||
|
public readonly tokenUrl = "https://discord.com/api/oauth2/token";
|
||||||
|
public readonly userInfoUrl = "https://discord.com/api/users/@me";
|
||||||
|
public readonly scopes = ["identify"];
|
||||||
|
settings: DiscordSettings = new DiscordSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as DiscordSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
// controls whether, on repeated authorizations, the consent screen is shown
|
||||||
|
url.searchParams.append("consent", "none");
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
client_secret: this.settings.clientSecret!,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: `${userInfo.username}#${userInfo.discriminator}`,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/EpicGames/EpicGamesSettings.ts
Normal file
23
src/connections/EpicGames/EpicGamesSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class EpicGamesSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
146
src/connections/EpicGames/index.ts
Normal file
146
src/connections/EpicGames/index.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { EpicGamesSettings } from "./EpicGamesSettings";
|
||||||
|
|
||||||
|
export interface UserResponse {
|
||||||
|
accountId: string;
|
||||||
|
displayName: string;
|
||||||
|
preferredLanguage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpicTokenResponse
|
||||||
|
extends ConnectedAccountCommonOAuthTokenResponse {
|
||||||
|
expires_at: string;
|
||||||
|
refresh_expires_in: number;
|
||||||
|
refresh_expires_at: string;
|
||||||
|
account_id: string;
|
||||||
|
client_id: string;
|
||||||
|
application_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class EpicGamesConnection extends Connection {
|
||||||
|
public readonly id = "epicgames";
|
||||||
|
public readonly authorizeUrl = "https://www.epicgames.com/id/authorize";
|
||||||
|
public readonly tokenUrl = "https://api.epicgames.dev/epic/oauth/v1/token";
|
||||||
|
public readonly userInfoUrl =
|
||||||
|
"https://api.epicgames.dev/epic/id/v1/accounts";
|
||||||
|
public readonly scopes = ["basic profile"];
|
||||||
|
settings: EpicGamesSettings = new EpicGamesSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as EpicGamesSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<EpicTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId}:${this.settings.clientSecret}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<EpicTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse[]> {
|
||||||
|
const { sub } = JSON.parse(
|
||||||
|
Buffer.from(token.split(".")[1], "base64").toString("utf8"),
|
||||||
|
);
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
url.searchParams.append("accountId", sub);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse[]>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo[0].accountId);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo[0].accountId,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo[0].displayName,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Facebook/FacebookSettings.ts
Normal file
23
src/connections/Facebook/FacebookSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class FacebookSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
137
src/connections/Facebook/index.ts
Normal file
137
src/connections/Facebook/index.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { FacebookSettings } from "./FacebookSettings";
|
||||||
|
|
||||||
|
export interface FacebookErrorResponse {
|
||||||
|
error: {
|
||||||
|
message: string;
|
||||||
|
type: string;
|
||||||
|
code: number;
|
||||||
|
fbtrace_id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserResponse {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FacebookConnection extends Connection {
|
||||||
|
public readonly id = "facebook";
|
||||||
|
public readonly authorizeUrl =
|
||||||
|
"https://www.facebook.com/v14.0/dialog/oauth";
|
||||||
|
public readonly tokenUrl =
|
||||||
|
"https://graph.facebook.com/v14.0/oauth/access_token";
|
||||||
|
public readonly userInfoUrl = "https://graph.facebook.com/v14.0/me";
|
||||||
|
public readonly scopes = ["public_profile"];
|
||||||
|
settings: FacebookSettings = new FacebookSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as FacebookSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("display", "popup");
|
||||||
|
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);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl(code);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.name,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/GitHub/GitHubSettings.ts
Normal file
23
src/connections/GitHub/GitHubSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GitHubSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
124
src/connections/GitHub/index.ts
Normal file
124
src/connections/GitHub/index.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { GitHubSettings } from "./GitHubSettings";
|
||||||
|
|
||||||
|
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!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
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<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl(code);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
})
|
||||||
|
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id.toString());
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id.toString(),
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.login,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Reddit/RedditSettings.ts
Normal file
23
src/connections/Reddit/RedditSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class RedditSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
146
src/connections/Reddit/index.ts
Normal file
146
src/connections/Reddit/index.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { RedditSettings } from "./RedditSettings";
|
||||||
|
|
||||||
|
export interface UserResponse {
|
||||||
|
verified: boolean;
|
||||||
|
coins: number;
|
||||||
|
id: string;
|
||||||
|
is_mod: boolean;
|
||||||
|
has_verified_email: boolean;
|
||||||
|
total_karma: number;
|
||||||
|
name: string;
|
||||||
|
created: number;
|
||||||
|
gold_creddits: number;
|
||||||
|
created_utc: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
message: string;
|
||||||
|
error: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RedditConnection extends Connection {
|
||||||
|
public readonly id = "reddit";
|
||||||
|
public readonly authorizeUrl = "https://www.reddit.com/api/v1/authorize";
|
||||||
|
public readonly tokenUrl = "https://www.reddit.com/api/v1/access_token";
|
||||||
|
public readonly userInfoUrl = "https://oauth.reddit.com/api/v1/me";
|
||||||
|
public readonly scopes = ["identity"];
|
||||||
|
settings: RedditSettings = new RedditSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as RedditSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId}:${this.settings.clientSecret}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id.toString());
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
// TODO: connection metadata
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id.toString(),
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.name,
|
||||||
|
verified: userInfo.has_verified_email,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Spotify/SpotifySettings.ts
Normal file
23
src/connections/Spotify/SpotifySettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SpotifySettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
189
src/connections/Spotify/index.ts
Normal file
189
src/connections/Spotify/index.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import RefreshableConnection from "../../util/connections/RefreshableConnection";
|
||||||
|
import { SpotifySettings } from "./SpotifySettings";
|
||||||
|
|
||||||
|
export interface UserResponse {
|
||||||
|
display_name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenErrorResponse {
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error: {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SpotifyConnection extends RefreshableConnection {
|
||||||
|
public readonly id = "spotify";
|
||||||
|
public readonly authorizeUrl = "https://accounts.spotify.com/authorize";
|
||||||
|
public readonly tokenUrl = "https://accounts.spotify.com/api/token";
|
||||||
|
public readonly userInfoUrl = "https://api.spotify.com/v1/me";
|
||||||
|
public readonly scopes = [
|
||||||
|
"user-read-private",
|
||||||
|
"user-read-playback-state",
|
||||||
|
"user-modify-playback-state",
|
||||||
|
"user-read-currently-playing",
|
||||||
|
];
|
||||||
|
settings: SpotifySettings = new SpotifySettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
/**
|
||||||
|
* The way Discord shows the currently playing song is by using Spotifys partner API. This is obviously not possible for us.
|
||||||
|
* So to prevent spamming the spotify api we disable the ability to refresh.
|
||||||
|
*/
|
||||||
|
this.refreshEnabled = false;
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as SpotifySettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId!}:${this.settings.clientSecret!}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
if (!connectedAccount.token_data?.refresh_token)
|
||||||
|
throw new Error("No refresh token available.");
|
||||||
|
const refresh_token = connectedAccount.token_data.refresh_token;
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId!}:${this.settings.clientSecret!}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.unauthorized(async () => {
|
||||||
|
// assume the token was revoked
|
||||||
|
await connectedAccount.revoke();
|
||||||
|
return DiscordApiErrors.CONNECTION_REVOKED;
|
||||||
|
})
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<UserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<UserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
token_data: { ...tokenData, fetched_at: Date.now() },
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.display_name,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Twitch/TwitchSettings.ts
Normal file
23
src/connections/Twitch/TwitchSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TwitchSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
181
src/connections/Twitch/index.ts
Normal file
181
src/connections/Twitch/index.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import RefreshableConnection from "../../util/connections/RefreshableConnection";
|
||||||
|
import { TwitchSettings } from "./TwitchSettings";
|
||||||
|
|
||||||
|
interface TwitchConnectionUserResponse {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
login: string;
|
||||||
|
display_name: string;
|
||||||
|
type: string;
|
||||||
|
broadcaster_type: string;
|
||||||
|
description: string;
|
||||||
|
profile_image_url: string;
|
||||||
|
offline_image_url: string;
|
||||||
|
view_count: number;
|
||||||
|
created_at: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TwitchConnection extends RefreshableConnection {
|
||||||
|
public readonly id = "twitch";
|
||||||
|
public readonly authorizeUrl = "https://id.twitch.tv/oauth2/authorize";
|
||||||
|
public readonly tokenUrl = "https://id.twitch.tv/oauth2/token";
|
||||||
|
public readonly userInfoUrl = "https://api.twitch.tv/helix/users";
|
||||||
|
public readonly scopes = [
|
||||||
|
"channel_subscriptions",
|
||||||
|
"channel_check_subscription",
|
||||||
|
"channel:read:subscriptions",
|
||||||
|
];
|
||||||
|
settings: TwitchSettings = new TwitchSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as TwitchSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
client_secret: this.settings.clientSecret!,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
if (!connectedAccount.token_data?.refresh_token)
|
||||||
|
throw new Error("No refresh token available.");
|
||||||
|
const refresh_token = connectedAccount.token_data.refresh_token;
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
client_secret: this.settings.clientSecret!,
|
||||||
|
refresh_token: refresh_token,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.unauthorized(async () => {
|
||||||
|
// assume the token was revoked
|
||||||
|
await connectedAccount.revoke();
|
||||||
|
return DiscordApiErrors.CONNECTION_REVOKED;
|
||||||
|
})
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<TwitchConnectionUserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Client-Id": this.settings.clientId!,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<TwitchConnectionUserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.data[0].id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
token_data: { ...tokenData, fetched_at: Date.now() },
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.data[0].id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.data[0].display_name,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Twitter/TwitterSettings.ts
Normal file
23
src/connections/Twitter/TwitterSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TwitterSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
183
src/connections/Twitter/index.ts
Normal file
183
src/connections/Twitter/index.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import RefreshableConnection from "../../util/connections/RefreshableConnection";
|
||||||
|
import { TwitterSettings } from "./TwitterSettings";
|
||||||
|
|
||||||
|
interface TwitterUserResponse {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
created_at: string;
|
||||||
|
location: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
verified: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TwitterErrorResponse {
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TwitterConnection extends RefreshableConnection {
|
||||||
|
public readonly id = "twitter";
|
||||||
|
public readonly authorizeUrl = "https://twitter.com/i/oauth2/authorize";
|
||||||
|
public readonly tokenUrl = "https://api.twitter.com/2/oauth2/token";
|
||||||
|
public readonly userInfoUrl =
|
||||||
|
"https://api.twitter.com/2/users/me?user.fields=created_at%2Cdescription%2Cid%2Cname%2Cusername%2Cverified%2Clocation%2Curl";
|
||||||
|
public readonly scopes = ["users.read", "tweet.read"];
|
||||||
|
settings: TwitterSettings = new TwitterSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as TwitterSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
url.searchParams.append("code_challenge", "challenge"); // TODO: properly use PKCE challenge
|
||||||
|
url.searchParams.append("code_challenge_method", "plain");
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId!}:${this.settings.clientSecret!}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
code_verifier: "challenge", // TODO: properly use PKCE challenge
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
if (!connectedAccount.token_data?.refresh_token)
|
||||||
|
throw new Error("No refresh token available.");
|
||||||
|
const refresh_token = connectedAccount.token_data.refresh_token;
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId!}:${this.settings.clientSecret!}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token,
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
code_verifier: "challenge", // TODO: properly use PKCE challenge
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<TwitterUserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<TwitterUserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.data.id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
token_data: { ...tokenData, fetched_at: Date.now() },
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.data.id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.data.name,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Xbox/XboxSettings.ts
Normal file
23
src/connections/Xbox/XboxSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class XboxSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
198
src/connections/Xbox/index.ts
Normal file
198
src/connections/Xbox/index.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { XboxSettings } from "./XboxSettings";
|
||||||
|
|
||||||
|
interface XboxUserResponse {
|
||||||
|
IssueInstant: string;
|
||||||
|
NotAfter: string;
|
||||||
|
Token: string;
|
||||||
|
DisplayClaims: {
|
||||||
|
xui: {
|
||||||
|
gtg: string;
|
||||||
|
xid: string;
|
||||||
|
uhs: string;
|
||||||
|
agg: string;
|
||||||
|
usr: string;
|
||||||
|
utr: string;
|
||||||
|
prv: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface XboxErrorResponse {
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class XboxConnection extends Connection {
|
||||||
|
public readonly id = "xbox";
|
||||||
|
public readonly authorizeUrl =
|
||||||
|
"https://login.live.com/oauth20_authorize.srf";
|
||||||
|
public readonly tokenUrl = "https://login.live.com/oauth20_token.srf";
|
||||||
|
public readonly userInfoUrl =
|
||||||
|
"https://xsts.auth.xboxlive.com/xsts/authorize";
|
||||||
|
public readonly userAuthUrl =
|
||||||
|
"https://user.auth.xboxlive.com/user/authenticate";
|
||||||
|
public readonly scopes = ["Xboxlive.signin", "Xboxlive.offline_access"];
|
||||||
|
settings: XboxSettings = new XboxSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as XboxSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
url.searchParams.append("approval_prompt", "auto");
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserToken(token: string): Promise<string> {
|
||||||
|
return wretch(this.userAuthUrl)
|
||||||
|
.headers({
|
||||||
|
"x-xbl-contract-version": "3",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
JSON.stringify({
|
||||||
|
RelyingParty: "http://auth.xboxlive.com",
|
||||||
|
TokenType: "JWT",
|
||||||
|
Properties: {
|
||||||
|
AuthMethod: "RPS",
|
||||||
|
SiteName: "user.auth.xboxlive.com",
|
||||||
|
RpsTicket: `d=${token}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json((res: XboxUserResponse) => res.Token)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${this.settings.clientId!}:${this.settings.clientSecret!}`,
|
||||||
|
).toString("base64")}`,
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
scope: this.scopes.join(" "),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<XboxUserResponse> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
"x-xbl-contract-version": "3",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
JSON.stringify({
|
||||||
|
RelyingParty: "http://xboxlive.com",
|
||||||
|
TokenType: "JWT",
|
||||||
|
Properties: {
|
||||||
|
UserTokens: [token],
|
||||||
|
SandboxId: "RETAIL",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<XboxUserResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userToken = await this.getUserToken(tokenData.access_token);
|
||||||
|
const userInfo = await this.getUser(userToken);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(
|
||||||
|
userId,
|
||||||
|
userInfo.DisplayClaims.xui[0].xid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
token_data: { ...tokenData, fetched_at: Date.now() },
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.DisplayClaims.xui[0].xid,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.DisplayClaims.xui[0].gtg,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
src/connections/Youtube/YoutubeSettings.ts
Normal file
23
src/connections/Youtube/YoutubeSettings.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class YoutubeSettings {
|
||||||
|
enabled: boolean = false;
|
||||||
|
clientId: string | null = null;
|
||||||
|
clientSecret: string | null = null;
|
||||||
|
}
|
151
src/connections/Youtube/index.ts
Normal file
151
src/connections/Youtube/index.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectedAccount,
|
||||||
|
ConnectedAccountCommonOAuthTokenResponse,
|
||||||
|
ConnectionCallbackSchema,
|
||||||
|
ConnectionLoader,
|
||||||
|
DiscordApiErrors,
|
||||||
|
} from "@spacebar/util";
|
||||||
|
import wretch from "wretch";
|
||||||
|
import Connection from "../../util/connections/Connection";
|
||||||
|
import { YoutubeSettings } from "./YoutubeSettings";
|
||||||
|
|
||||||
|
interface YouTubeConnectionChannelListResult {
|
||||||
|
items: {
|
||||||
|
snippet: {
|
||||||
|
// thumbnails: Thumbnails;
|
||||||
|
title: string;
|
||||||
|
country: string;
|
||||||
|
publishedAt: string;
|
||||||
|
// localized: Localized;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
kind: string;
|
||||||
|
etag: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
kind: string;
|
||||||
|
etag: string;
|
||||||
|
pageInfo: {
|
||||||
|
resultsPerPage: number;
|
||||||
|
totalResults: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class YoutubeConnection extends Connection {
|
||||||
|
public readonly id = "youtube";
|
||||||
|
public readonly authorizeUrl =
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth";
|
||||||
|
public readonly tokenUrl = "https://oauth2.googleapis.com/token";
|
||||||
|
public readonly userInfoUrl =
|
||||||
|
"https://www.googleapis.com/youtube/v3/channels?mine=true&part=snippet";
|
||||||
|
public readonly scopes = [
|
||||||
|
"https://www.googleapis.com/auth/youtube.readonly",
|
||||||
|
];
|
||||||
|
settings: YoutubeSettings = new YoutubeSettings();
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.settings = ConnectionLoader.getConnectionConfig(
|
||||||
|
this.id,
|
||||||
|
this.settings,
|
||||||
|
) as YoutubeSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorizationUrl(userId: string): string {
|
||||||
|
const state = this.createState(userId);
|
||||||
|
const url = new URL(this.authorizeUrl);
|
||||||
|
|
||||||
|
url.searchParams.append("client_id", this.settings.clientId!);
|
||||||
|
url.searchParams.append("redirect_uri", this.getRedirectUri());
|
||||||
|
url.searchParams.append("response_type", "code");
|
||||||
|
url.searchParams.append("scope", this.scopes.join(" "));
|
||||||
|
url.searchParams.append("state", state);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenUrl(): string {
|
||||||
|
return this.tokenUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exchangeCode(
|
||||||
|
state: string,
|
||||||
|
code: string,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
this.validateState(state);
|
||||||
|
|
||||||
|
const url = this.getTokenUrl();
|
||||||
|
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
})
|
||||||
|
.body(
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: code,
|
||||||
|
client_id: this.settings.clientId!,
|
||||||
|
client_secret: this.settings.clientSecret!,
|
||||||
|
redirect_uri: this.getRedirectUri(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.post()
|
||||||
|
.json<ConnectedAccountCommonOAuthTokenResponse>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(token: string): Promise<YouTubeConnectionChannelListResult> {
|
||||||
|
const url = new URL(this.userInfoUrl);
|
||||||
|
return wretch(url.toString())
|
||||||
|
.headers({
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.json<YouTubeConnectionChannelListResult>()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
throw DiscordApiErrors.GENERAL_ERROR;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null> {
|
||||||
|
const userId = this.getUserId(params.state);
|
||||||
|
const tokenData = await this.exchangeCode(params.state, params.code!);
|
||||||
|
const userInfo = await this.getUser(tokenData.access_token);
|
||||||
|
|
||||||
|
const exists = await this.hasConnection(userId, userInfo.items[0].id);
|
||||||
|
|
||||||
|
if (exists) return null;
|
||||||
|
|
||||||
|
return await this.createConnection({
|
||||||
|
token_data: { ...tokenData, fetched_at: Date.now() },
|
||||||
|
user_id: userId,
|
||||||
|
external_id: userInfo.items[0].id,
|
||||||
|
friend_sync: params.friend_sync,
|
||||||
|
name: userInfo.items[0].snippet.title,
|
||||||
|
type: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ import {
|
|||||||
LazyRequestSchema,
|
LazyRequestSchema,
|
||||||
User,
|
User,
|
||||||
Presence,
|
Presence,
|
||||||
|
partition,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import {
|
import {
|
||||||
WebSocket,
|
WebSocket,
|
||||||
@ -302,11 +303,3 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* https://stackoverflow.com/a/50636286 */
|
|
||||||
function partition<T>(array: T[], filter: (elem: T) => boolean) {
|
|
||||||
const pass: T[] = [],
|
|
||||||
fail: T[] = [];
|
|
||||||
array.forEach((e) => (filter(e) ? pass : fail).push(e));
|
|
||||||
return [pass, fail];
|
|
||||||
}
|
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
export class ApiConfiguration {
|
export class ApiConfiguration {
|
||||||
defaultVersion: string = "9";
|
defaultVersion: string = "9";
|
||||||
activeVersions: string[] = ["6", "7", "8", "9"];
|
activeVersions: string[] = ["6", "7", "8", "9"];
|
||||||
endpointPublic: string = "/api";
|
endpointPublic: string | null = null;
|
||||||
}
|
}
|
||||||
|
118
src/util/connections/Connection.ts
Normal file
118
src/util/connections/Connection.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas";
|
||||||
|
import { Config, DiscordApiErrors } from "../util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection that can be used to connect to an external service.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the redirect_uri for a connection type
|
||||||
|
* @returns redirect_uri for this connection
|
||||||
|
*/
|
||||||
|
getRedirectUri() {
|
||||||
|
const endpointPublic =
|
||||||
|
Config.get().api.endpointPublic ?? "http://localhost:3001";
|
||||||
|
return `${endpointPublic}/connections/${this.id}/callback`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the callback
|
||||||
|
* @param args Callback arguments
|
||||||
|
*/
|
||||||
|
abstract handleCallback(
|
||||||
|
params: ConnectionCallbackSchema,
|
||||||
|
): Promise<ConnectedAccount | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Connected Account in the database.
|
||||||
|
* @param data connected account data
|
||||||
|
* @returns the new connected account
|
||||||
|
*/
|
||||||
|
async createConnection(
|
||||||
|
data: ConnectedAccountSchema,
|
||||||
|
): Promise<ConnectedAccount> {
|
||||||
|
const ca = ConnectedAccount.create({ ...data });
|
||||||
|
await ca.save();
|
||||||
|
return ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user has an exist connected account for the given extenal id.
|
||||||
|
* @param userId the user id
|
||||||
|
* @param externalId the connection id to find
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async hasConnection(userId: string, externalId: string): Promise<boolean> {
|
||||||
|
const existing = await ConnectedAccount.findOne({
|
||||||
|
where: {
|
||||||
|
user_id: userId,
|
||||||
|
external_id: externalId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!existing;
|
||||||
|
}
|
||||||
|
}
|
98
src/util/connections/ConnectionConfig.ts
Normal file
98
src/util/connections/ConnectionConfig.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { ConnectionConfigEntity } from "../entities/ConnectionConfigEntity";
|
||||||
|
|
||||||
|
let config: any;
|
||||||
|
let pairs: ConnectionConfigEntity[];
|
||||||
|
|
||||||
|
export const ConnectionConfig = {
|
||||||
|
init: async function init() {
|
||||||
|
if (config) return config;
|
||||||
|
console.log("[Connections] 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);
|
||||||
|
|
||||||
|
// return applyConfig(config);
|
||||||
|
return applyConfig(val);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(`[Connections] WARN: Empty config key`);
|
||||||
|
console.log(pair);
|
||||||
|
} else return pair.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pairsToConfig(pairs: ConnectionConfigEntity[]) {
|
||||||
|
const 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;
|
||||||
|
}
|
86
src/util/connections/ConnectionLoader.ts
Normal file
86
src/util/connections/ConnectionLoader.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import Connection from "./Connection";
|
||||||
|
import { ConnectionConfig } from "./ConnectionConfig";
|
||||||
|
import { ConnectionStore } from "./ConnectionStore";
|
||||||
|
|
||||||
|
const root = "dist/connections";
|
||||||
|
const 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) => {
|
||||||
|
const modPath = path.resolve(path.join(root, x));
|
||||||
|
const mod = new (require(modPath).default)() as Connection;
|
||||||
|
ConnectionStore.connections.set(mod.id, mod);
|
||||||
|
|
||||||
|
mod.init();
|
||||||
|
// console.log(`[Connections] Loaded connection '${mod.id}'`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public static getConnectionConfig(id: string, defaults?: any): any {
|
||||||
|
let cfg = ConnectionConfig.get()[id];
|
||||||
|
if (defaults) {
|
||||||
|
if (cfg) cfg = Object.assign({}, defaults, cfg);
|
||||||
|
else {
|
||||||
|
cfg = defaults;
|
||||||
|
this.setConnectionConfig(id, cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg?.enabled) console.log(`[Connections] ${id} enabled`);
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
config: Partial<any>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!config)
|
||||||
|
console.warn(`[Connections/WARN] ${id} tried to set config=null!`);
|
||||||
|
|
||||||
|
await ConnectionConfig.set({
|
||||||
|
[id]: Object.assign(
|
||||||
|
config,
|
||||||
|
ConnectionLoader.getConnectionConfig(id) || {},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
25
src/util/connections/ConnectionStore.ts
Normal file
25
src/util/connections/ConnectionStore.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Connection from "./Connection";
|
||||||
|
import RefreshableConnection from "./RefreshableConnection";
|
||||||
|
|
||||||
|
export class ConnectionStore {
|
||||||
|
public static connections: Map<string, Connection | RefreshableConnection> =
|
||||||
|
new Map();
|
||||||
|
}
|
48
src/util/connections/RefreshableConnection.ts
Normal file
48
src/util/connections/RefreshableConnection.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces";
|
||||||
|
import Connection from "./Connection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection that can refresh its token.
|
||||||
|
*/
|
||||||
|
export default abstract class RefreshableConnection extends Connection {
|
||||||
|
refreshEnabled = true;
|
||||||
|
/**
|
||||||
|
* Refreshes the token for a connected account.
|
||||||
|
* @param connectedAccount The connected account to refresh
|
||||||
|
*/
|
||||||
|
abstract refreshToken(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the token for a connected account and saves it to the database.
|
||||||
|
* @param connectedAccount The connected account to refresh
|
||||||
|
*/
|
||||||
|
async refresh(
|
||||||
|
connectedAccount: ConnectedAccount,
|
||||||
|
): Promise<ConnectedAccountCommonOAuthTokenResponse> {
|
||||||
|
const tokenData = await this.refreshToken(connectedAccount);
|
||||||
|
connectedAccount.token_data = { ...tokenData, fetched_at: Date.now() };
|
||||||
|
await connectedAccount.save();
|
||||||
|
return tokenData;
|
||||||
|
}
|
||||||
|
}
|
23
src/util/connections/index.ts
Normal file
23
src/util/connections/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Connection";
|
||||||
|
export * from "./ConnectionConfig";
|
||||||
|
export * from "./ConnectionLoader";
|
||||||
|
export * from "./ConnectionStore";
|
||||||
|
export * from "./RefreshableConnection";
|
61
src/util/dtos/ConnectedAccountDTO.ts
Normal file
61
src/util/dtos/ConnectedAccountDTO.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConnectedAccount } from "../entities";
|
||||||
|
|
||||||
|
export class ConnectedAccountDTO {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
access_token?: string;
|
||||||
|
friend_sync?: boolean;
|
||||||
|
name: string;
|
||||||
|
revoked?: boolean;
|
||||||
|
show_activity?: number;
|
||||||
|
type: string;
|
||||||
|
verified?: boolean;
|
||||||
|
visibility?: number;
|
||||||
|
integrations?: string[];
|
||||||
|
metadata_?: any;
|
||||||
|
metadata_visibility?: number;
|
||||||
|
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.token_data && with_token
|
||||||
|
? connectedAccount.token_data.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 || false);
|
||||||
|
this.integrations = connectedAccount.integrations;
|
||||||
|
this.metadata_ = connectedAccount.metadata_;
|
||||||
|
this.metadata_visibility = +(
|
||||||
|
connectedAccount.metadata_visibility || false
|
||||||
|
);
|
||||||
|
this.two_way_link = connectedAccount.two_way_link;
|
||||||
|
}
|
||||||
|
}
|
@ -22,11 +22,11 @@ import {
|
|||||||
ChannelType,
|
ChannelType,
|
||||||
Emoji,
|
Emoji,
|
||||||
Guild,
|
Guild,
|
||||||
Member,
|
|
||||||
PublicUser,
|
PublicUser,
|
||||||
Role,
|
Role,
|
||||||
Sticker,
|
Sticker,
|
||||||
UserGuildSettings,
|
UserGuildSettings,
|
||||||
|
PublicMember,
|
||||||
} from "../entities";
|
} from "../entities";
|
||||||
|
|
||||||
// TODO: this is not the best place for this type
|
// TODO: this is not the best place for this type
|
||||||
@ -67,7 +67,7 @@ export interface IReadyGuildDTO {
|
|||||||
large: boolean | undefined;
|
large: boolean | undefined;
|
||||||
lazy: boolean;
|
lazy: boolean;
|
||||||
member_count: number | undefined;
|
member_count: number | undefined;
|
||||||
members: Member[];
|
members: PublicMember[];
|
||||||
premium_subscription_count: number | undefined;
|
premium_subscription_count: number | undefined;
|
||||||
properties: {
|
properties: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -124,7 +124,7 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
|
|||||||
large: boolean | undefined;
|
large: boolean | undefined;
|
||||||
lazy: boolean;
|
lazy: boolean;
|
||||||
member_count: number | undefined;
|
member_count: number | undefined;
|
||||||
members: Member[];
|
members: PublicMember[];
|
||||||
premium_subscription_count: number | undefined;
|
premium_subscription_count: number | undefined;
|
||||||
properties: {
|
properties: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -191,7 +191,7 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
|
|||||||
this.large = guild.large;
|
this.large = guild.large;
|
||||||
this.lazy = true; // ??????????
|
this.lazy = true; // ??????????
|
||||||
this.member_count = guild.member_count;
|
this.member_count = guild.member_count;
|
||||||
this.members = guild.members;
|
this.members = guild.members?.map((x) => x.toPublicMember());
|
||||||
this.premium_subscription_count = guild.premium_subscription_count;
|
this.premium_subscription_count = guild.premium_subscription_count;
|
||||||
this.properties = {
|
this.properties = {
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
|
@ -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";
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||||
|
import { ConnectedAccountTokenData } from "../interfaces";
|
||||||
import { BaseClass } from "./BaseClass";
|
import { BaseClass } from "./BaseClass";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
|
|
||||||
@ -27,6 +28,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;
|
||||||
@ -38,26 +42,44 @@ export class ConnectedAccount extends BaseClass {
|
|||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
access_token: string;
|
friend_sync?: boolean = false;
|
||||||
|
|
||||||
@Column({ select: false })
|
|
||||||
friend_sync: boolean;
|
|
||||||
|
|
||||||
@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?: number = 0;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
verified: boolean;
|
verified?: boolean = true;
|
||||||
|
|
||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
visibility: number;
|
visibility?: number = 0;
|
||||||
|
|
||||||
|
@Column({ type: "simple-array" })
|
||||||
|
integrations?: string[] = [];
|
||||||
|
|
||||||
|
@Column({ type: "simple-json", name: "metadata", nullable: true })
|
||||||
|
metadata_?: any;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
metadata_visibility?: number = 0;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
two_way_link?: boolean = false;
|
||||||
|
|
||||||
|
@Column({ select: false, nullable: true, type: "simple-json" })
|
||||||
|
token_data?: ConnectedAccountTokenData | null;
|
||||||
|
|
||||||
|
async revoke() {
|
||||||
|
this.revoked = true;
|
||||||
|
this.token_data = null;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
29
src/util/entities/ConnectionConfigEntity.ts
Normal file
29
src/util/entities/ConnectionConfigEntity.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -260,9 +260,9 @@ export class Member extends BaseClassWithoutId {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
await Role.findOneOrFail({ where: { id: role_id, guild_id } }),
|
Role.findOneOrFail({ where: { id: role_id, guild_id } }),
|
||||||
]);
|
]);
|
||||||
member.roles = member.roles.filter((x) => x.id == role_id);
|
member.roles = member.roles.filter((x) => x.id !== role_id);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
member.save(),
|
member.save(),
|
||||||
@ -330,17 +330,25 @@ export class Member extends BaseClassWithoutId {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const memberCount = await Member.count({ where: { guild_id } });
|
const memberCount = await Member.count({ where: { guild_id } });
|
||||||
const memberPreview = await Member.find({
|
|
||||||
where: {
|
const memberPreview = (
|
||||||
guild_id,
|
await Member.find({
|
||||||
user: {
|
where: {
|
||||||
sessions: {
|
guild_id,
|
||||||
status: Not("invisible" as const), // lol typescript?
|
user: {
|
||||||
|
sessions: {
|
||||||
|
status: Not("invisible" as const), // lol typescript?
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
relations: ["user", "roles"],
|
||||||
take: 10,
|
take: 10,
|
||||||
});
|
})
|
||||||
|
).map((member) => ({
|
||||||
|
...member.toPublicMember(),
|
||||||
|
user: member.user.toPublicUser(),
|
||||||
|
roles: member.roles.map((x) => x.id),
|
||||||
|
}));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
await Member.count({
|
await Member.count({
|
||||||
@ -440,6 +448,15 @@ export class Member extends BaseClassWithoutId {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toPublicMember() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const member: any = {};
|
||||||
|
PublicMemberProjection.forEach((x) => {
|
||||||
|
member[x] = this[x];
|
||||||
|
});
|
||||||
|
return member as PublicMember;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChannelOverride {
|
export interface ChannelOverride {
|
||||||
|
@ -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";
|
||||||
|
@ -25,3 +25,4 @@ export * from "./dtos/index";
|
|||||||
export * from "./schemas";
|
export * from "./schemas";
|
||||||
export * from "./imports";
|
export * from "./imports";
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
|
export * from "./connections";
|
||||||
|
35
src/util/interfaces/ConnectedAccount.ts
Normal file
35
src/util/interfaces/ConnectedAccount.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ConnectedAccountCommonOAuthTokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
expires_in?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectedAccountTokenData {
|
||||||
|
access_token: string;
|
||||||
|
token_type?: string;
|
||||||
|
scope?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
expires_in?: number;
|
||||||
|
expires_at?: number;
|
||||||
|
fetched_at: number;
|
||||||
|
}
|
@ -422,6 +422,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 & {
|
||||||
@ -563,6 +567,7 @@ export type EventData =
|
|||||||
| TypingStartEvent
|
| TypingStartEvent
|
||||||
| UserUpdateEvent
|
| UserUpdateEvent
|
||||||
| UserDeleteEvent
|
| UserDeleteEvent
|
||||||
|
| UserConnectionsUpdateEvent
|
||||||
| VoiceStateUpdateEvent
|
| VoiceStateUpdateEvent
|
||||||
| VoiceServerUpdateEvent
|
| VoiceServerUpdateEvent
|
||||||
| WebhooksUpdateEvent
|
| WebhooksUpdateEvent
|
||||||
@ -614,6 +619,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",
|
||||||
@ -665,6 +671,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"
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./Activity";
|
export * from "./Activity";
|
||||||
export * from "./Presence";
|
export * from "./ConnectedAccount";
|
||||||
export * from "./Interaction";
|
|
||||||
export * from "./Event";
|
export * from "./Event";
|
||||||
|
export * from "./Interaction";
|
||||||
|
export * from "./Presence";
|
||||||
export * from "./Status";
|
export * from "./Status";
|
||||||
|
36
src/util/schemas/ConnectedAccountSchema.ts
Normal file
36
src/util/schemas/ConnectedAccountSchema.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ConnectedAccountTokenData } from "../interfaces";
|
||||||
|
|
||||||
|
export interface ConnectedAccountSchema {
|
||||||
|
external_id: string;
|
||||||
|
user_id: string;
|
||||||
|
token_data?: ConnectedAccountTokenData;
|
||||||
|
friend_sync?: boolean;
|
||||||
|
name: string;
|
||||||
|
revoked?: boolean;
|
||||||
|
show_activity?: number;
|
||||||
|
type: string;
|
||||||
|
verified?: boolean;
|
||||||
|
visibility?: number;
|
||||||
|
integrations?: string[];
|
||||||
|
metadata_?: any;
|
||||||
|
metadata_visibility?: number;
|
||||||
|
two_way_link?: boolean;
|
||||||
|
}
|
25
src/util/schemas/ConnectionCallbackSchema.ts
Normal file
25
src/util/schemas/ConnectionCallbackSchema.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ConnectionCallbackSchema {
|
||||||
|
code?: string;
|
||||||
|
state: string;
|
||||||
|
insecure: boolean;
|
||||||
|
friend_sync: boolean;
|
||||||
|
openid_params?: any; // TODO: types
|
||||||
|
}
|
23
src/util/schemas/ConnectionUpdateSchema.ts
Normal file
23
src/util/schemas/ConnectionUpdateSchema.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||||
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ConnectionUpdateSchema {
|
||||||
|
visibility?: boolean;
|
||||||
|
show_activity?: boolean;
|
||||||
|
metadata_visibility?: boolean;
|
||||||
|
}
|
@ -30,6 +30,9 @@ export * from "./ChannelModifySchema";
|
|||||||
export * from "./ChannelPermissionOverwriteSchema";
|
export * from "./ChannelPermissionOverwriteSchema";
|
||||||
export * from "./ChannelReorderSchema";
|
export * from "./ChannelReorderSchema";
|
||||||
export * from "./CodesVerificationSchema";
|
export * from "./CodesVerificationSchema";
|
||||||
|
export * from "./ConnectedAccountSchema";
|
||||||
|
export * from "./ConnectionCallbackSchema";
|
||||||
|
export * from "./ConnectionUpdateSchema";
|
||||||
export * from "./DmChannelCreateSchema";
|
export * from "./DmChannelCreateSchema";
|
||||||
export * from "./EmojiCreateSchema";
|
export * from "./EmojiCreateSchema";
|
||||||
export * from "./EmojiModifySchema";
|
export * from "./EmojiModifySchema";
|
||||||
|
@ -21,3 +21,11 @@
|
|||||||
export function containsAll(arr: unknown[], target: unknown[]) {
|
export function containsAll(arr: unknown[], target: unknown[]) {
|
||||||
return target.every((v) => arr.includes(v));
|
return target.every((v) => arr.includes(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://stackoverflow.com/a/50636286 */
|
||||||
|
export function partition<T>(array: T[], filter: (elem: T) => boolean) {
|
||||||
|
const pass: T[] = [],
|
||||||
|
fail: T[] = [];
|
||||||
|
array.forEach((e) => (filter(e) ? pass : fail).push(e));
|
||||||
|
return [pass, fail];
|
||||||
|
}
|
||||||
|
@ -578,6 +578,7 @@ export const DiscordApiErrors = {
|
|||||||
UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014),
|
UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014),
|
||||||
UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015),
|
UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015),
|
||||||
UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016),
|
UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016),
|
||||||
|
UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400),
|
||||||
UNKNOWN_SESSION: new ApiError("Unknown session", 10020),
|
UNKNOWN_SESSION: new ApiError("Unknown session", 10020),
|
||||||
UNKNOWN_BAN: new ApiError("Unknown ban", 10026),
|
UNKNOWN_BAN: new ApiError("Unknown ban", 10026),
|
||||||
UNKNOWN_SKU: new ApiError("Unknown SKU", 10027),
|
UNKNOWN_SKU: new ApiError("Unknown SKU", 10027),
|
||||||
@ -786,6 +787,11 @@ export const DiscordApiErrors = {
|
|||||||
40006,
|
40006,
|
||||||
),
|
),
|
||||||
USER_BANNED: new ApiError("The user is banned from this guild", 40007),
|
USER_BANNED: new ApiError("The user is banned from this guild", 40007),
|
||||||
|
CONNECTION_REVOKED: new ApiError(
|
||||||
|
"The connection has been revoked",
|
||||||
|
40012,
|
||||||
|
400,
|
||||||
|
),
|
||||||
TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError(
|
TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError(
|
||||||
"Target user is not connected to voice",
|
"Target user is not connected to voice",
|
||||||
40032,
|
40032,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user